作为.NET最低层次的公共基础,微软中介语言(MSIL或IL)对一般开发者具有非常重要的意义。除了好奇心以外,仔细研究应用程序的IL能让你更为清楚地了解到公共语言运行时(CLR)执行高级C#或VB.NET代码的基本原理,从而有助于你发现和解决一些比较细微的问题。
在这篇文章里,我将引领读者了解IL,学习有关的一些关键指令,同时对CLR的操作机理做一点基础性解释。我不打算教你用IL编程,而是分析一些IL语法和语句使你对IL有更多了解。
微软的IL拆卸实用程序Ildasm.exe(通常位于Program FilesMicrosoft.NetFrameworkSDKBin目录下)可以析构.NET assembly(装配)、根据你的要求从程序中抽取IL代码。对某一assembly调用该使用程序后,ILDASM会给出该assembly中所有类和名称空间的一个视图,如图A所示:
当你进到某个类的成员或其方法,ILDASM就会为你显示该成员的IL代码。如果之前你曾经看到过汇编器或J++字节码,那么IL可能在你看来会觉得有点眼熟。在另一方面,如果你仅对抽象的高级程序语言有所了解,那么IL看起来更像是胡言乱语。
好,现在你知道如何窥视assembly的IL代码了,但这些代码都意味着什么呢?在回答这个问题之前,首先让我们先来了解下CLR的有关知识。
对.NET程序来说,.NET CLR在功能上就如同一块虚拟的CPU,它执行IL代码、操作数据。CLR和真实的CPU类似之处在于它们都不直接操作内存中的变量而是使用程序变量的临时拷贝,CLR把这些程序变量存放在堆栈上。从内存拷贝某个变量到堆栈的行为称做装载(loading),而从堆栈拷回某个变量到内存的行为则被称做存储(storing)。
所以把两个数字相加的过程应该是这样的:
1.装载第1个数字并把它推入堆栈。
2.装载第2个数字并把它推入堆栈。
3.从堆栈中取出这两个数字并把它们相加。
4.把结果存储到内存。
理解IL的关键是知道堆栈的工作原理。堆栈是一种抽象数据结构,其操作机理是后进先出。当你把新条目推进堆栈时,已经在堆栈内的任何条目都会压到堆栈的深处。同样的,把一个条目从堆栈移出则会让堆栈内的其他条目都向堆栈的顶部移动。只有堆栈最顶端的条目能从堆栈中取出,条目离开堆栈的顺序和它们被推进堆栈的顺序一样。你不妨回想下自动售货机的装货和取货过程就明白了。
既然你已经明白了CLR操作的基础知识,下面我们就接着讨论你面前的那些代码。怎么?没有看到什么代码?那么请你看看这里列出的IL代码。
你首先看见的是对当前方法的IL声明,其中包括方法的名字,返回类型、参数列表以及附着于该方法的其他修饰关键词(static/shared、public、virtual等等)。对象构造器则被赋给一个特殊的名字:.ctor。
在IL中,方法参数按照它们在参数列表中的位置依次被引用。如果方法是静态或共享方法,那么参数0则是参数列表中的第1个参数。而对实例方法来说,参数0则是指向该方法所在类的实例的指针(Me或者this)。方法中的所有局部变量都在.locals标记的段落中以同样的方式声明。
在声明所有的局部变量以后,程序的实际正文才开始。每条IL指令,或opcode都可以根据你的喜好以一个IL_ 标记作为代码行开头。我们接下来再了解些更重要的IL指令。
以LD开头的指令把变量从内存装载到堆栈供其操作。装载指令有若干条,每一条装载指令都操作特定类型的变量。以下就是其中的一些装载指令:
每一条装载指令都有对应的一条存储指令,后者以ST开头,负责把一个条目存入内存。例如,STLOC就负责把堆栈最顶端的条目存入一个局部变量。存储指令指定变量的句法规则通常和它们对应的装载指令类似。
如果你不能比较两个值而且根据其比较结果做出决定,那么许多问题都无法用任何程序语言来解决。IL有一套比较操作符,它们都以C字母开头,比较堆栈中的值。通常,如果比较结果为真则会把1推入堆栈否则就推入0。
大多数这类指令都很容易由它们的名字区分出来。例如,CEQ比较两个值是否相等,而CGT则确定堆栈最顶端的值是否比第二个最顶端值更大。 CLT类同于CGT,不过执行的是小于比较操作。
通常,在对两个值进行比较之后会根据比较的结果结果实施一些操作。IL分支指令(以BR开头)根据堆栈最顶端的条目中的内容跳到其他指令。BRTRUE 和BRFALSE弹出堆栈最顶端的条目,然后根据该项为真(1)还是为假(0)而分别跳到指定的代码行。如果没有执行指令跳跃则继续执行下一条指令。另外还有一个无条件分支操作符BR,它总是跳到指定的代码行。
你会发现分支操作就好像源代码中的if语句以及显式执行的Goto操作。IL中的分支命令同样具有高级流程控制结构的对等体,比如:if, case, while, for等等。
CALL和CALLVIRT指令调用其他方法和函数。CALL通常表示被调用的方法是静态的或共享的,而CALLVIRT则用于实例方法。就两种指令来说,方法的名字都会在指令中包括。被送到方法的任何参数都会被弹出堆栈而且要在方法被调用之前装载。
因为创建一个新对象需要调用构造器,所以IL的对象创建也类似于其他的方法调用。参数首先被装载到堆栈,然后执行NEWOBJ指令,它调用对象的构造器同时把对象的索引放回堆栈。指令中得有对象的名字。
以上就是大致的IL语法操作。除了满足你内心的求知欲望以外,我希望你能从我的阐述中得到足够的信息来理解IL代码的真实含义。