Head First IL中间语言之 实例深入经典诠释

在上一篇的博文中,我以一个具有代表性的IL base instruction, ldc 指令,来介绍了IL语言中最基本的一类指令及其参数的使用方法.大家反映还不错.
这里,就再写一篇博文,用一个应用程序的实例来深入的说明一番CLR环境下的IL语言的语法,基本运行机制和原理.

首先,找一段IL语言的例子,从codeproject找了一段IL程序修改了下
.method static void main() cil managed
{
    .maxstack 2
    .entrypoint
    .locals init (int32, temp)
    
    newobj instance void Donis.CSharpBook.ZClass::.ctor()
    dup
    call instance int32 Donis.CSharpBook.ZClass::AddFields()
    stloc.0
    ldstr "the total is {0}"
    ldloc.0
    box int32
    call void [mscorlib] System.Console::WriteLine(string,object)
    call instance int32 Donis.CSharpBook.ZClass::SubtractFields()
    stloc.0
    ldstr "The difference is {0}"
    ldloc.0
    box int32
   call void [mscorlib] System.Console::WriteLine(string,object)
    
  }

这段程序,深究的程度不同,值得琢磨的地方也就越多;下面,我来一点一点的分析:

我们跳过定义和开头,从这一句开始:
newobj instance void Donis.CSharpBook.ZClass::.ctor()
这里,使用了一个newobj指令.这个指令用来分配生成一个未初始化的对象或者是值类型,并且call ctor()构造函数.
这里,有的朋友会郁闷,为什么这个指令可以分配一个value type值类型.这的确是这个指令本分的功能而且是比较特别的地方.
ECMA标准文档对这个指令的描述如下:(请允许我引用一段E文那,呵呵,E文不好的词霸调出来吧):
--------------------------Reference--------------------------------
The newobj instruction creates a new object or a new instance of a value type. ctor is a metadata token that indicates the name, class, and signature of the constructor to call. If a constructor exactly matching the indicated name, class and signature cannot be found, MissingMethodException is thrown.

The newobj instruction allocates a new instance of the class associated with ctor and initializes all the fields in the new instance to 0 (of the proper type) or null as appropriate. It then calls the constructor with the given arguments along with the newly created instance. After the constructor has been called, the now initialized object reference is pushed on the stack.

From the constructor’s point of view, the uninitialized object is argument 0 and the other arguments passed to newobj follow in order.Value types are not usually created using newobj. They are usually allocated either as arguments or local variables, using newarr (for zero-based, one-dimensional arrays), or as fields of objects. Once allocated, they are initialized using initobj. However, the newobj instruction can be used to create a new instance of a value type on the stack, that can then be passed as an argument, stored in a local, etc.
--------------------------Reference--------------------------------
这里,大家最好务必读一下,有助于下面的理解.
同时,特别提醒大家注意这一句:"After the constructor has been called, the now initialized object reference is pushed on the stack."
这一句,有助于下面大家dup指令的理解,IL编译器的指令优化和多方法调用的理解.

OK,接下来就是dup指令.ECMA的CLR文档的part 3对这个指令的解释很简单,只有一句:Duplicate the value on the top of the stack.实际上用法也的确很简单.
复杂的一部分,是中间语言Complier的一些指令优化拐了一点点小弯.

接下来,我们来说明这一句:
call instance int32 Donis.CSharpBook.ZClass::AddFields()
不好意思哈,又要引用一段E文:
--------------------------Reference--------------------------------
The call instruction calls the method indicated by the descriptor method. method is a metadata token (a methodref, methoddef, or methodspec) that indicates the method to call, and the number, type, and order of the arguments that have been placed on the stack to be passed to that method, as well as the calling convention to be used. The call instruction can be immediately preceded by a tail. prefix to specify that the current method state should be released before transferring control .
--------------------------Reference--------------------------------
上面说的有必要再补充下,Call方法再执行的时候,计算堆栈,注意,这里说的是计算堆栈stack,不是托管堆heap,
从栈顶开始,首先要放入instance的referece,也就是ZClass在托管堆里面的引用,然后依次放入需要传入到AddFields方法里面的参数.我们的例子中,不传递参数.

then:
    stloc.0
    ldstr "the total is {0}"
    ldloc.0
第一句指令,把刚才call那句执行完毕以后的一个返回的类型为int32的变量pop到local变量集合的第一个中去.然后load一个字符串,最后,把local变量里面的第一个,也就是temp变量的值stack的最上面.
这里需要特别提醒一下的是:ldstr这句中,字符串是保存在托管堆中的.load到stack中的,只是heap的一个引用.也就是说,这里只是一个引用类型.理解这点,有助于下面指令的理解.

接下来,该是本文中精彩的部分到了:
这里两条指令一起介绍:
    box int32
    call void [mscorlib] System.Console::WriteLine(string,object)
box指令用来执行装箱操作,他防止了指令异常的发生.它从计算堆栈中复制一个值类型实例的所有的字段,然后在托管堆上面创建一个对值类型进行装箱的对象,并且将新建对象的引用放回到计算堆栈当中去.此时,原来的值类型就被一个引用类型代替了.unbox指令执行相反的拆箱操作.
为什么要用box指令呢?有人可能会问道这个.
这里,是因为下面的WriteLine方法调用了两个引用类型的参数,所以,需要把int32类型的一个值类型转换成为一个Object的引用类型.
注意:这里返回类型是void,读者们可以思考一下,此时的stack的顶部,是什么呢??

最后是这条指令特别介绍一下:
call instance int32 Donis.CSharpBook.ZClass::SubtractFields()
这个指令前面已经详细介绍过一次,这里再次讨论一下.
这里可能有人会问道,call指令需要在stack的顶部放置ZClass的reference啊,这里为什么没有用dup或者别的指令啊.
啊哈,这里是因为在上面的指令中,newobj将ZClass的reference放置到了stack中,dup又把这个值复制了一次从新push到了stack中,这里stack中就有两个引用啦.
这也涉及到IL编译器的编译优化问题的相关的研究,这里就不深入讨论这个话题了.

值得一提的是,我在写这篇文章之前,看到相当多类似话题的讨论中,很多人对dup指令的用法感到很困惑,怎么有的时候要用,有的时候有不用呢?怎么前面用了后面又不用呢?然后各种猜想假设就接踵而来.........
这里,和newobj指令和call指令的使用结合起来解释这个问题就容易了.

恩,不罗嗦了,over吧.敲累了.后续更多精彩内容奉上 ^_^

posted on 2007-10-19 23:28  lbq1221119  阅读(3319)  评论(18编辑  收藏  举报

导航