现在的位置: 首页 > 综合 > 正文

说说emit(中)ILGenerator

2013年04月26日 ⁄ 综合 ⁄ 共 6229字 ⁄ 字号 评论关闭

说说emit()ILGenerator

/玄魂

在上一篇博客(说说emit()基本操作)中,我描述了基本的技术实现上的需求,难度和目标范围都很小,搭建了基本的架子。在代码中实现了程序集、模块、类型和方法的创建,唯一的缺憾是方法体。

方法体是方法内部的逻辑,我们需要将这个逻辑用IL代码描述出来,然后注入到方法体内部。这里自然地引出两个主题,IL代码和用来将Il代码注入到方法体内的工具(ILGenerator)。本篇博客将主要围绕这两个主题展开。但是这一篇博客不可能将IL讲的很详细,只能围绕ILGenerator的应用来讲解。若想了解IL的全貌,我想还是要看ECMA的文档了(http://www.ecma-international.org/publications/standards/Ecma-335.htm)。

2.1 CIL指令简介

这里我们通过几个简单例子来对IL指令有个初步的认识。

新建一个名为“HelloWorld”的控制台项目,代码如清单2-1(虽然在我之前的文章里用过HelloWorld来解释Il,虽然无数篇博客都用过这个例子,但是我还是不厌其烦的用它)。

代码清单2-1  HelloWorld

using System;

 

namespace HelloWorld

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("Hello World");

        }

    }

}

编译上面的代码,然后使用ILDasm打开HelloWorld.exe,导出.il文件,内容如下:

//  Microsoft (R) .NET Framework IL Disassembler.  Version 4.0.30319.1

//  Copyright (c) Microsoft Corporation.  All rights reserved.

// Metadata version: v4.0.30319

.assembly extern mscorlib

{

  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..

  .ver 4:0:0:0

}

.assembly HelloWorld

{

 //()

}

.module HelloWorld.exe

// MVID: {CBB65270-D266-4B29-BAC1-4F255546CDA6}

.imagebase 0x00400000

.file alignment 0x00000200

.stackreserve 0x00100000

.subsystem 0x0003       // WINDOWS_CUI

.corflags 0x00020003    //  ILONLY 32BITREQUIRED

// Image base: 0x049F0000

 

 

// =============== CLASS MEMBERS DECLARATION ===================

 

.class private auto ansi beforefieldinit HelloWorld.Program

       extends [mscorlib]System.Object

{

  .method private hidebysig static void  Main(string[] args) cil managed

  {

    .entrypoint

    // Code size       13 (0xd)

    .maxstack  8

    IL_0000:  nop

    IL_0001:  ldstr      "Hello World"

    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)

    IL_000b:  nop

    IL_000c:  ret

  } // end of method Program::Main

 

  .method public hidebysig specialname rtspecialname

          instance void  .ctor() cil managed

  {

    // Code size       7 (0x7)

    .maxstack  8

    IL_0000:  ldarg.0

    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()

    IL_0006:  ret

  } // end of method Program::.ctor

 

} // end of class HelloWorld.Program

在上面的代码中,隐藏的内容为AssemblyInfo.cs中内容,也就是程序集级别的配置内容。首先注意以”.”开头的字段,.assembly.module.class.method等等,我们称之为CIL指令(CIL Directive)。和指令一同使用的,通常直接跟在指令后面的,称之为CIL 特性(CIL Attributes),上面代码中的 externextendsprivatepublic都属于CIL特性,它们的作用是用来描述CIL指令如何被执行。下面先从CIL指令(CIL Directive)的角度看看上面的代码都告诉了我们什么信息。

.assembly extern mscorlib

{

  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                        

  .ver 4:0:0:0

}

当前程序集引用了程序集mscorlib,该程序集的强名称签名公钥标识为“B7 7A 5C 56 19 34 E0 89”,版本为“4:0:0:0

.assembly HelloWorld

{

 //()

}

定义当前程序集,名称为HelloWorld

.module HelloWorld.exe

模块为.module HelloWorld.exe

.imagebase 0x00400000

映像文件基址。

.file alignment 0x00000200

文件对齐大小。

.subsystem 0x0003       // WINDOWS_CUI

指定程序要求的应用程序环境。

.stackreserve 0x00100000

调用堆栈(Call Stack)内存大小。

.corflags 0x00020003    //  ILONLY 32BITREQUIRED

保留字段,未使用。

.class private auto ansi beforefieldinit HelloWorld.Program

       extends [mscorlib]System.Object

声明类HelloWorld.Programprivate是访问类型,auto指明内存布局类型,auto表示内存布局由.NET自动决定(LayoutKind,共有三个值:SequentialAutoExplicit),ansi表示在托管和非托管转换时使用的编码类型。extends表示继承。

.method private hidebysig static void  Main(string[] args) cil managed

.method,声明方法;private,访问类型;hidebysig,相当于c#方法修饰符newstatic,静态方法;void ,返回类型;cil managed,表示托管执行。

.entrypoint

程序入口点。

.maxstack  8

执行方法时的计算堆栈大小。

