介紹了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自身的語法和語義。