您的位置:首页 > 其它

.NET CIL系列第三篇:正反向工程

2009-05-03 23:39 246 查看
介绍了CIL的基础知识之后,现在来研究CIL编程的实际使用,我们从正反向工程开始讨论。
正反向工程

大家已经知道可以使用ildasm.exe来查看由C#编译器生成的CIL代码(参见.NET CIL系列第一篇:CIL介绍和入门),不过也许不知道ildasm.exe还允许将加载到ildasm.exe的程序集中的CIL都导出到一个外部文件中。一旦有了CIL代码,就可以使用CIL编译器ilasm.exe任意编辑或重新编译代码。

说明:reflector.exe可以用于查看某个程序集的CIL代码,也可以把CIL代码翻译为接近的C#代码。然而如果程序集包含的CIL结构没有等价的C#实现(C#和VB等只各自实现了CIL所有特性集的子集),我们只能使用ildasm.exe。

这个技术叫做正反向工程(round-trip-engineering),在以下这些情况下它将很有用处。
n
需要修改一个没有源代码的程序集
n
正在使用的.NET语言编译器不够完美,产生了一些效率不足的CIL代码,而用户希望修改。
n
用户在构建可与COM互操作的程序集并且希望补充那些在转换过程中丢失的IDL特性,例如COM的[helpstring]特性。
为了解释正反向工程的过程,我们使用文本编辑器来创建一个新的C#代码文件(HelloProgram.cs),并且定义下面的类(也可以使用Visual Studio 2008,但要记得删除AssemblyInfo.cs这个文件来减少生成的CIL代码数量):

// 简单的C#控制台程序

using System;

class Program

{

static void Main(string[] args)

{

Console.WriteLine("Hello CIL code!");

Console.ReadLine();

}

}
将这个文件保存到一个方便的位置(如D:/HelloCilCode),然后使用csc.exe编译:

csc /out:D:/HelloCilCode/HelloProgram.exe D:/HelloCilCode/HelloProgram.cs
现在开启ildasm.exe打开HelloProgram.exe,选择File->Dump菜单选项,将原始的CIL代码保存到一个新的*il文件(HelloProgram.il),这个文件位于包含已编译程序集的文件夹中(结果对话框总的所有默认值都保持不变)。

说明:将程序集中的内容转储到文件时,ildasm.exe会生成一个*.res文件。此时,我们刚才创建的HelloProgram.cs源代码文件和HelloProgram.exe文件已经可以忽略或删除了,因为我们不需要再用到它们了。

现在可以使用任意的文本编辑器打开这个*.il文件来查看CIL代码。结果如下(有少量的格式更改和注释):

//引用的程序集

.assembly extern mscorlib

{

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

.ver 2:0:0:0

}

//我们的程序集

.assembly HelloProgram

{

.hash algorithm 0x00008004

.ver 0:0:0:0

}

.module HelloProgram.exe

.imagebase 0x00400000

.file alignment 0x00000200

.stackreserve 0x00100000

.subsystem 0x0003

.corflags 0x00000001

//我们的类定义

.class private auto ansi beforefieldinit Program

extends [mscorlib]System.Object

{

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

{

//标示出这个方法是可执行文件的入口。

.entrypoint

.maxstack
8

IL_0000:
nop

IL_0001:
ldstr
"Hello CIL code!"

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

IL_000b:
nop

IL_000c:
call
string [mscorlib]System.Console::ReadLine()

IL_0011:
pop

IL_0012:
ret

}

//默认构造函数

.method public hidebysig specialname rtspecialname

instance void
.ctor() cil managed

{

.maxstack
8

IL_0000:
ldarg.0

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

IL_0006:
ret

}

}

首先需要注意的是,打开的*.il文件声明了编译当前程序集所需要引用的外部程序集,这里,我们可以看到有一个.assembly extern标记用来标识总会出现的mscorlib.dll。当然你的类库也许会用到其他程序集的类型,那么就会在这里看到对应的.assembly extern指令。

接着看到的是被赋予了一个默认0.0.0.0版本的HelloProgram.exe程序集的正式定义(如果没有通过AssemblyVersion特性来指定一个值的话)。接下来是进一步通过.module、imagebase这些CIL指令进一步说明该程序集。

在记录了引用的外部程序集和定义了当前的程序集后,是定义Program类型。请注意这个.class指令有很多特性(多数是一些可选的特性),例如extends,它标识类型的基类:

.class private auto ansi beforefieldinit Program

extends
[mscorlib]System.Object

{……}

其余代码实现了这个类的默认构造函数和Main()方法,都在.method指令中定义。一旦成员通过正确的指令和特性定义后,就由操作码来实现。

有一点非常重要,在CIL中与.NET类型(例如System.Console)交互时,总是需要使用这个类型的完整名字。而且,在这个完整的名字前还需要加上以方括号括起来的定义这个类型的程序集的友好名字。考虑下面Main()方法的CIL实现:

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

{

//标示出这个方法是可执行文件的入口。

.entrypoint

.maxstack
8

IL_0000:
nop

IL_0001:
ldstr
"Hello CIL code!"

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

IL_000b:
nop

IL_000c:
call
string [mscorlib]System.Console::ReadLine()

IL_0011:
pop

IL_0012:
ret

}

