IL代码分析方法
Hello, world历史
.NET学习方法论
1. 引言
1988年Brian W. Kernighan和Dennis M. Ritchie合著了软件史上的经典巨著《The C programming Language》,我推荐所有的程序人都有机会重温这本历史上的经典之作。从那时起,Hello, world示例就作为了几乎所有实践型程序设计书籍的开篇代码,一直延续至今,除了表达对巨人与历史的尊重,本文也以Hello, world示例作为我们扣开IL语言的起点,开始我们循序渐进的IL认识之旅。
2. 从Hello, world开始
首先,当然是展示我们的Hello, world代码,开始一段有益的分享。
using System;
using System.Data;
public class HelloWorld
{
public static void Main()
{
Console.WriteLine("Hello, world.");
}
}
这段代码执行了最简单的过程,向陌生的世界打了一个招呼,那么运行在高级语言背后真相又是什么呢,下面开始我们基于上述示例的IL代码分析。
3. IL体验中心
对编译后的可执行文件HelloWorld.exe应用ILDasm.exe反编译工具,还原HelloWorld的为文本MSIL编码,至于其工作原理我们期望在系列的后续文章中做以交代,我们查看其截图为:
![[你必须知道的.NET]第十三回:从Hello, world开始认识IL](http://www.cqzol.com/programming/UploadFiles_7546/200902/2009022607070075.jpeg)
由上图可知,编译后的IL结构中,包含了MANIFEST和HelloWorld类,其中MANIFEST是个附加信息列表,主要包含了程序集的一些属性,例如程序集名称、版本号、哈希算法、程序集模块等,以及对外部引用程序集的引用项;而HelloWorld类则是我们下面介绍的主角。
3.1 MANIFEST清单分析
打开MANIFEST清单,我们可以看到
图片看不清楚?请点击这里查看原图(大图)。
从这段IL代码中,我们的分析如下:
.assembly指令用于定义编译目标或者加载外部库。在IL清单中可见,.assembly extern mscorlib表示外部加载了外部核心库mscorlib,而.assembly HelloWorld则表示了定义的编译目标。值得注意的是,.assembly将只显示程序中实际应用到的程序集列表,而对于加入using引用的程序集,如果并未在程序中引用,则编译器会忽略多加载的程序集,例如System.Data将被忽略,这样就有效避免了过度加载引起的代码膨胀。
我们知道mscorlib.dll程序集定义managed code依赖的核心数据类型,属于必须加载项。 例如接下来要分析的.ctor指令表示构造函数,从代码中我们知道没有为HelloWord类提供任何显示的构造函数,因此可以肯定其继承自基类System.Object,而这个System.Object就包含在mscorlib程序集中。
在外部指令中还会指明了引用版本(.ver);应用程序实际公钥标记(.publickeytoken),公钥Token是SHA1哈希码的低8位字节的反序(如下图所示),用于唯一的确定程序集;还包括其他信息如语言文化等。
![[你必须知道的.NET]第十三回:从Hello, world开始认识IL](http://www.cqzol.com/programming/UploadFiles_7546/200902/2009022607070146.jpeg)
HelloWorld程序集中包括了.hash algorithm指令,表示实现安全性所使用的哈希算法,系统缺省为0x00008004,表明为SHA1算法;.ver则表示了HelloWorld程序集的版本号;
程序集由模块组成, .module为程序集指令,表明定义的模块的元数据,以指定当前模块。
其他的指令还有:imagebase为影像基地址;.file alignment为文件对齐数值;.subsystem为连接系统类型,0x0003表示从控制台运行;.corflags为设置运行库头文件标志,默认为1;这些指令不是我们研究的重点,详细的信息请参考MSDN相关信息。
3.2 HelloWorld类分析
首先是HelloWorld类,代码为:
.class public auto ansi beforefieldinit HelloWorld
extends [mscorlib]System.Object
{
} // end of class HelloWorld
.class表明了HelloWorld是一个public类,该类继承自外部程序集mscorlib的System.Object类。
public为访问控制权限,这点很容易理解。
auto表明程序加载时内存的布局是由CLR决定的,而不是程序本身
ansi属性则为了在没有被管理和被管理代码间实现无缝转换。没有被管理的代码,指的是没有运行在CLR运行库之上的代码,例如原来的C,C++代码等。
beforefieldinit属性为HelloWorld提供了一个附加信息,用于标记运行库可以在任何时候执行类型构造函数方法,只要该方法在第一次访问其静态字段之前执行即可。如果没有beforefieldinit则运行库必须在某个精确时间执行类型构造函数方法,从而影响性能优化,详细的情况可以参与MSDN相关内容。
然后是.ctor方法,代码为:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// 代码大小 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method HelloWorld::.ctor
cil managed 说明方法体中为IL代码,指示编译器编译为托管代码。
.maxstack表明执行构造函数.ctor期间的评估堆栈(Evaluation Stack)可容纳数据项的最大个数。关于评估堆栈,其用于保存方法所需变量的值,并在方法执行结束时清空,或者存储一个返回值。
IL_0000,是一个标记代码行开头,一般来说,IL_之前的部分为变量的声明和初始化。
ldarg.0 表示装载第一个成员参数,在实例方法中指的是当前实例的引用,该引用将用于在基类构造函数中调用。
call指令一般用于调用静态方法,因为静态方法是在编译期指定的,而在此调用的是构造函数.ctor()也是在编译期指定的;而另一个指令callvirt则表示调用实例方法,它的调用过程有异于call,函数的调用是在运行时确定的,首先会检查被调用函数是否为虚函数,如果不是就直接调用,如果是则向下检查子类是否有重写,如果有就调用重写实现,如果没有还调用原来的函数,依次类推直到找到最新的重写实现。
ret表示执行完毕,返回。
最后是Main方法,代码为:
.method public hidebysig static void Main() cil managed
{
.entrypoint
// 代码大小 11 (0xb)
.maxstack 8
IL_0000: ldstr "Hello, world."
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method HelloWorld::Main
.entrypoint指令表明了CLR加载程序HelloWorld.exe时,是首先从.entrypoint方法开始执行的,也就是表明Main方法将作为程序的入口函数。每个托管程序必须有并且只有一个入口点。这区别于将Main函数作为程序入口标志。
ldstr指令表示将字符串压栈,"Hello, world."字符串将被移到stack顶部。CLR通过从元数据表中获得文字常量来构造string对象,值得注意的是,在此构造string对象并未出现在《第五回:深入浅出关键字---把new说透》中提到的newobj指令,对于这一点的解释我们将在下一回中做简要分析。
hidebysig属性用于表示如果当前类作为父类时,类中的方法不会被子类继承,因此HelloWorld子类中不会看到Main方法。
接下来的一点补充:
关于注释,IL代码中的注释和C#等高级语言的注释相同,其实编译器在编译IL代码时已经将所有的注释去掉,所以任何对程序的注释在IL代码中是看不见的。
3.3 回归简洁
去粗取精,我们的IL代码可以简化,下面的代码是基于上面的分析,并去处不重要的信息,以更简洁的方式来展现的HelloWorld版IL代码,详细的分析就以注释来展开吧。
4. 结论
结束本文,我们从一个点的角度和IL来了一次接触,除了了解几个重要的指令含义,更重要的是已经走进了IL的世界。通过一站式的扫描HelloWorld的IL编码,我们还不足以从全局来了解IL,不过第一次的亲密接触至少让我们太陌生,而且随着系列文章的深入我们将逐渐建立起这种认知,从而提高我们掌握了解.NET底层的有效工具。本系列也将在后续的文章中,逐渐建立起这种使用工具的方法,敬请关注。