说说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),上面代码中的 extern,extends、private、public都属于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.Program。private是访问类型,auto指明内存布局类型,auto表示内存布局由.NET自动决定(LayoutKind,共有三个值:Sequential,Auto和Explicit),ansi表示在托管和非托管转换时使用的编码类型。extends表示继承。
.method private hidebysig static void Main(string[] args) cil managed
.method,声明方法;private,访问类型;hidebysig,相当于c#方法修饰符new;static,静态方法;void ,返回类型;cil managed,表示托管执行。
.entrypoint
程序入口点。
.maxstack 8
执行方法时的计算堆栈大小。
在方法内部,执行逻辑的编码,被称作操作码(Opcode,Operation Code),如nop,ldstr。操作码也通常被翻译为指令,但是它的英文是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和v2两个局部变量。事实上这里不仅仅是声明这么简单,这里必须要开辟内存空间, 若要开辟内存空间必须要赋值,也就是说声明的同时要进行赋值,这就是默认值的由来。这个操作就是指令中的init 完成的。更深入的分析,请参考http://blog.liranchen.com/2010/07/behind-locals-init-flag.html。
第一个赋值操作int v1 = 2;是如何完成的呢?
1) ldc.i4.2,加载32位整型数2到计算堆栈;
2) stloc.0,从计算堆栈顶部弹出值赋值到局部变量列表中的第一个变量。
再看第二条语句object v2 = v1的实现过程
1) ldloc.0,加载局部变量列表中的一个变量到计算堆栈中;
2) box [mscorlib]System.Int32,对计算堆栈中的顶部值执行装箱操作
3) stloc.1,从计算堆栈顶部弹出值赋值给局部变量列表中的第二个变量
其他操作类似,就不做解释了。
我们再看一个while循环的操作,了解循环是如何实现的,c#代码如下:
staticvoid Main(string[] args)
{
int i = 0;
while (i < 5)
{
i++;
}
}
对应的Il代码为:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 20 (0x14)
.maxstack 2
.entrypoint
.locals init (
[0] int32 i,
[1] bool CS$4$0000
)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: br.s IL_000b
// loop start (head: IL_000b)
IL_0005: nop
IL_0006: ldloc.0
IL_0007: ldc.i4.1
IL_0008: add
IL_0009: stloc.0
IL_000a: nop
IL_000b: ldloc.0
IL_000c: ldc.i4.5
IL_000d: clt
IL_000f: stloc.1
IL_0010: ldloc.1
IL_0011: brtrue.s IL_0005
// end loop
IL_0013: ret
} //
end of method Program::Main
首先声明了一个bool类型的局部变量([1] bool CS$4$0000)。在循环开始之前,先执行
IL_0003: br.s IL_000b
跳转到IL_000b处。先加载变量到计算堆栈:
IL_000b: ldloc.0
IL_000c: ldc.i4.5
然执行比较操作:
IL_000d: clt
如果第一个值小于第二值,将整数1压入计算堆栈,否则将整数0压入计算堆栈,同时执行弹栈操作。接下来将比较结果赋值给局部变量列表中的第二个变量:
IL_000f: stloc.1
之后再将局部变量列表中的第二个变量压栈:
IL_0010: ldloc.1
然后判断bool值,确定是否执行循环体内代码:
IL_0011: brtrue.s IL_0005
如果为true,跳转到 IL_0005处,然后继续执行加法操作:
IL_0005: nop
IL_0006: ldloc.0
IL_0007: ldc.i4.1
IL_0008: add
IL_0009: stloc.0
IL_000a: nop
IL_000a: nop执行之后再从 IL_000b处开始新一轮的循环。
关于IL指令的介绍就到这里,不然就收不住笔了,越写越多。现在把思路收回到Emit,当我们了解相关C#代码如何用Il实现之后,下一步就是考虑将Il之类植入方法内部,在Emit的过程中,我们拿什么来表达要植入的Il指令呢?是原生的字符串吗?当然不是,.NET准备了OpCodes 类,该类以字段的形式封装了操作码。
解决了写操作码的问题,下一个问题就是如何发出(Emit)操作码到方法内部了?这就是ILGenerator类。
2.2 ILGenerator
ILGenerator类的功能,一句话,生成IL代码。想要获取ILGenerator类的实例,只有三个途径:
1) ConstructorBuilder.GetILGenerator方法
2) DynamicMethod.GetILGenerator 方法
3) MethodBuilder.GetILGenerator 方法
上面涉及到了在Emit中能够动态生成方法的三种途径,ConstructorBuilder类用来配置的构造函数,构造函数内部的IL要使用它的GetILGenerator方法返回的ILGenerator类发出。DynamicMethod类,是在当前运行上下文环境中动态生成方法的类,使用该类不必事先创建程序集、模块和类型,同样发出其内部的IL使用DynamicMethod.GetILGenerator方法返回的ILGenerator类实例。MethodBuilder我在《说说emit(上)基本操作》中做了介绍,写到这里,突然发现很悲剧的是,竟然没有办法很顺畅的和上篇博客很顺畅的衔接起来。看来写文章也是要讲求设计的。既然无法很好的衔接,也就不强求了,这里将上篇博客提到的示例糅合到一起,实现几个超级简单的Mock接口的例子。
我要实现的调用效果是这样的:
Mock<IAssessmentAopAdviceProvider> mocker = newMock<IAssessmentAopAdviceProvider>();
mocker.Setup(t => t.Before(3)).Returns("HelloWorld!");
Console.WriteLine(mocker.Obj.Before(2));
接收一个接口,初始化一个Mock类的实例,然后通过Setup和Returns扩展方法设定实现该接口的实例在指定方法上的返回值。这里我们先不考虑对不同参数的处理逻辑。
Mock类的定义如下:
publicclassMock<T>
{
public T Obj
{
get;
set;
}
publicSetupContext Contex { get; set; }
public Mock()
{
}
}
Mock类的Obj属性是特定接口的实例。Contex属性是上下文信息,当前内容很简单,只包含一个MethodInfo属性。定义如下:
publicclassSetupContext
{
publicMethodInfo MethodInfo { get; set; }
}
这个上下文信息目前只满足接口有一个方法的情况,对应的相关实现也只考虑一个方法,在这个示例程序中我们无需过分纠结其他细节,以免乱了主次。
接下来是三个扩展方法。
publicstaticclassMockExtention
{
publicstaticMock<T> Setup<T>(thisMock<T> mocker, Expression<Action<T>> expression)
{
mocker.Contex = newSetupContext();
mocker.Contex.MethodInfo = expression.ToMethodInfo();
return mocker;
}
publicstaticvoid Returns<T>(thisMock<T> mocker, object returnValue)
{
if (mocker.Contex != null && mocker.Contex.MethodInfo != null)
{
//这里为简单起见,只考虑IAssessmentAopAdviceProvider接口
mocker.Obj= (T)AdviceProviderFactory.GetProvider(mocker.Contex.MethodInfo.Name,(string)returnValue);
}
}
publicstaticMethodInfo ToMethodInfo(thisLambdaExpression expression)
{
var memberExpression = expression.Body as System.Linq.Expressions.MethodCallExpression;
;
if (memberExpression != null)
{
return memberExpression.Method;
}
returnnull;
}
}
Setup是Mock类的扩展方法,配置要Mock的方法信息;Returns扩展方法则调取对应的工厂获取接口的实例。
ToMethodInfo是LambdaExpression扩展方法,该方法从Lambda表达式中获取MethodInfo。
这里对应的对象工厂也简单化,直接返回IAssessmentAopAdviceProvider接口实例。
首先,在构造函数中,初始化assemblyBuilder和moduleBuilder,代码如下:
static AdviceProviderFactory()
{
assemblyName.Version = newVersion("1.0.0.0");
assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProviderModule", "test.dll",true);
}
上面的代码就不解释了,相关内容在前一篇博客有详细的解释。
GetProvider方法当前没有任何逻辑,只是调用了CreateInstance方法。
代码如下:
publicstaticIAssessmentAopAdviceProvider GetProvider(string methodName,string returnValue)
{
//创建接口的实例
return CreateInstance("MvcAdviceReportProviderInstance",methodName,returnValue);
}
CreateInstance方法负责创建类型和方法的实现:
privatestaticIAssessmentAopAdviceProvider CreateInstance(string instanceName,string methodName,string returnValue)
{
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider.MvcAdviceProviderType", TypeAttributes.Public, typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
// typeBuilder.AddInterfaceImplementation(typeof(IAssessmentAopAdviceProvider));
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), newType[] { typeof(int) });
beforeMethodBuilder.DefineParameter(1, ParameterAttributes.None ,"value");
ILGenerator generator1 = beforeMethodBuilder.GetILGenerator();
LocalBuilder local1= generator1.DeclareLocal(typeof(string));
local1.SetLocalSymInfo("param1");
generator1.Emit(OpCodes.Nop);
generator1.Emit(OpCodes.Ldstr, returnValue);
generator1.Emit(OpCodes.Stloc_0);
generator1.Emit(OpCodes.Ldloc_0);
generator1.Emit(OpCodes.Ret);
Type providerType = typeBuilder.CreateType();
assemblyBuilder.Save("test.dll");
IAssessmentAopAdviceProvider provider = Activator.CreateInstance(providerType) asIAssessmentAopAdviceProvider;
return provider;
}
在CreateInstance方法中,首先定义类型:
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider.MvcAdviceProviderType", TypeAttributes.Public, typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
注意第三个和第四个参数,分别是该类型的基类型和实现的接口列表。
然后我们根据传人的方法名称和参数定义方法:
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual, typeof(string), newType[] { typeof(int) });
DefineMethod方法中,传人的第一个参数是方法的名称,第二个参数是访问类型,第三个参数是修饰符,第四个参数是方法的参数类型列表。这里需要注意第二个参数,也就是方法的修饰符,因为接口中的方法定义都是virtual的,所以在实现接口的时候,方法也必须声明为MethodAttributes.Virtual。
接下来定义方法的参数,使用MethodBuilder.DefineParameter方法。
beforeMethodBuilder.DefineParameter(1, ParameterAttributes.None ,"value");
DefineParameter方法的第一参数指定当前定义的参数在方法参数列表中的顺序,从1开始,如果设置为0则代表方法的返回参数。第二个参数是设置参数的特性,如输入参数,输出参数等等。第三个参数是指定该参数的名称。
方法定义完毕,接下来就是发出Opcode,返回一个指定的字符串。先获取ILGenerator实例,如下:
ILGenerator generator1 = beforeMethodBuilder.GetILGenerator();
我们若想返回一个字符串,必须先为该字符串声明一个局部变量,可以使用LocalBuilder.DeclareLocal方法,如下:
LocalBuilder local1= generator1.DeclareLocal(typeof(string));
local1.SetLocalSymInfo("param1");
ocal1.SetLocalSymInfo("param1")指定局部变量的名称。
需要注意,如果模块定义时不允许发出符号信息,是无法使用SetLocalSymInfo方法的,AdviceProviderFactory的构造函数中,我们定义模块的代码
moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProviderModule", "test.dll",true);
最后一个参数是指定是否允许发出符号信息的。
发出的Opcode很简单:
generator1.Emit(OpCodes.Nop);
generator1.Emit(OpCodes.Ldstr, returnValue);
generator1.Emit(OpCodes.Stloc_0);
generator1.Emit(OpCodes.Ldloc_0);
generator1.Emit(OpCodes.Ret);
第一条指令不执行任何操作。
第二条指令加载一个字符串到计算堆栈中。
第三条指令赋值计算堆栈顶部的数据到局部变量列表中的第一个变量。
第四条指令加载局部变量列表中的第一个变量的数据引用到计算堆栈。
第五条指令方法返回。
整个Emit的过程结束了,接下来要创建实例:
Type providerType = typeBuilder.CreateType();
assemblyBuilder.Save("test.dll");
IAssessmentAopAdviceProvider provider = Activator.CreateInstance(providerType) asIAssessmentAopAdviceProvider;
在上面的代码中,我们保存了模块,使用反编译工具加载该模块,看看生成的代码是不是预期的。Il代码如下:
class public auto ansi MvcAdviceProvider.MvcAdviceProviderType
extends [mscorlib]System.Object
implements [EmitMock]EmitMock.IAssessmentAopAdviceProvider
{
// Methods
.method public virtual
instance string Before (
int32 'value'
) cil managed
{
// Method begins at RVA 0x2050
// Code size 9 (0x9)
.maxstack 1
.locals init (
[0] string
)
IL_0000: nop
IL_0001: ldstr "HelloWorld!"
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ret
} //
end of method MvcAdviceProviderType::Before
.method public specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x2068
// Code size 7 (0x7)
.maxstack 2
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} //
end of method MvcAdviceProviderType::.ctor
} // end of class
MvcAdviceProvider.MvcAdviceProviderType
c#代码如下:
using EmitMock;
using System;
namespace MvcAdviceProvider
{
public class MvcAdviceProviderType : IAssessmentAopAdviceProvider
{
public string Before(int value)
{
return "HelloWorld!";
}
}
}
最后,编写一个控制台程序来测试一下:
staticvoid Main(string[] args)
{
EmitMock.Mock<IAssessmentAopAdviceProvider> mocker = newMock<IAssessmentAopAdviceProvider>();
mocker.Setup(t => t.Before(3)).Returns("HelloWorld!");
Console.WriteLine(mocker.Obj.Before(2));
Console.Read();
}
运行结果如下图:
在下一篇博客,不准备继续介绍Emit的应用,在抱怨Emit的繁琐之余,是否还有其他选择呢?我们来谈一谈《Emit和Mono.cecil》。