MSIL 心得

Microsoft intermediate language (MSIL)是一种编程语言,可以把它看成是组成.NET Framework的一部分,不论从内容还是形式上它都像是一种汇编语言,但是与传统的汇编语言又不太一样,初学MSIL的时候觉得它很亲切,我可以用使用高级语言编程的习惯来使用MSIL编程,例如它是面向对象的,可以用newobj指令生成一个类型实例,所以我在代码中可以这样来新建一个类型的对象:

newobj     instance void AOP_Programing.UsingAOP::.ctor()

可以用callvirt指令来调用其虚方法:

callvirt   instance void AOP_Programing.UsingAOP::Display()

    

1MSIL初探

我们知道,对于托管应用,不论是Windows 桌面应用还是Web应用都会经过两次编译,第一次编译是由特定语言的编译器将源代码编译为MSIL,例如C#编译器可以将用C#写的源代码编译为MSIL,而在生成MSIL同时会生成相应的元数据,例如如下简单例子:

    1)源代码:
    

using  System;

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,其实这个值是告诉我们当前应用处在正常情况下,如果发现运行的时候会出现超出这个容量的值,那么说明我们的代码可能存在逻辑问题,所以往大了说某种程度上这个值能告诉我们代码是否有潜在的逻辑问题,举个例子说明;

定义三个变量abca的值为1b的值为2c的值为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指令集列出来,方便对照
 

ContractedBlock.gif ExpandedBlockStart.gif 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中一个求幂的代码为例:
    

ContractedBlock.gif ExpandedBlockStart.gif K&R
  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 ([0int32 i,
 25                        [1bool 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 ([0int32 i,
 68                      [1int32 p,
 69                      [2int32 a0,
 70                      [3bool 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 ([0class 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 指令

用来指定代码中会用到的其他程序集,这些程序集的公共类型和方法可以在我们的代码中使用,例如上述代码中的12

.assembly extern  mscorlib{},实际上即使不在这里加mscorlib这个程序集,编译器也会自动加上,因为这个程序集包含了所有的内建类型的定义, Sytem.Object也定义在这个类集当中,所以C#编译器在编译过程中会自动加上对mscorlib的引用。

2) .assembly指令

定义了当前程序集的名字,另外也可以包含诸如版本号、public key token、程序集语言等信息,如代码35所示。

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指令

     用来定义一个方法,包括静态方法、构造函数、非静态方法等,下面对代码中求幂的方法进行说明。

   第6364行,定义方法Power,有两个参数:basenn,这两个参数被存储在方法参数存储区,分别为参数1和参数2,最后有个cil managed说明当前方法为托管代码;

   第66行,定义了Evaluation Stack的最大容量暂时设为8,可以等我们写完下面代码再写这个值;

   第6770行,定义了当前方法所用到的局部变量,这些局部变量位于该方法的局部变量存储区,在这里我们用到了4个局部变量,除了ip外,a用来;

   第7273行,为局部变量p赋初值1ldc.i4.1是将常数1压栈到Evaluation Stackstloc.1是将栈顶的数值弹出后存入局部变量p,上述i4代表int32类型,下表列出了常用类型及其对应的简写形式。


   第7576行,为局部变量i赋初值1,指令执行完后Evaluation Stack为空。

   第7880行,比较局部变量i和方法的参数2的值。第80行执行前,Evaluation Stack中有两个数据:分别为局部变量i和由ldarg.2指令从方法参数表中取出的参数2的值,也就是n的值。第80行执行结束后将比较结果压栈,此时Evaluation Stack中只有一个值,就是这个比较结果。

   第8185行,是对结束条件的判断,如果栈中值为1表明i>n,则由跳转指令brtrue.s跳转到98行,跳转前将Evaluation Stack栈顶值弹出,此时Evaluation Stack为空。

   第8790行,实现p=p×basen,更新局部变量p的值

   第9296行,对控制循环的局部变量i进行加1操作后更新局部变量i,最后,执行br.s指令,无条件的跳转到第78

   第9899行, 由于这个方法有返回值,所以方法返回时需要将局部变量 p 的值压栈,最后方法返回,清空相关存储区。

   从上述描述可以看到Evaluation Stack 中最多会有,由于 
   上述代码中第2160行是对Power方法的一个测试,其他没什么好说的,但是这段代码包含了一些不太好的编程习惯:你会发现有3个地方出现了box指令,只要有该指令就说明此处发生了装箱操作,也就是会发生以下事情:

1) 从托管堆中分配内存,大小为这个值类型字段所需的内存,另外还有type object pointersync 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

猜你喜欢

转载自blog.csdn.net/weixin_33964094/article/details/93642169