技术开发 频道

CLR内部有太多太多IL看不到的东西

 于是我们发现,虽然步骤没有变,但是由于地址172FF8中的值改变了,因此call的目标也变了。新的目标中包含了SomeMethod方法的IL代码编译后的机器码,而我们现在看到便是这个机器码的汇编表现形式。

 在《使用WinDbg获得托管方法的汇编代码》一文中老赵也曾经做过类似的试验,只是这次更简化了一些。在上一次的回复中,有朋友提问说“在ngen之后,是否便可以直接看到这些汇编代码,即使方法还没有被调用过”。老赵的说法是“可以,但是要观察到这一点并不如现在那样简单”。您能否亲自验证这一点呢?

 示例三:泛型方法是为每个类型各生成一份代码吗?

 IL和我们平时用的C#程序代码不一样,其中使用了各种指令,而不是像C#那样有类似于英语的关键字,甚至是语法。但是有一点是类似的,它的主要目的是表现程序逻辑,而他们表现得逻辑也大都是相同的,接近的。你创建对象那么我也创建,你调用方法那么我也调用。因此才可以有.NET Reflector帮我们把IL反编译为比IL更高级的C#代码。如果IL把太多细节都展开了,把太多信息都丢弃了,那么怎么可以如此容易就恢复呢?例如,您可以把一篇Word文章转化为图片,那么又如何才能把图片再转回为Word格式呢?C => 汇编、汇编 => C,此类例子数不胜数。

 再举一个例子,例如您有以下的范型方法:

private static void GenericMethod<T>()

 {

 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

0
相关文章