我们.NET开发人员必定离不开IL,就算您没有学习,也一定可以在各处看到它的身影。最近在自由互联上活跃的IL文章译者包建强同学的一些看法让我大为震惊,决定独立开篇,希望可以让大家看到不同的声音。真理越辩越明,也欢迎大家来一起讨论,发表自己意见。我也会尽量把朋友们留在我博客上的看法汇总起来,并加以回应。
《我谈IL》也是系列文章,目前的计划有4篇,依次是:
- IL是什么,它又不是什么?那么汇编呢?。
- CLR内部有太多太多IL看不到的东西,包括您平时必须了解的那些(示例)。
- IL可以看到的东西,基本上都可以使用C#等高级语言来发现(示例)。
- 什么时候应该学IL,该怎么学IL。
您现在看到的便是本系列的第1篇:IL是什么,它又不是什么?那么汇编呢?。
我曾经在博客《浅谈尾递归的优化方式》和《使用WinDbg获得托管方法的汇编代码》涉及到了一些x86汇编,包同学在文章后留言,认为通过UltraEdit32此类编辑器观察到x86汇编:
我第三次仔细看了一遍最后这段代码,这段代码应该是从UltraEdit32中看到的,所以里面很多word不是IL中的关键字……
……就像我昨天提到用UltraEdit32这个工具来解读汇编代码,道理是一样的……
同样的事情发生在我最近写的一篇文章《从汇编入手,探究泛型的性能问题》以及包同学自己的文章后面的回复中:
如果不熟悉IL,又怎么能自己动手分析性能呢?你这篇文章不就证明了学习一点IL的重要性了么?
其实IL就是一门汇编语言,很奇怪有人一边在用IL Assembler分析性能,一边又在讲不要学习IL的话……
包同学作为微软MVP,是许多IL文章的译者,还有一本译作《Expert .NET 2.0 IL Assembler》即将出版,本应是这方面的专家。但是我现在非常担心这位专家的话会给许多学习.NET的朋友们一些较为严重的误导。从包同学之前的话来看,他对于IL和汇编的概念,从各自的作用到获取方式几乎完全混淆起来。因此冲动的我每次看到这样的言论都忍不住跳出来批驳一番,而这次更决定独立成文进行详细说明。
IL是微软.NET平台上衍生出来的一门中间语言,.NET平台上的各种高级语言(如C#,VB,F#)的编译器会将各自的文字表述方式转化为IL。各种不同的文字形式最终被统一到了IL的表述方式,其中包含了.NET平台上的各种元素,如“范型”,“类”、、“接口”、“模块”、“属性”等等。值得注意的是,各种高级语言本身可能根本没有这些“概念”在里头,如IronScheme是一个在.NET平台上的Scheme语言实现,其中根本没有前面提到的这些IL——亦或说是.NET平台上的名词。IL本身并不知道自己是由哪种高级语言转化而来的,哪种语言中有哪些特性,IL也根本不会关心。
谁来关心这些呢?自然是各语言的编译器了。这就是.NET平台上的高级语言的第一次转化:高级语言 => IL。
而我们平时说的“汇编”则要简单得多,这个简单并不代表“容易掌握,方便使用”,这个“简单”是指它的“定义”。汇编是让CPU直接使用的“语言”,请注意“直接”二字:一条汇编指令便是让CPU作一件事情(如寄存器的复制,从内存中读取数据等等),毫无二义。不同族CPU拥有不同的指令集,但是它们都有一样的特征:指令的数量相对较少,每个指令功能都简单之至。这也是为什么现在几乎没有人直接使用汇编写程序的原因,试想一下给您红、绿、蓝三原色,让您绘制一幅色彩绚丽的图画有多么困难。
由于CPU只认识汇编代码,因此就算是IL也需要再次进行转化,才能被CPU执行。这次转化便由“JIT Compiler”完成。CLR加载了IL之后,当每个方法——请注意这是IL中的概念——第一次被执行时,就会使用JIT将IL代码进行编译为机器码——对了,刚才我忘了提,机器码和汇编其实也是一一对应的,您可以这样理解:汇编是机器码的文字表现形式,提供了一些方便人们记忆的“助记符”。与IL不同的是,CLR,JIT都是真正了解CPU的,对于同样的IL,JIT会把它为不同的CPU架构(如x86/IA64等等)生成不同的机器码。这也是Java/.NET中“Compile Once,Run Everywhere”这一口号的技术基础:它们为不同的CPU架构提供了不同的“IL转化器”,仅此而已。与高级语言到IL的转化类似,CPU也完全不知道自己在执行的指令是从哪里来的,可能是JIT从IL转化而来,可能是JVM从Java Bytecode转化而来,也有可能是C语言编译得来,也有可能是由MIT/GNU Scheme解释而来。
这就是.NET平台上的高级语言在机器上运行的第二次转化:IL => 汇编(机器码)。
因此,IL和汇编的区别是显著的。IL拥有各种高级特性,它知道什么是范型,什么是类和方法(以及它们的“名称”),什么是继承,什么是字符串,布尔值,什么是User对象。而CPU只知道寄存器,地址,内存,01010101。与汇编相比,IL简直太高级了,几乎完全是一个高级语言,比C语言还要高级。因此,您会看到.NET Reflector几乎可以把IL代码“一五一十”地反编译为可读性良好的C#代码,包括类,属性,方法等等;而从汇编只能勉勉强强地反编译为C语言——而且其中的“方法名”等信息已经完全不可恢复了,更别说“模块”等高级抽象的内容。您想要把汇编反编译成C#代码?相信在将来这是可行的,不过现在这还是天方夜谭。
那么我们再来看看包同学的观点,例如首先他认为“用UltraEdit32这个工具来解读汇编代码”。如果您理解了我之前的解释,应该可以意识到这完全是一种谬论。IL是一种高度抽象,在运行之前,还需要由JIT转化为机器码才行。同样的IL代码,可以由不同CPU架构下的JIT编译成不同的机器码(同样的IL代码在同样的机器上是否也生成同样的机器码呢?答案是否定的,例如“泛型”……下一篇文章中我们会对此进行观察)。甚至于,CLR在运行了一段时间之后,可以让JIT重新生成一段更适合当前环境,性能更高的机器码供CPU执行。从这个角度上说,IL是静态的,而汇编是动态的。设法使用一个静态查看工具UltraEdit32来阅读一个动态的,不确定的内容,这又该如何实现呢?
不过真要说起来,使用UltraEdit32从理论上的确可以阅读一个编译后的IL代码,因为此时IL已经以二进制的形式存储在程序集文件中。例如IL Disassembler(ildasm.exe)和.NET Reflector便是通过读取程序集文件的数据(而不是在将程序集加载到CLR中)来获得IL代码,微软也发布了Common Compiler Infrastructure: Metadata和CCI: Code and AST两个和.NET基础结构有关的开源项目。而近在“自由互联”中,也有Anders Liu大牛写过一个CliPeViewer,从程序集的物理结构中读取其元数据,再进一步便可获取到IL代码了。
虽然已经有了多次请求,但是包同学还是没有公布他使用UltraEdit32获取汇编代码的做法。窃以为,是因为“这个真无法实现”的原因吧。
至于包同学的另一个看法是,我使用IL Assembler(他应该是指IL Disassembler)查看汇编代码所犯的错误同样是混淆了IL和汇编两种截然不同的东西。那些都是我在程序运行之后,使用WinDbg观察汇编的结果,也就是说,是JIT将IL进行再次编译(第一次是指高级语言编译器生成IL)的结果。由于JIT每次进行处理的最小单元是“方法”,因此如果一个.NET方法还没有执行过,则是无法获取它的汇编代码的。我在《使用WinDbg获得托管方法的汇编代码》一文中清楚地演示了目标方法在Before JIT和After JIT两个不同情况下,由WinDbg观察到的结果。
由于IL还是过于高级,因此很多真真切切的东西并无法看到,因此我也不得不用汇编从根本上证实了泛型不会降低性能——严格来说,由于IL在不同平台上生成的汇编不同,我其实也只是证实了在x86平台上的这个结论。请您回想一下,您看过的.NET书籍中有多少是使用IL来说明问题的呢?为了心中踏实,我刚才又去翻了翻《Essential .NET》、《CLR via C#》这《Customizing the .NET Common Language Runtime》这几本“偏底层”的书,证实了这一观点。其实理由很简单,一是大师能够用朴实的语言,或更易理解的高级代码把问题明明白白彻彻底底地讲清楚,二便是因为IL可以说明的东西实在有限。例如,泛型在运行期间是否生成了新类型?CLR对待引用类型和值类型的泛型时是否一样?
在包同学之前的文章中,横道天笑也回复了类似的看法:
……我觉得IL的作用就仅此而已,对于其他,甚至是虚方法调用等等从IL上都看不出来什么。因为你真的不知道callvir指令内部到底干了啥,更别说性能了,因为IL之后还有个JIT,JIT之后才得到的是我们传统上的汇编,汇编语言与机器指令是一一对应的,所以从汇编语言上才能发现一切的一切,比如多态靠的是对象地址加偏移,比如我的那篇汇编讨论性能的文章。
这篇文章的目的是“讲道理”,在接下来了两篇文章中,我还会通过示例,也就是“摆事实”来解释为什么说“CLR内部有太多太多IL看不到的东西,包括平时您必须了解的那些”,以及“IL可以看到的东西,基本上都可以使用C#等高级语言来发现”。
最后,为了避免给大家带来误导,我还是希望多补充几句。我的这几篇文章似乎是在“呼吁”大家不要学习IL,其实不然。我是希望在澄清事实的的基础上,探究出一些更有实践价值结论。例如学习IL和汇编有什么好处,什么是对IL和汇编的滥用,以及究竟该如何可以更快速方便地解决某些问题等等——例如我使用汇编来说明泛型的性能问题就是一种滥用,最好的方法应该是“编写试验代码”。
因此,本系列文章的最后一篇想讨论的话题便是“什么时候应该学IL,该怎么学IL”。在这里,我也请大家留下您的观点,真理便是在各种观点中总结出来的,不是吗?
看了大家的评论,我觉得可能还需要把汇编和机器码的关系进行一些补充。
- 有朋友提出,汇编不是和机器码一一对应的,因为还有宏汇编,高级汇编等等。其实高级汇编其实已经是一种“语言”了,它最终还是被转换成机器码执行。我说的汇编是指汇编“指令”,由机器码简单一一替换成助记符给人看。
- 还有某位朋友指出的一点,如果JIT生成的是汇编,那么肯定无法给机器直接执行。文章里可能没有说清楚,我的意思是,JIT生成机器码,而汇编是机器码进行简单替换后,方便给人看的结果。阅读汇编代码,就完全了解了CPU是如何一步一步进行执行任务的,没有丝毫偏差。这也是我认为观察汇编就是阅读机器码的原因。
- 老赵谈IL(1):IL是什么,它又不是什么?那么汇编呢?
- 老赵谈IL(2):CLR内部有太多太多IL看不到的东西,包括您平时必须了解的那些
- 老赵谈IL(3):IL可以看到的东西,其实大都也可以用C#来发现
- 老赵谈IL(4):什么时候应该学IL,该怎么学IL