下面CIL代码中的默认构造函数中使用了另一个“围绕加载(load-centric)”的操作指令(ldarg.0)。这里,加载到栈中的值不是由我们给出的自定义变量,而是当前的对象引用(下面会进一步说明)。同时也要注意,这个默认的构造函数显示的调用了基类的构造函数。

//默认构造函数

.method public hidebysig specialname rtspecialname

instance void
.ctor() cil managed

{

.maxstack
8

IL_0000:
ldarg.0

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

IL_0006:
ret

}

CIL代码标签的作用

我们已经注意到了在每一行代码前都有一个形如IL_XXXX:的前缀(例如IL_0000、IL_0001等)。这些标记被称作代码标签(code label),是可以随便修改的(只要在同一个有效空间内没有重复)。当使用ildasm.exe导出一个程序集时,将会自动在前面加上IL_XXXX这样的代码标签。当然也可以用更有描述性的方法来标识:

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

{

.entrypoint

.maxstack 8

Nothing_1: nop

Load_String: ldstr "Hello CIL code!"

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

Nothing_2: nop

WaitFor_KeyPress: call string [mscorlib]System.Console::ReadLine()

RemoveValueFromStack: pop

Leave_Function: ret

}

事实上大多数代码标签完全是可选的。只有当我们编写有多个分支和循环结构的CIL代码,通过这些代码标签指定逻辑流转到哪里的时候,这些代码标签才是必须的。对当前的示例,完全可以全部移除这些自动生成的标签,不会有什么副作用:

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

{

.entrypoint

.maxstack 8

nop

ldstr "Hello CIL code!"

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

nop

call string [mscorlib]System.Console::ReadLine()

pop

ret

}

CIL交互:修改*.il文件

现在,在对基本的CIL文件的组成了解的基础上,完成正反向工程之旅。我们的目标是对这个CIL文件做如下的修改:

n
增加对System.Windows.Forms.dll的引用;

n
在Main()中增加加载一个局部字符串;

n
调用System.Windows.Forms.MessageBox.Show()方法,使用上面的局部字符串作为参数。

首先通过增加一个新的.assembly指令(同extern特性一同使用)来表示你需要使用System.Windows.Forms.dll。我们只需要修改*.il文件,在表示外部引用mscorlib的代码后增加如下的逻辑:

.assembly extern System.Windows.Forms

{

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

.ver 2:0:0:0

}

要清楚的是,赋给.ver指令的数值可能会根据你所安装的.NET平台版本不同而不同。在上面,我们使用的是System.Windows.Forms.dll的2.0.0.0版本,它的公钥标记是B7 7A 5C 56 19 34 E0 89,通过打开GAC(C:/Windows/assembly文件夹)可以查到你计算机上的System.Windows.Forms.dll程序集版本,可以从程序集的属性页面上复制正确的版本和公钥标记的值。

下面需要修改Main()函数。从*.il文件中找到这个函数,删除实现部分(需要保留.maxstack和entrypoint指令,下面我注释这两个指令的作用):

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

{

//标示出这个方法是可执行文件的入口。

.entrypoint

//方法执行阶段可以被压入栈中的最大的变量数目,默认是8

.maxstack 8

// ToDo: 编写的新的CIL代码。

}

重复一下,我们的目标是将一个新的字符串入栈,然后调用MessageBox.Show()方法(而不是原来的Console.WriteLine())。前面提到过,在使用外部定义的类型时,必须要使用这个类型的完整名称(同程序集的友好名称相结合使用)。在修改Main()方法的时候,需要注意这一点:

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

{

.entrypoint

.maxstack 8

ldstr "CIL is way cool"

call valuetype [System.Windows.Forms]

System.Windows.Forms.DialogResult

[System.Windows.Forms]

System.Windows.Forms.MessageBox::Show(string)

pop

ret

}

实际上,上面的CIL代码对应于如下的C#类定义:

class Program

{

static void Main(string[] args)

{

System.Windows.Forms.MessageBox.Show("CIL is way cool");

}

}

使用ilasm.exe编译CIL代码

假设已经修改并保存了这个*.il文件,那么就可以使用ilasm.exe(CIL编译器)来编译一个新的.NET程序集。这个CIL编译器有大量命令行参数(通过-?选项可以查看它们)。下表列出了一些重要的参数:

Ilasm.exe的命令行参数

参数

作用

/debug

包括调试信息(例如局部变量和参数的名字,行号)

/dll

输出*.dll文件

/exe

输出*.exe文件,这个是默认的设置,可以忽略

/key

编译程序集时使用给定的*.snk文件强名称

/noautoinherit

当基类没有给出时,防止类类型自动从System.Object继承

/output

指定输出的文件名和扩展名。如果没有使用此参数,那么产生的文件名(减去文件扩展名)同第一个源文件名相同

通过如下的命令行,就可以将HelloProgram.il文件编译成.NET的*.exe文件了:

Ilam /exe HelloProgram.il /output=NewAssembly.exe

如果一切正确,那么将看到一个下图所示的报告

  


使用ilasm.exe编译*.il文件

现在,可以运行这个新生成的程序了。这次将看到消息显示在消息框中而不是控制台窗口中,如下图:



正反向工程之旅的结果

OK今天到这里,下次我们将一起学习CIL的各个指令和特性的作用——即CIL自身的语法和语义。
文章来源:http://www.csdnit.com/showtopic-1244.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: