Microsoft intermediate language (MSIL)是一种编程语言,可以把它看成是组成.NET Framework的一部分,不论从内容还是形式上它都像是一种汇编语言,但是与传统的汇编语言又不太一样,初学MSIL的时候觉得它很亲切,我可以用使用高级语言编程的习惯来使用MSIL编程,例如它是面向对象的,可以用newobj指令生成一个类型实例,所以我在代码中可以这样来新建一个类型的对象:
newobj instance void AOP_Programing.UsingAOP::.ctor()
可以用callvirt指令来调用其虚方法:
callvirt instance void AOP_Programing.UsingAOP::Display()
1、MSIL初探
我们知道,对于托管应用,不论是Windows 桌面应用还是Web应用都会经过两次编译,第一次编译是由特定语言的编译器将源代码编译为MSIL,例如C#编译器可以将用C#写的源代码编译为MSIL,而在生成MSIL的同时会生成相应的元数据,例如如下简单例子:
1)源代码:
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Console.ReadKey();
}
}
}
2)用C#编译器编译后得到一个名字为hello.exe的可执行文件:
3)编译后生成的MSIL代码,用IL DASM打开:
如:Main方法的MSIL如下:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 19 (0x13)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Hello World!"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0011: pop
IL_0012: ret
} // end of method Program::Main
4)元数据:
由于较长所以不再列出,可以用Ctrl+M查看元数据。
简单的hello world代码包含更深层次的内容。
第二次编译发生在运行时,这个时候CLR会利用JIT编译器依据当前硬件平台将MSIL翻译为本地CPU指令,以方法为例,JIT编译器只会在方法第一次被调用时执行编译操作,这里会消耗一定CPU时间,完成编译后修改方法地址,下次再调用该方法时会直接通过该地址访问已经编译好的CPU指令。
从以上描述我们知道MSIL至少有如下优点:可移植性高,通用性强,只要有编译器支持,可以将任何语言翻译为MSIL,进而使其运行在 .Net平台上,极大提高代码复用性,这就是所谓的 compile-once-and-run anywhere,在将IL编译成本地CPU指令时,CLR会对其进行验证,因此MSIL又有高可靠性和安全性的优点。
2、理解几类存储区
执行某个方法的时候会有以下几个存储区被用到:
1) 局部变量区
方法所用到的每一个局部变量都需要在局部变量区初始化,格式为: .locals [ init ]‘(’ Local sSignature ‘)’,例如:.locals init (string V_0,uint8[] V_1),表明当前方法有两个局部变量,一个是String类型,一个是byte类型的数组,该区不能被直接访问。
2) 静态字段存储区
用来存储当前类型的全局变量,在C#中指声明为Static的字段,例如:.field public static int32 Length。
3) 方法参数区
用来存储被执行方法的传入参数,该区不能被直接访问。
4) 托管堆
引用类型的变量会被分配到这个区域,这个区域对象的生存期会受到垃圾回收器GC的全程监控,当GC被启动时,它将会对托管堆里不被任何其它对象所引用的对象进行内存回收,当然如果该对象定义了析构函数即使已经不被引用也可能不被回收。
5) 非托管堆
主要指由C++/CLI编写的非托管代码动态分配内存时可以将对象分配到这个区域。
6) 动态内存池
随着方法调用的结束而被回收,方法可以在这个区域动态分配内存。
7) Evaluation Stack
是一个非常重要的数据结构,它在内存分配和我们的应用之间起桥梁作用,所有的计算、结果数据的移入移出都要通过它,它是一个LIFO的栈,例如我们可以用各种load指令来从其它存储区取得数据放入Evaluation Stack,可以看成是push(压栈),也可以使用各种store指令来将当前计算结果存储到相应的存储区,可以看成是pop(出栈)。
如果方法没有返回值则要保证方法调用结束时,Evaluation Stack为空,如果有返回值则方法调用结束的时候Evaluation Stack只存该返回值,如果违反上述规则,则运行时会抛出InvalidProgramException的异常。
在上述代码我们看到有个.maxstack指令,这个指令是用来指定同时在栈中存在的值,也就是栈容量,如果我们没有指定它的大小,则编译器会自动设为默认8,其实这个值是告诉我们当前应用处在正常情况下,如果发现运行的时候会出现超出这个容量的值,那么说明我们的代码可能存在逻辑问题,所以往大了说某种程度上这个值能告诉我们代码是否有潜在的逻辑问题,举个例子说明;
定义三个变量a、b、c,a的值为1,b的值为2,c的值为a+b,最后将c的值由控制台打印出来,代码如下:
// test.il
.assembly extern mscorlib {
}
.assembly test{
.ver 1:0:1:0
}
.module test.exe
.method privatescope static void Mains() cil managed {
.entrypoint
.maxstack 2 //..代码1
.locals init (int32 V_0,
int32 V_1,
int32 V_2)
ldc.i4.1
stloc.0
ldc.i4.2
stloc.1
ldloc.0
ldloc.1
add //..代码2
stloc.2
ldloc.2
call void [mscorlib]System.Console::WriteLine(int32)
call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib] System.Console::ReadKey()
pop //..代码3
ret
}
由代码2可知我们用了add指令,所以.maxstack的值至少要为2,在我的32位机器上编译上述代码,如下:
从编译结果上看可以知道我们生成了一个叫test.exe.的程序集,它包含了一个全局方法,其实就是我们的Mains方法,且是程序集的入口方法,与C#的入口方法必须叫Main不同,我们的入口方法叫Mains,说明用MSIL写代码时可以为入口方法起任何名字。双击这个exe文件则输出结果3,如果我们将代码1处相应指令改为.maxstack 1,重新编译后双击这个exe文件则有如下结果:
Evaluation Stack 和其它存储区的关系如下:
3、实践
下面把MSIL指令集列出来,方便对照
MSIL Instruction Set
Base Instructions
Instruction Description Stack Transition
1 add add two values, returning a new value …, value1, value2…, result
2 add.ovf.<signed> add integer value with overflow check …, value1, value2…, result
3 and bitwise AND …, value1, value2 …, result
4 arglist get argument list … …, argListHandle
5 beq.<length> branch on equal …, value1, value2 …
6 bge.<length> branch on greater than or equal to …, value1, value2 …
7 bge.un.<length> branch on greater/equal, unsigned or unordered …, value1, value2 …
8 bgt.<length> branch on greater than …, value1, value2 …
9 bgt.un<length> branch on greater than, unsigned or unordered …, value1, value2 …
10 ble.<length> branch on less than or equal to …, value1, value2 …
11 ble..un<length> branch on less/equal, unsigned or unordered …, value1, value2 …
12 blt.<length> branch on less than …, value1, value2 …
13 blt.un.<length> branch on less than, unsigned or unordered …, value1, value2 …
14 bne.un<length> branch on not equal or unorded …, value1, value2 …
15 br.<length> unconditional branch …, …
16 break breakpoint instruction …, …
17 brfalse.<length> branch on false, null, or zero …, value …
18 brtrue.<length> branch on non-false or non-null …, value …
19 call call a method …, arg1, arg2 … argn …, retVal (not always returned)
20 calli indirect method call …, arg1, arg2 … argn, ftn …, retVal (not always returned)
21 ceq compare equal …, value1, value2…, result
22 cgt compare greater than …, value1, value2…, result
23 cgt.un compare greater than, unsigned or unordered …, value1, value2…, result
24 ckfinite check for a finite real number …, value …, value
25 clt compare less than …, value1, value2…, result
26 clt.un compare less than, unsigned or unordered …, value1, value2…, result
27 conv.<to type> data conversion …, value …, result
28 conv.ovf<to type> data conversion with overflow detection …, value …, result
29 conv.ovf.<to type>.un unsigned data conversion with overflow detection …, value …, result
30 cpblk copy data from memory to memory …, destaddr, srcaddr, size …
31 div divide values …, value1, value2…, result
32 div.un divide integer values, unsigned …, value1, value2…, result
33 dup duplicate the top value of the stack …, value …, value, value
34 endfilter end filter clause of SEH …, value …
35 endfinally end the finally or fault clause of exception block … …
36 initblk initialize a block of memory to a value …, addr, value, size …
37 jmp jump to method … …
38 ldarg.<length> load argument onto the stack … …, value
39 ldarga.<length> load an argument address …, …, address of argument number argNum
40 ldc.<type> load numeric constant … …, num
41 ldftn load method pointer … …, ftn
42 ldind.<type> load value indirect onto the stack …, addr …, value
43 ldloc load local variable onto the stack … …, value
44 ldloca.<length> load local variable address … …, address
45 ldnull load a null pointer … …, null value
46 leave.<length> exit a protected region of code …,
47 localloc allocate space in the local dynamic memory pool size address
48 mul multiply values …, value1, value2 …, result
49 mul.ovf<type> multiply integer values with overflow check …, value1, value2 …, result
50 neg negate …, value …, result
51 nop no operation …, …,
52 not bitwise complement …, value …, result
53 or bitwise OR …, value1, value2 …, result
54 pop remove the top element of the stack …, value …
55 rem compute the remainder …, value1, value2 …, result
56 rem.un compute integer remainder, unsigned …, value1, value2 …, result
57 ret return from method retVal on callee evaluation stack (not always present)
…, retVal on caller evaluation stack (not always present)
58 shl shift integer left …, value, shiftAmount …, result
59 shr shift integer right …, value, shiftAmount …, result
60 shr.un shift integer right, unsigned …, value, shiftAmount …, result
61 starg.<length> store a value in an argument slot …, value …,
62 stind.<type> store value indirect from stack …, addr, val …
63 stloc pop value from stack to local variable …, value …
64 sub substract numeric values …, value1, value2 …, result
65 sub.ovf.<type> substract integer values, checking for overflow …, value1, value2 …, result
66 switch table switch on value …, value …,
67 xor bitwise XOR , value1, value2 , result
Object Model Instructions
Instruction Description Stack Transition
1 box convert value type to object reference …, valueType …, obj
2 callvirt call a method associated, a runtime, with an object …, obj, arg1, … argN …, returnVal (not always returned)
3 cast class cast an object to a class …, obj …, obj2
4 cpobj copy a value type …, destValObj, srcValObj …,
5 initobj Initialize a value type …,addrOfValObj …,
6 isinst test if an object is is an instance of a class or interface …, obj …, result
7 ldelem.<type> load an element fo an array …, array, index …, value
8 ldelema load address of an element of an array …, array, index …, address
9 ldfld load field of an object …, obj …, value
10 ldflda load field address …, obj …, address
11 ldlen load the length of an array …, array …, length
12 ldobj copy value type to the stack …, addrOfValObj …, valObj
13 ldsfld load static field of a class …, …, value
14 ldsflda load static field address …, …, address
15 ldstr load a literal string …, …, string
16 ldtoken load the runtime representation of metadata token … …, RuntimeHandle
17 ldvirtfn load a virtual method pointer … object …, ftn
18 mkrefany push a typed reference on the stack …, ptr …, typedRef
19 newarr Create a zero-base, on-dimensional array …, numElems …, array
20 newobj create a new object …, arg1, … argN …, obj
21 refanytype load the type out of a typed reference …, TypedRef …, type
22 refanyval load the address out of a typed reference …, TypedRef …, address
23 rethrow rethrow the current exception …, …,
24 sizeof load the size in bytes of a value type …, …, size (4 bytes, unsigned)
25 stelem.<type> store an element of an array …, array, index, value …,
26 stfld store into a field of an object …, obj, value …,
27 stobj store a value type from the stack into memory …, addr, valObj …,
28 stsfld store a static field of class …, val …,
29 throw throw an exception …, object …,
30 unbox convert boxed value type to its raw form
如何使用上述指令集表?以add add two values, returning a new value …, value1, value2…, result 这条指令为例,add是指令的名字,接着是指令的用途说明,表明该指令的作用是求栈中两个值的和,最后是执行add指令前后栈中数据的变化情况,...表示我们不关心的栈中原有值,add指令会将栈顶的value1和value2的值弹出并进行计算,最后将计算结果result压栈。从这些指令我们也可以看出,所有的计算操作都是发生在Evaluation Stack中的。
下面以K&R写的The C programming Language中一个求幂的代码为例:
1 .assembly extern mscorlib {
2 }
3 .assembly UsingMSIL{
4 .ver 1:0:1:0
5 }
6 .module UsingMSIL.exe
7
8 .class public auto ansi beforefieldinit UsingMSIL.Math
9 extends [mscorlib]System.Object
10 {
11
12 .method public hidebysig specialname rtspecialname
13 instance void .ctor() cil managed
14 {
15 .maxstack 8
16 ldarg.0
17 call instance void [mscorlib]System.Object::.ctor()
18 ret
19 }
20
21 .method public hidebysig instance void Display() cil managed
22 {
23 .maxstack 6
24 .locals init ([0] int32 i,
25 [1] bool a1)
26
27 ldc.i4.0
28 stloc.0
29 br.s ex
30 lp: ldstr "{0} 2^{0}={1} -3^{0}={2}\n"
31 ldloc.0
32 box [mscorlib]System.Int32
33 ldarg.0
34 ldc.i4.2
35 ldloc.0
36 call instance int32 UsingMSIL.Math::Power(int32,
37 int32)
38 box [mscorlib]System.Int32
39 ldarg.0
40 ldc.i4.s -3
41 ldloc.0
42 call instance int32 UsingMSIL.Math::Power(int32,
43 int32)
44 box [mscorlib]System.Int32
45 call void [mscorlib]System.Console::WriteLine(string,
46 object,
47 object,
48 object)
49 ldloc.0
50 ldc.i4.1
51 add
52 stloc.0
53 ex: ldloc.0
54 ldc.i4.s 10
55 clt
56 stloc.1
57 ldloc.1
58 brtrue.s lp
59 ret
60 }
61
62
63 .method public hidebysig instance int32 Power(int32 basen,
64 int32 n) cil managed
65 {
66 .maxstack 2
67 .locals init ([0] int32 i,
68 [1] int32 p,
69 [2] int32 a0,
70 [3] bool a1)
71
72 ldc.i4.1
73 stloc.1
74
75 ldc.i4.1
76 stloc.0
77
78 lp: ldloc.0
79 ldarg.2
80 cgt
81 ldc.i4.1
82 ceq
83 stloc.3
84 ldloc.3
85 brtrue.s ex
86
87 ldloc.1
88 ldarg.1
89 mul
90 stloc.1
91
92 ldloc.0
93 ldc.i4.1
94 add
95 stloc.0
96 br.s lp
97
98 ex: ldloc.1
99 ret
100 }
101
102 }
103
104 .method private hidebysig static void Main(string[] args) cil managed
105 {
106 .entrypoint
107 .maxstack 1
108 .locals init ([0] class UsingMSIL.Math m)
109
110 newobj instance void UsingMSIL.Math::.ctor()
111 stloc.0
112 ldloc.0
113 callvirt instance void UsingMSIL.Math::Display()
114 nop
115 call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
116 pop
117 ret
118 }
119
从上往下看这段代码:
1) .assembly extern 指令
用来指定代码中会用到的其他程序集,这些程序集的公共类型和方法可以在我们的代码中使用,例如上述代码中的1、2行:
.assembly extern mscorlib{},实际上即使不在这里加mscorlib这个程序集,编译器也会自动加上,因为这个程序集包含了所有的内建类型的定义, Sytem.Object也定义在这个类集当中,所以C#编译器在编译过程中会自动加上对mscorlib的引用。
2) .assembly指令
定义了当前程序集的名字,另外也可以包含诸如版本号、public key token、程序集语言等信息,如代码3~5行所示。
3).module指令
一个程序集assembly最少包含一个module如代码第6行所示。
4) .Class指令
用来声明一个类型,这个声明要包含类型的访问修饰符,如public等,编码方式、beforefieldinit标志(此标志使得运行库能够在任何时候执行类型构造函数方法,只要该方法在第一次访问该类型的静态字段之前执行即可。换句话说,beforefieldinit 为运行库提供了一个执行主动优化的许可。具体内容可以参见:http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/us0501StaticsinNET.mspx?mfr=true)、类型名以及当前类的父类等,如代码第8行所示。
5) .method指令
用来定义一个方法,包括静态方法、构造函数、非静态方法等,下面对代码中求幂的方法进行说明。
第63~64行,定义方法Power,有两个参数:basen和n,这两个参数被存储在方法参数存储区,分别为参数1和参数2,最后有个cil managed说明当前方法为托管代码;
第66行,定义了Evaluation Stack的最大容量暂时设为8,可以等我们写完下面代码再写这个值;
第67~70行,定义了当前方法所用到的局部变量,这些局部变量位于该方法的局部变量存储区,在这里我们用到了4个局部变量,除了i和p外,a用来;
第72~73行,为局部变量p赋初值1,ldc.i4.1是将常数1压栈到Evaluation Stack,stloc.1是将栈顶的数值弹出后存入局部变量p,上述i4代表int32类型,下表列出了常用类型及其对应的简写形式。
第75~76行,为局部变量i赋初值1,指令执行完后Evaluation Stack为空。
第78~80行,比较局部变量i和方法的参数2的值。第80行执行前,Evaluation Stack中有两个数据:分别为局部变量i和由ldarg.2指令从方法参数表中取出的参数2的值,也就是n的值。第80行执行结束后将比较结果压栈,此时Evaluation Stack中只有一个值,就是这个比较结果。
第81~85行,是对结束条件的判断,如果栈中值为1表明i>n,则由跳转指令brtrue.s跳转到98行,跳转前将Evaluation Stack栈顶值弹出,此时Evaluation Stack为空。
第87~90行,实现p=p×basen,更新局部变量p的值。
第92~96行,对控制循环的局部变量i进行加1操作后更新局部变量i,最后,执行br.s指令,无条件的跳转到第78行。
第98~99行,由于这个方法有返回值,所以方法返回时需要将局部变量p的值压栈,最后方法返回,清空相关存储区。 从上述描述可以看到Evaluation Stack 中最多会有,由于
上述代码中第21~60行是对Power方法的一个测试,其他没什么好说的,但是这段代码包含了一些不太好的编程习惯:你会发现有3个地方出现了box指令,只要有该指令就说明此处发生了装箱操作,也就是会发生以下事情:
1) 从托管堆中分配内存,大小为这个值类型字段所需的内存,另外还有type object pointer和sync block index所需的内存;
2) 将这个值类型的字段值复制到托管堆中的新对象中;
3) 返回新对象地址。
由此可见,装箱操作会影响到程序运行效率。
最后,运行结果如下:关于MSIL的更多信息,可以通过查看微软向 European Computer Manufacturers Association (ECMA) 提供的文档(地址如下:http://www.ecma-international.org/publications/standards/Ecma-335.htm)以及MSDN来了解。
最后,我认为时常将自己写的代码反汇编一下对提高代码质量有较大帮助,虽然微软为我们封装的很好,但是我觉得还是需要透过MSIL来了解一些幕后,我想这对我们的提高会有帮助的。
继续学习中........ :)
转载于:https://www.cnblogs.com/vivounicorn/archive/2009/08/17/vivounicorn.html