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