[你必须知道的.NET]第十三回:从Hello, world开始认识IL

发布日期:2007.7.22 作者:Anytao

©2007 Anytao.com ,原创作品,转贴请注明作者和出处。

本文将介绍以下内容:

  • 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编码,至于其工作原理我们期望在系列的后续文章中做以交代,我们查看其截图为:

Anytao.com
 

 

由上图可知,编译后的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位字节的反序(如下图所示),用于唯一的确定程序集;还包括其他信息如语言文化等。

 

  • 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底层的有效工具。本系列也将在后续的文章中,逐渐建立起这种使用工具的方法,敬请关注。

 

 

参考文献

(USA)Joe Duffy , Professional .NET Framework 2.0

(USA)David Chappell, Understanding .NET

 

CLR团队公告

CLR基础研究团队成立了,本团队以研究和探求CLR基础和.NET Framework底层知识为宗旨,在此热烈的欢迎博客园的朋友们。如果对CLR及.NET底层研究有兴趣,请申请加入CLR基础研究团队,在一个专注的环境里共享你的技术。在申请之前请您阅读团队纲领,CLR团队信息如下:

团队地址:http://clr.cnblogs.com/
团队纲领:查看
团队申请:进入

团队近期活动:整理收集团队成员CLR系列文章,在团队公告展播。 

温故知新

[开篇有益]
[第一回:恩怨情仇:is和as]
[第二回:对抽象编程:接口和抽象类]
[第三回:历史纠葛:特性和属性]
[第四回:后来居上:class和struct]
[第五回:深入浅出关键字---把new说透]
[第六回:深入浅出关键字---base和this]
[第七回:品味类型---从通用类型系统开始]
[第八回:品味类型---值类型与引用类型(上)-内存有理]
[第九回:品味类型---值类型与引用类型(中)-规则无边]
[第十回:品味类型---值类型与引用类型(下)-应用征途]
[第十一回:参数之惑---传递的艺术(上)]
[第十二回:参数之惑---传递的艺术(下)]

©2007 Anytao.com

原创作品,转贴请注明作者和出处,留此信息。

本贴子以现状提供且没有任何担保,同时也没有授予任何权利。
This posting is provided "AS IS" with no warranties, and confers no rights.

posted @ 2007-07-22 22:11 Anytao 阅读(9884) 评论(29) 编辑 收藏

 回复 引用   
#1楼 2007-07-22 22:40 kisskiki[未注册用户]
对于底层我的基础确实很薄弱,所以一直关注你的文章,这篇文章看了以后有几个疑问
1 静态方法不是不用实例化该类就可以调用么?所以我觉得编译的时候也不会将class的实例化也弄进到il里面?
2 如果执行的时候是怎么搜索到后面讲的entrypoint了?莫非这个入口地址放在
MANIFEST里面么?

 回复 引用 查看   
#2楼 2007-07-22 23:24 overred      
不错
《.net程序设计框架》相信你百看不厌

 回复 引用 查看   
#3楼[楼主] 2007-07-22 23:55 Anytao      
@kisskiki
1 call指令用于调用静态函数或者共享函数,IL默认情况下,是静态调用。所以从上述示例中可知,call instance void [mscorlib]System.Object::.ctor()用于调用基类的构造函数。
2 关于entrypoint指令应该和MANIFEST没有什么太多联系,编译器能够根据MANIFEST清单的编译程序集和模块确定目标编译对象,至于以何种机制确定entrypoint,还需要时间了解了解。
:-)

 回复 引用 查看   
#4楼[楼主] 2007-07-22 23:56 Anytao      
@overred
:-)

 回复 引用 查看   
#5楼 2007-07-23 01:47 Adrian.      
@公钥Token是SHA1哈希码的第8位字节
表达有误, Public Key Token是公钥(160字节长)的SHA1 hash后的末尾8字节的倒序..
这有个代码示例:http://www.cnblogs.com/Dah/archive/2007/06/21/792302.html

IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret

@ldarg.0 表示装载第一个成员参数,也就是默认构造函数将默认初始化为0值。
ldarg.0 的确是表示把参数0压栈, 但在实例方法里指的是当前实例引用, 也就是C#中的"this", 它是一个看不到的参数, 随之调用的Object的ctor(当然是实例方法)即需要这个"this"引用.

@call指令一般用于调用静态方法,在此调用的当然是构造函数.ctor();而另一个指令callvirt则表示调用实例方法。
这个表述感觉有点误导, 构造器不是静态方法, 但却用了call, 其实call与callvirt最大的区别是callvirt可以调用晚期绑定的方法, 而构造器是编译时指定的..

 回复 引用 查看   
#6楼[楼主] 2007-07-23 08:50 Anytao      
@Adrian.
对不起, 有些表达有误, 及时更改.

 回复 引用 查看   
#7楼 2007-07-23 09:21 紫色阴影      
@Anytao
call指令也可以调用实例方法,只是它不负责检查this是否为null,而且不能用于多态调用。callvirt会根据方法进行判断,如果是虚方法就进行多态调用,如果不是就普通调用,并且会检查this是否为null,如果是就抛出异常。
但是这种情况:
override void Foo()
{
base.Foo();//这里用call指令调用即使它是虚方法,因为用callvirt调用会引起无限递归
}

 回复 引用   
