于是我们发现,虽然步骤没有变,但是由于地址172FF8中的值改变了,因此call的目标也变了。新的目标中包含了SomeMethod方法的IL代码编译后的机器码,而我们现在看到便是这个机器码的汇编表现形式。
在《使用WinDbg获得托管方法的汇编代码》一文中老赵也曾经做过类似的试验,只是这次更简化了一些。在上一次的回复中,有朋友提问说“在ngen之后,是否便可以直接看到这些汇编代码,即使方法还没有被调用过”。老赵的说法是“可以,但是要观察到这一点并不如现在那样简单”。您能否亲自验证这一点呢?
示例三:泛型方法是为每个类型各生成一份代码吗?
IL和我们平时用的C#程序代码不一样,其中使用了各种指令,而不是像C#那样有类似于英语的关键字,甚至是语法。但是有一点是类似的,它的主要目的是表现程序逻辑,而他们表现得逻辑也大都是相同的,接近的。你创建对象那么我也创建,你调用方法那么我也调用。因此才可以有.NET Reflector帮我们把IL反编译为比IL更高级的C#代码。如果IL把太多细节都展开了,把太多信息都丢弃了,那么怎么可以如此容易就恢复呢?例如,您可以把一篇Word文章转化为图片,那么又如何才能把图片再转回为Word格式呢?C => 汇编、汇编 => C,此类例子数不胜数。
再举一个例子,例如您有以下的范型方法:
{
Console.WriteLine(typeof(T));
}
static void Main(string[] args)
{
GenericMethod<string>();
GenericMethod<int>();
GenericMethod<object>();
GenericMethod<DateTime>();
GenericMethod<Program>();
GenericMethod<double>();
Console.ReadLine();
}
有朋友认为,范型会造成多份代码拷贝。那么您是否知道,使用不同的范型类型去调用GenericMethod方法,会各生成一份机器码吗?我们先看一下IL吧:
.method private hidebysig static void GenericMethod<T>() cil managed
{
.maxstack 8
L_0000: ldtoken !!T
L_0005: call class [mscorlib]System.Type
[mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
L_000a: call void [mscorlib]System.Console::WriteLine(object)
L_000f: ret
}
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 8
L_0000: call void TestConsole.Program::GenericMethod<string>()
L_0005: call void TestConsole.Program::GenericMethod<int32>()
L_000a: call void TestConsole.Program::GenericMethod<object>()
L_000f: call void TestConsole.Program::GenericMethod<valuetype [mscorlib]System.DateTime>()
L_0014: call void TestConsole.Program::GenericMethod<class TestConsole.Program>()
L_0019: call void TestConsole.Program::GenericMethod<float64>()
L_001e: ret
}
这……怎么和我们的C#代码如此接近。嗯,谁让IL清清楚楚明明白白地知道什么叫做“泛型”,于是直接使用这个特性就可以了。所以我们还是用别的办法吧。
其实要了解CLR是否为每个不同类型生成了一份新的机器码,只要看看汇编中是否每次都call到同一个地址中去便可以了。用相同的方法可以看到Main方法的汇编代码如下:
0:003> !u 00a70070
Normal JIT generated code
....Main(System.String[])
Begin 00a70070, size 44
>>> 00a70070 55 push ebp
00a70071 mov ebp,esp
// 准备GenericMethod<string>
00a70073 mov ecx,3A30C4h (MD: ....GenericMethod[[System.String, mscorlib]]())
// 引用类型实际都共享一个GenericMethod<System.__Canon>方法的代码
00a70078 call dword ptr ds:[3A3098h] (....GenericMethod[[System.__Canon, mscorlib]](), ...)
// 调用GenericMethod<int>
00a7007e call dword ptr ds:[3A3108h] (....GenericMethod[[System.Int32, mscorlib]](), ...)
// 准备GenericMethod<object>
00a70084 mov ecx,3A3134h (MD: ....GenericMethod[[System.Object, mscorlib]]())
// 引用类型实际都共享一个GenericMethod<System.__Canon>方法的代码
00a70089 call dword ptr ds:[3A3098h] (....GenericMethod[[System.__Canon, mscorlib]](), ...)
// 调用GenericMethod<DateTime>
00a7008f call dword ptr ds:[3A3178h] (....GenericMethod[[System.DateTime, mscorlib]](), ...)
// 准备GenericMethod<object>
00a70095 mov ecx,3A31A4h (MD: ....GenericMethod[[TestConsole.Program, TestConsole]]())
// 引用类型实际都共享一个GenericMethod<System.__Canon>方法的代码
00a7009a call dword ptr ds:[3A3098h] (....GenericMethod[[System.__Canon, mscorlib]](), ...)
// 调用GenericMethod<double>
00a700a0 call dword ptr ds:[3A31E8h] (....GenericMethod[[System.Double, mscorlib]](), ...)
*** WARNING: Unable to verify checksum for C:\...\mscorlib.ni.dll
// 调用Console.ReadLine()
00a700a6 call mscorlib_ni+0x6d1c24 (71631c24) (System.Console.get_In(), mdToken: 06000771)
00a700ab mov ecx,eax
00a700ad mov eax,dword ptr [ecx]
00a700af call dword ptr [eax+64h]
00a700b2 pop ebp
00a700b3 ret
从这里我们可以看到,CLR为引用类型(string/object/Program)生成共享的机器码,它们都实际上在调用一个GenericMethod<System.__Canon>所生成的代码。而对于每个不同的值类型(int/DateTime/double),CLR则会为每种类型各生成一份。自然,您有充分的理由说:“调用的目标地址不一样,但是可能机器码是相同的”。此外,CLR的“泛型共享机器码”特性也并非如此简单,如果有多个泛型参数(且引用和值类型“混搭”)呢?如果虽然有泛型参数,但是确没有使用呢?关于这些,您可以自行进行验证。本文的目的在于说明一些问题,并非是要把这一细节给深究到底。
总结
以上三个示例都是用IL无法说明的,而这样的问题其实还有很多,例如:
引用类型和值类型是怎么分配的
GC是怎么分代,怎么工作的
Finalizer做什么的,对GC有什么影响
拆箱装箱到底做了些什么
CLR是怎么验证强签名程序集的
跨AppDomain通信是怎么Marshal by ref或by value的
托管代码是怎么做P/Invoke的
……
您会发现,这些东西虽然无法用IL说明,却其中大部分可以说是最最基本的一些.NET/CLR工作方式的常识,更别说一些细节(数组存放方式,方法表结构)了。它们依旧需要别人来告诉您,您就算学会了IL指令,学会了IL表现逻辑的方式,您还是无法自己知道这些。
IL还是太高级了,太高级了,太高级了……CLR作为承载IL的平台,负担的还是太多。与CPU相比,CLR就像一个溺爱孩子的父母,操办了孩子生活所需要的一切。这个孩子一嚷嚷“我要吃苹果”,则父母就会拿过来一个苹果。您咋看这个孩子,都还是无法了解父母是如何获得苹果的(new一个Apple对象),怎么为孩子收拾残局的(GC)。虽然这些经常是所谓的“成年人(.NET 程序员)必知必会”。而您如果盯着孩子看了半天,耐心分析他吃苹果的过程(使用IL编写的逻辑),最后终于看懂了,可惜发现——tmd老子自己也会吃苹果啊(从C#等高级语言中也能看出端倪来)!不过这一点,还是由下一篇文章来分析和论证吧。
这也是为什么各种.NET相关的书,即使是《CLR via C#》或《Essential .NET》此类偏重“内幕”的书,也只是告诉您什么是IL,它能做什么。然后大量的篇幅都是在使用各种示意图配合高级语言进行讲解,然后通过试验来进行验证,不会盯着IL捉摸不停。同理,我们可以看到《CLR via C#》,《CLR via VB.NET》和《CLR via CLI/C++》,但从来没有过《CLR via IL》。IL还是对应于高级语言,直接对应着.NET特性,而不是CLR的内部实现——既然IL无法说明比高级语言更多的东西,那么为什么要“via IL”?同样的例子还有,MSDN Magazine的CLR Inside Out专栏也没有使用IL来讲解内容,Mono甚至使用了与MS CLR不同实现方式来“编译”相同的IL(Mono是不能参考任何CLR和.NET的代码的,一行都看不得)。你要了解CLR?那么多看看Rotor,多看看Mono——看IL作用不大,它既不是您熟悉CLR的必要条件也不是充分条件,因为您关注的不是对IL的读取,甚至不是IL到机器码的转换方式,而是CLR各处所使用的方案。
最后,老赵还是想再补充的一句:本文全篇在使用WinDbg进行探索,这并非要以了解IL作为基础,您完全可以不去关心IL那些缤纷复杂的指令的作用是什么。甚至于您完全忽略IL的存在,极端地“认为”是C#直接编译出的机器码,也不妨碍您来使用本文的做法来一探究竟——细节上会有不同,但是看到的东西是一样的。
不过这并不意味着,您不需要了解一些额外的东西。就老赵看来,您需要具备哪些条件呢?
学习计算机组成原理,计算机体系结构等基础课程的内容,至少是这些课程中的基础。
以事实为基准,而不是“认为是,应该是”的办事方式。
严谨的态度,缜密的逻辑,大胆的推测。
……
“大胆的推测”和“认为是,应该是”并非一个意思。大胆的推测是根据已知现象,运用逻辑进行判断,从而前进,而最终这些推测要通过事实进行确定。正所谓“大胆推测,小心求证”。
以上这些是您“自行进行探索”所需要的条件,而如果您只是要“看懂”某个探索过程的话,就要看“描述”者的表达情况了。一般来说,看懂一个探索过程的要求会低很多,相信只要您有耐心,并且有一些基本概念(与这些条件有关,与IL无关),想要看懂老赵的探索过程,以及吸收最后的结论应该不是一件困难的事情。
原文地址:http://www.cnblogs.com/jeffreyzhao/archive/2009/06/03/my-view-of-il-2-il-shows-little-about-clr.html