Leo Zhang

A simple man with my own ideal

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指令集列出来,方便对照
 

MSIL指令集

   
      如何使用上述指令集表?以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中一个求幂的代码为例:
    

K&R

 

从上往下看这段代码:

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来了解一些幕后,我想这对我们的提高会有帮助的。

    继续学习中........ :)

posted on 2009-08-17 17:28  Leo Zhang  阅读(2446)  评论(6编辑  收藏  举报

导航