在方法内部,执行逻辑的编码,被称作操作码(OpcodeOperation Code),如nopldstr。操作码也通常被翻译为指令,但是它的英文是Instruction而不是Directive,本文称之为操作指令。完整的操作码速查手册,可参考http://wenku.baidu.com/view/143ab58a6529647d27285234.html

操作码实际上都是二进制指令,每个指令有其对应的命名,比如操作码0x72对应的名称为ldstr。在操作码前面类似“IL_0000:”这些以冒号结尾的单元是(标签)Label,其值可以任意指定,在执行跳转时会用到Label

在操作码之前,都会先设置计算堆栈大小。计算堆栈(Evaluation Stack)是用来保存局部变量和方法传人参数的空间。在方法执行前后都要保证计算堆栈为空。

从内存中拷贝数据到计算堆栈的操作称之为Load,以ld开头的操作指令执行的都是load操作,例如ldc.i4为加载一个32位整型数到计算堆栈中,Ldargs.3为将索引为3的参数加载到计算堆栈上。

从计算堆栈拷贝数据回内存的操作为Store,以st开头的操作指令执行的操作都是Store,例如stloc.0为从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中,starg.s为将位于计算堆栈顶部的值存储在参数槽中的指定索引处。

在方法体的开始部分,需要指定在方法执行过程中需要的计算堆栈的最大值,也就是.maxstack指令(directive)。在上面的示例程序中,我们指定最大堆栈值为8,事实上它是编译器指定的默认值。计算运算堆栈的大小最简单的方法是计算方法参数和变量的个数,但是个数往往大于实际需要的堆栈大小。编译器往往会对代码做编译优化,使指定的堆栈大小更合理(最大使用大小)。例如下面的代码

   staticvoid Main(string[] args)

        {

            int v1 = 0;

            int v2 = 0;

            int v3 = 0;

            int v4 = 0;

            int v5 = 0;

            int v6 = 0;

            int v7 = 0;

            int v8 = 0;

            int v9 = 0;

            int v10 = 0;

            Console.WriteLine("Hello World");

        }

 

 

编译之后,编译器设置的计算堆栈为大小为1

修改成下面的代码之后,计算堆栈的大小是多少呢?

  classProgram

    {

        staticvoid Main(string[] args)

        {

            int v1 = 0;

            int v2 = 0;

            int v3 = 0;

            int v4 = 0;

            int v5 = 0;

            int v6 = 0;

            int v7 = 0;

            int v8 = 0;

            int v9 = 0;

            int v10 = 0;

            UseParams(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10);

            Console.WriteLine("Hello World");

        }

 

        privatestaticvoid UseParams(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10)

        {

            int sum = v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10;

        }

 

    }

初步统计Main方法的计算堆栈的大小应该是11(变量个数),但是最大使用量是10,所以最终最大计算堆栈的大小应该是10

 

 

 

其实使用计算堆栈的原则很简单,在使用变量之前将其压栈,使用后弹栈

这里再啰嗦一句,个人认为学习Il编码的最简单方法是先了解基本原理,准备一份指令表,用C#编写实例代码,然后使用反编译工具反编译查看Il指令,最后再自己模仿编写。

现在我们回头看最简单的HelloWorld程序的内部IL实现。

   .entrypoint

    // Code size       13 (0xd)

    .maxstack  8

    IL_0000:  nop

    IL_0001:  ldstr      "Hello World"

    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)

    IL_000b:  nop

    IL_000c:  ret

逐句解释下。

IL_0000:  nop

不执行任何push或者pop操作

ldstr      "Hello World"

加载字符串"Hello World"的引用到计算堆栈。

call       void [mscorlib]System.Console::WriteLine(string)

调用程序集为mscorlib中的System.Console类的方法WriteLine。此时会自动弹出计算堆栈中的值赋值为调用方法的参数。

IL_000c:  ret

ret就是return,结束当前方法,返回返回值。

下面我们再来看两个小例子,加深下理解。

  staticvoid Main(string[] args)

        {

            int v1 = 2;

            object v2 = v1;

            Console.WriteLine((int)v2);

        }

这段代码,涉及一个简单的赋值操作和一个装箱拆箱。我们看对应的IL代码:

.method private hidebysig static 
    
void Main (
        
string[] args
    ) 
cil managed 
{
    
// Method
begins at RVA 0x2050

    
// Code
size 23 (0x17)

    
.maxstack 1
    
.entrypoint
    
.locals init (
        [0] 
int32 v1,
        [1] 
object v2
    )

    IL_0000: nop
    IL_0001: 
ldc.i4.2
    IL_0002: 
stloc.0
    IL_0003: 
ldloc.0
    IL_0004: 
box [mscorlib]System.Int32
    IL_0009: 
stloc.1
    IL_000a: 
ldloc.1
    IL_000b: 
unbox.any [mscorlib]System.Int32
    IL_0010: 
call void [mscorlib]System.Console::WriteLine(int32)
    IL_0015: 
nop
    IL_0016: 
ret
// end of method
Program::Main

  首先是局部变量的声明,IL会在每方法的顶部声明所有的局部变量,使用.locals init

 .locals init ( [0] int32 v1,[1] object v2 )

在示例中声明了v1

抱歉!评论已关闭.