#8楼 2007-07-23 09:29 刘荣华
Anytao兄。
call和callvirt的说明措辞确实显得简单了点,呵呵。再详细点就好啦。

支持!

 回复 引用 查看   
#9楼[楼主] 2007-07-23 09:51 Anytao      
@紫色阴影
受教,回头看看先。

 回复 引用 查看   
#10楼[楼主] 2007-07-23 09:53 Anytao      
@刘荣华
因为是开篇,属于入门级的,还不想铺开来深入,希望以后系列里深入展开,呵呵,可是等着你的垃圾回收后续哩,希望能往下继续写,值得期待。

 回复 引用   
#11楼 2007-07-23 10:13 刘荣华
@Anytao
本来是打算周末能POST一篇的,但是周末老婆要shopping,无奈只能。。。哈哈

写这个东西不太好把握这个度。
写浅了怕挨砖,往深里写的话,却又发现每一个细小的东西都可以引发的出长篇大论来。列了一个提纲才发现这个任务是非常艰巨,是需要长期投入啊。:)

 回复 引用 查看   
#12楼[楼主] 2007-07-23 10:32 Anytao      
@刘荣华
呵呵, 和你有同样的感受, 我的List已经有十几篇要完成,是个费时费力的事情,不过总体感觉过程还是很享受的,同时我发现写作的过程也是对自身知识体系的完善过程,很受用。
至于文章的深浅,我们都不必在意太多,大部分的朋友都是兼容并包的,对于意见我们虚心接受,也是对自己的提高。
我的邮件是taotalk@126.com,有时间聊聊:-)

 回复 引用 查看   
#13楼[楼主] 2007-07-23 13:53 Anytao      
@空间租用
广告已经删除。

 回复 引用   
#14楼 2007-07-23 17:16 blindsniper[未注册用户]
看到call和callvirt指令的时候就想起了你在《第五回:深入浅出关键字---把new说透》里面说的new关键字作为修饰符,用于向基类成员隐藏继承成员的功能

这儿callvirt指令正好可以把多态和new作为修饰符说得更清楚了吧
callvirt根据当前引用,查看被调用的函数是否是虚函数,不是则直接调用该函数;如果是,则在该对象空间内向下查找是否有重写实现,如没有,则也直接调用该函数,如有,则调用重写实现;继续进行上述过程,直到找到最新重写实现,如果遇到了newobj关键字,那么就调用上一级的此方法

不知道自己说清楚没有...

 回复 引用 查看   
#15楼[楼主] 2007-07-24 11:17 Anytao      
@blindsniper
运行时callvirt会查找虚拟方法表,new关键字作为修饰符时,可以中断基类接口成员的虚拟方法表,这是由newslot标记的。

 回复 引用 查看   
#16楼 2007-07-24 21:20 随风流月      
不错,不过现在 Orcas Beta 1 里的编译器有很多 BUG...
看 IL 都快看吐了,特别是编译器生成的 BUG 代码...

 回复 引用 查看   
#17楼[楼主] 2007-07-25 08:37 Anytao      
@随风流月
呵呵, 不会吧, 还没来得及玩玩Orcas,有空试试先。
不过,这些bug,ms应该会及时修复吧,期待呀。

 回复 引用   
#18楼 2007-07-26 16:27 ,逸程网.[未注册用户]
***2-5折,国际特价更优 01051666018 ***一网打尽,逸程网.
 回复 引用   
#19楼 2007-10-22 15:38 guohao0826[未注册用户]
从代码中我们知道没有为HelloWord类提供任何显示的构造函数,因此可以肯定其继承自基类System.Object,

弱弱的问下:

构造函数好像是应该不能继承的吧? 也不知道是不是我理解错了。

 回复 引用 查看   
#20楼[楼主] 2007-10-22 19:15 Anytao      
@guohao0826
实在抱歉,这里表达有些误差,你的说法很对。这里指的并非构造函数继承自System.Obejct的构造函数,而指HelloWord类继承自Object类。
构造函数的确不存在继承一说,子类可以调用父类构造函数,但并非是继承关系。

 回复 引用   
#21楼 2008-04-18 18:00 文祥[未注册用户]
Public Key貌似是160bytes的!
 回复 引用 查看   
#22楼[楼主] 2008-04-19 22:05 Anytao      
@文祥
160字节包括了文件头的32字节数和128位的公钥长度,此不矛盾:-)

 回复 引用 查看   
#23楼 2008-09-24 16:55 ZeroCool      
“没有被管理和被管理代码”读起来很拗口,通常的翻译都是“托管与非托管”。
 回复 引用 查看   
#24楼[楼主] 2008-09-26 10:28 Anytao      
@ZeroCool
呵呵,正解,确实有点儿绕圈,实际就是你指正的“托管和非托管”。

 回复 引用 查看   
#25楼 2009-05-07 22:21 .NETLiu      
请教一个问题:
“关于entrypoint指令应该和MANIFEST没有什么太多联系,编译器能够根据MANIFEST清单的编译程序集和模块确定目标编译对象”

这里说的编译器是JIT编译器吗?