1-
我們用C#、VB.NET語言編寫的代碼最終都會被編譯成程序集或IL。因此用VB.NET編寫的代碼 可以在C#中修改,隨後在COBOL中使用。因此,理解IL是非常有必要的。
一旦熟悉了IL,理解.NET 技術就不會有障礙了,因為所有的.NET語言都會編譯為IL。IL是一門中性語言。IL是先發明的,隨後才有 了C#、VB.NET等語言。
我們將在一個短而精辟的程序中展示IL。我們還假設讀者至少熟悉一 門.NET語言。
a.il
.method void vijay()
{
}
隨後,我們用IL編寫了一個非常短小的IL程序——它顯然是不能工作的,並 將它命名為a.il。那麼我們怎麼才能把它編譯為一個可執行程序呢?不需要為此而焦急,Microsoft提供 了一個ilasm程序,它的唯一任務就是從IL文件中創建可執行文件。
在允許這個命令之前,要確保 你的變量路徑被設置為framework中的bin子目錄。如果不是,請輸入命令如下:
set path=c:\progra~1\microsoft.net\frameworksdk\bin;%PATH%
現在,我們使用如下命令:
c:\il>ilasm /nologo /quiet a.il
這樣做會生成下面的錯誤:
Source file is ANSI
Error: No entry point declared for executable
***** FAILURE *****
將來,我們將不會顯示由ilasm生成的輸出的第一行 和最後一行。我們還將移除非空白行之間的空白行。
在IL中,允許我們使用句點.作為一行的開始 ,這是一條指令,要求編譯器執行某個功能,如創建一個函數或類,等等。任何開始於句點的語句都是一 條實際俄編譯器指令。
.method表示創建一個名為vijay的函數(或方法),並且這個函數返回 void,即它不返回任何值。因為缺少較好的命名法則,函數名稱vijay顯得很隨意。
匯編器顯然理 解不了這個程序,從而會顯示“no entry point”的消息。這個錯誤信息的生成是因為IL文件 能夠包括無數的函數,而匯編器無法區分哪個會被首先被執行。
在IL中,首先被執行的函數被稱 為進入點(entrypoint)函數。在C#中,這個函數是Main。函數的語法是,名稱之後是一對圓括號()。 函數代碼的開始和結束用花括號{}來表示。
a.il
.method void vijay()
{
.entrypoint
}
c:\il>ilasm /nologo /quiet a.il
Source file is ANSI
Creating PE file
Emitting members:
Global Methods: 1;
Writing PE file
Operation completed successfully
現在不會生成任何錯誤了 。偽指令(directive)entrypoint表示程序執行必須開始於這個函數。在這個例子中,我們不得不使用 這個偽指令,雖然事實上這個程序只有一個函數。當在DOS提示符中給出dir命令後,我們看到有3個文件 會被創建。a.exe是一個可執行文件,現在可以執行它來看到程序的輸出。
C:\il>a
Exception occurred: System.BadImageFormatException: Exception from HRESULT: 0x8007000B. Failed to load C:\IL\A.EXE.
當我們試圖執行上面的程序時,我們的運氣似乎不太好,因為會生成上面的運行時錯誤。一個可能的 原因是,這個函數是不完整的,每個函數都應當具有一個“函數結束”指令在函數體中。我們 匆忙之中顯然沒有注意到這個事實。
a.il
.method void vijay()
{
.entrypoint
ret
}
“函數結束”指令被稱為ret。前面所有的函數都必須以這個指令作為結束。
Output
Exception occurred: System.BadImageFormatException: Exception from HRESULT: 0x8007000B. Failed to load C:\IL\A.EXE.
在執行這個程序時,我們再次得到了相同的錯誤。這次我們的問題又在哪裡呢?
a.il
.assembly mukhi {}
.method void vijay()
{
.entrypoint
ret
}
錯誤在於我們忘記在名稱後面使用必不可少的偽指令assembly。我們將其合成在上面的 代碼中,並在一對空的花括號之後使用了名稱mukhi。這個程序集偽指令用於給出程序的名稱。它又被稱 為一個部署單元。
上面的代碼是可以匯編而沒有任何錯誤的最小的程序,雖然它在執行時並沒有 做什麼有用的事情。它沒有任何名為Main的函數。它只有一個帶有entrypoint偽指令的函數vijay。現在 匯編這個程序並運行而根本不會有任何錯誤。
在.NET中,程序集的概念是極其重要的,應該對其 有徹底的認識。我們將在本章後半部分使用這個偽指令。
a.il
.assembly mukhi {}
.method void vijay()
{
.entrypoint
ret
}
.method void vijay1()
{
.entrypoint
ret
}
Error
***** FAILURE *****
上面錯誤信息的原因是,上面的程序有2個函 數,vijay和vijay1,每個函數都包括了.entrypoint偽指令。正如前面提到的那樣,這個指令指定了關於 那個函數會被首先執行。
因此,在功能上,它類似於C#中的Main函數。當C#代碼被轉換為IL代碼 時,在Main函數中包含的代碼會被轉換為IL中的函數中並包括.entrypoint偽指令。例如,如果在COBOL程 序中執行的第一個函數被稱為abc,那麼在IL中生成的代碼就會在這個函數中插入.entrypoint偽指令。
在常規的程序語言中,首先被執行的函數必須有一個特定的名稱,例如Main,但是在IL中,只需 要一個.entrypoint偽指令。因此,因為一個程序只能由一個開始點,所以在IL代碼中只允許一個函數包 括.entrypoint偽指令。
迫切地看到,沒有生成任何錯誤消息編號或說明,使得調試這個錯誤非常 困難。
a.il
.assembly mukhi {}
.method void vijay()
{
ret
.entrypoint
}
.entrypoint偽指令需要被定位為函數中的第一個指令或最後一個指令。它僅出現在函數 體中,從而將它的狀態宣布為第一個被執行的函數。偽指令不是程序集指令,甚至可以被放置在任何ret 指令之後。提醒你一下,ret表示函數代碼的結束。
a.il
.assembly mukhi {}
.method void vijay()
{
.entrypoint
call void System.Console::WriteLine()
ret
}
我們可能有一個用C#、VB.NET編寫的函數,但是在IL中執行這個函數的機制是相同的。 如下所示:
我們必須使用匯編指令調用。調用指令之後,按照給定的順序,為以下詳細內容:
函數的返回類型(void)
命名空間(System)
類 (Console)
函數名稱 (WriteLine())
函數被調用但不會生成任何輸出。因為,我們傳遞一個參數到WriteLine函數中。
a.il
.assembly mukhi {}
.method void vijay()
{
.entrypoint
call void System.Console::WriteLine(class System.String)
ret
}
上面的代碼有一處“閃光點”。當一個函數在IL中被調用時,除了它的返回 類型之外,被傳遞的參數的數據類型,也必須被指定。我們將Writeline設置為——希望得到 一個System.String類型作為參數,但是由於沒有字符串被傳遞到這個函數中,所以它會生成一個運行時 錯誤。
因此,在調用一個函數時,在IL和其他程序語言之間有一個明顯的區別。在IL中,當我們 調用一個函數,我們必須指定關於該函數我們所知道的任何內容,包括它的返回類型和它的參數的數據類 型。通過在運行期間進行恰當的檢查,保證了匯編器能夠在語法上驗證代碼的有效性。
現在我們 將看到如何將參數傳遞到一個函數中。
a.il
.assembly mukhi {}
.method void vijay()
{
.entrypoint
ldstr "hell"
call void System.Console::WriteLine(class System.String)
ret
}
Output
hell
匯編器指令ldstr把字符串放到棧上。Ldstr的名稱是文本 "load a string on the stack"的縮寫版本。棧是一塊內存區域,它用來傳遞參數到函數中。 所有的函數從棧上接收它們的參數。因此,像ldstr這樣的指令是必不可少的。
a.il
.assembly mukhi {}
.method public hidebysig static void vijay()il managed
{
.entrypoint
ldstr "hell"
call void System.Console::WriteLine(class System.String)
ret
}
Output
hell
我們在方法vijay上添加了一些特性。接下來我們將逐個講解 它們。
public:被稱為可訪問特性,它決定了都有誰可以訪問一個方法。public意味著這個方法 可以被程序的其他任何部分所訪問。
hidebysig:類可以從其它多個類中派生。hidebysig特性保 證了父類中的函數在具有相同名稱或簽名的派生類中會被隱藏。在這個例子中,它保證了如果函數vijay 出現在基類中,那麼它在派生類中就是不可見的。
static:方法可以是靜態的或非靜態的。靜態 方法屬於一個類而不屬於一個實例。因此, 就像我們只有一個單獨的類,我們不能擁有一個靜態函數的 多份復制。靜態函數可以在哪裡創建是沒有約束的。帶有entrypoint指令的函數必須是靜態的。靜態函數 必須具有相關聯的實體或者源代碼,並且使用類型名稱而不是實例名稱來引用它們。
il managed: 由於它的復雜性質,我們將關於這個特性的解釋延後。當時機成熟時,它的功能將會被解釋清楚。
上面涉及的特性並沒有修改函數的輸出。 稍後,你將明白為什麼我們要提供這些特性的解釋。
無論何時我們用C#語言編寫一個程序,我們首先在類的名稱前指定關鍵字class,隨後,我們將源 代碼封閉在一對花括號內。示范如下:
a.cs
class zzz
{
}
讓我們引進稱為class的IL指令:
a.il
.assembly mukhi {}
.class zzz
{
.method public hidebysig static void vijay()il managed
{
.entrypoint
ldstr "hell"
call void System.Console::WriteLine(class System.String)
ret
}
}
注意到,匯編器輸出中的改變: Class 1 Methods: 1;
Output
hell
偽指令.class之後是類的名稱。它在IL中是可選的,讓我們通過添 加一些類的特性來增強這個類的功能。
a.il
.assembly mukhi {}
.class private auto ansi zzz
{
.method public hidebysig static void vijay()il managed
{
.entrypoint
ldstr "hell"
call void System.Console::WriteLine(class System.String)
ret
}
}
Output
hell
我們添加了 3個特性到類的偽指令中。
private:這 表示了對類的成員的訪問被約束為只能在當前類中。
auto:這表示類在內存中的布局將只由運行 時來決定,而不是由我們的程序決定。
ansi:源代碼通常被劃分為兩個主要的類別:托管代碼和 非托管代碼。
以諸如C語言編寫的代碼被稱為非托管代碼或不可信任的代碼。我們需要一個特性來 處理非托管代碼和托管代碼之間的互操作。例如,當我們想要在托管和非托管代碼之間轉移字符串時,這 個特性會被使用到。
如果我們跨越托管代碼的邊界並鑽進非托管代碼的領域,那麼一個字符串 ——由2字節Unicode字符組成的數組,將會被轉換為一個ANSI字符串——由1字節 ANSI字符組成的數組;反之亦然。修飾符ansi用於消除托管和非托管代碼之間的轉換。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.method public hidebysig static void vijay()il managed
{
.entrypoint
ldstr "hell"
call void System.Console::WriteLine(class System.String)
ret
}
}
Output
hell
類zzz從System.Object中派生。在.NET中,為了定義類型的 一致性,所有的類型最終都派生於System.Object。因此,所有的對象都有一個共同的基類Object。在IL 中,類從其它類中派生,與C++、C#和Java的表現方式相同,
a.il
.module aa.exe
.subsystem 3
.corflags 1
.assembly extern mscorlib
{
.originator = (03 68 91 16 D3 A4 AE 33 )
.hash = (52 44 F8 C9 55 1F 54 3F 97 D7 AB AD E2 DF 1D E0
F2 9D 4F BC )
.ver 1:0:2204:21
}
.assembly a as "a"
{
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.class private auto ansi zzz extends System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldstr "hell"
call void System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
.maxstack 8
ldstr "hell1"
call void System.Console::WriteLine(class System.String)
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
Output
hell
你一定想知道為什麼我們會編寫出這麼難看的程序。在迷霧 驅散之前你需要保持耐心,所有的一切就要開始有意義了。我們將逐個解釋新引進的函數和特性。
.ctor: 我們引進了一個新的函數.ctor,它調用了WriteLine函數來顯示hell1,但是它沒有被調 用。.ctor涉及到了構造函數。
rtspecialname: 這個特性會告訴運行時——函數的名 稱是特殊的,它會以一種特殊的方式被對待。
specialname: 這個特性會提示編譯器和工具 ——函數是特殊的。運行時可能選擇忽略這個特性。
instance: 一個常規的函數會被 一個實例函數調用。這樣一個函數與一個對象關聯,不同於靜態方法,後者關聯到一個類。
在合 適的時候,為函數選擇特定名稱的原因會變得明朗。
ldarg.0: 這是一個匯編器指令,它加載this 指針或第0個參數的地址到執行棧上。我們隨後將詳細解釋ldarg.0。
mscorlib: 在上面的程序中 ,函數.ctor會被基類System.Object調用。通常,函數的名稱以包括代碼的庫的名稱作為前綴。這個庫的 名稱被放置在方括號中。在這個例子中,它是可選的——因為mscorlib.dll是默認的庫,並且 它包括了.NET所需要的大部分類。
.maxstack: 這個偽指令指定了在一個方法被調用時,能夠出現 在計算棧上的元素的最大數量。
.module: 所有的IL文件必須是一個邏輯實體的一部分,或它們的 組合體,我們將這些實體稱為模塊(module)。文件被添加到使用了.module偽指令的模塊中。模塊的名 稱可能被規定為aa.exe,但是可執行文件的名稱和前面保持一樣,即a.exe。
.subsystem: 這個指 令用於指定可執行體運行在什麼操作系統上。這是另一種指定可執行體所代表的種類的方式。一些數字值 和它們對應的操作系統如下所示:
2 - A Windows Character 子系統。
3 - A Windows GUI 子系統。
5 – 像OS/2這樣的老系統。
.corsflags: 這個偽指令用於指定對於64 位計算機唯一的標志。值1表示它是從il中創建的可執行文件,而值64表示一個庫。
.assembly: 在前面,我們曾經簡單涉及過一個名為.assembly的指令。現在讓我們進行深入的研究。
無論我們 創建了什麼,都是一個稱為清單(manifest)的實體的一部分。.assembly偽指令標注了一個清單的開始 位置。在層次上,模塊是清單最小的實體。.assembly偽指令指定了這個模塊屬於哪個程序集。模塊只能 包括一個單獨的.assembly偽指令。
對於exe文件,這個偽指令的存在是必須的,但是,對於.dll 中的模塊,則是可選的。這是因為,我們需要使用這個偽指令來創建一個程序集。這是.NET的基本需要。 程序集偽指令包括了其它偽指令。
.hash: 散列計算是一門在計算機世界中通用的技術,這裡有大 量使用到的散列方法或算法。這個偽指令用於散列計算。
.ver: .ver:偽指令包括了4個由冒號分 割的數字。按照下面給定的順序,它們代表了下面的信息:
主版本編號
次版本編號
內部版本號
修訂版本號
extern: 如果有涉及到其它程序集的需求,就要使用到 extern偽指令。.NET核心類的代碼位於mscorlib.dll中。除了這個dll之外,當我們的程序需要涉及到大 量其它的dll時,extern偽指令就要排上用場了。
originator: 在轉移到解釋上面程序的本質和意 義之前,這是我們要研究的最後一個偽指令。這個偽指令揭示了創建該dll的標識。它包括了dll的所有者 公鑰的8個字節。它顯然是一個散列值。
讓我們以一種不同的方式一步一步地溫習到目前為止我們 所做的事情。
(a)我們開始於一個我們能夠編寫的最簡單的程序。這個程序被稱為a.cs,並包括 了下面的代碼:
a.cs
class zzz
{
public static void Main()
{
System.Console.WriteLine("hi");
}
}
(b)然後我們使用下面的命令運行C#編譯器。
>csc a.cs
因此,會創建名為a.exe的exe文件。
(c)在可執行體中,我們運行一個名為 ildasm的程序,它是由Microsoft提供的:
>ildasm /out=a.txt a.exe
這就創建了一個txt文件,具有下面的內容:
a.txt
// Microsoft (R) .NET Framework IL Disassembler. Version 1.0.2204.21
// Copyright (C) Microsoft Corp. 1998-2000
// VTableFixup Directory:
// No data.
.subsystem 0x00000003
.corflags 0x00000001
.assembly extern mscorlib
{
.originator = (03 68 91 16 D3 A4 AE 33 ) // .h..3
.hash = (52 44 F8 C9 55 1F 54 3F 97 D7 AB AD E2 DF 1D E0
F2 9D 4F BC ) // RD..U.T?O.
.ver 1:0:2204:21
}
.assembly a as "a"
{
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.module aa.exe
// MVID: {89CFAD60-F5BD-11D4-A55A-96B5C7D61E7B}
.class private auto ansi zzz
extends System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
// Code size 11 (0xb)
.maxstack 8
IL_0000: ldstr "hell"
IL_0005: call void System.Console::WriteLine(class System.String)
IL_000a: ret
} // end of method zzz::vijay
.method public hidebysig specialname rtspecialname
instance void .ctor() il managed
{
// Code size 17 (0x11)
.maxstack 8
IL_0000: ldstr "hell"
IL_0005: call void System.Console::WriteLine(class System.String)
IL_000a: ldarg.0
IL_000b: call instance void [mscorlib]System.Object::.ctor()
IL_0010: ret
} // end of method zzz::.ctor
} // end of class zzz
//*********** DISASSEMBLY COMPLETE ***********************
當我們閱讀上面的 文件時,你將明白它的所有內容都已經在前面解釋過了。我們開始於一個簡單的C#程序,然後將它編譯到 一個可執行文件中。在正常的環境下,它將被轉換為機器語言或這個程序運行在所在的計算機/微處理器 的匯編程序。一旦創建了可執行體,我們就使用ildasm來反匯編它。反匯編輸出被保存到一個新的文件 a.txt中。這個文件可能被命名為a.il,然後我們可以通過對其運行ilasm反過來再次創建這個可執行體。
讓我們看一下最小的VB.NET程序。我們將它命名為one.vb,而它的源代碼如下所示:
one.vb
Public Module modmain
Sub Main()
System.Console.WriteLine("hell")
End Sub
End Module
在編寫完上述的代碼後,我們運行Visual.Net編譯器vbc如下:
>vbc one.vb
這就產生了文件one.exe。
下面,我們執行ildasm 如下所示:
>ildasm /out=a.txt one.exe
這就生成了下面的文件 a.txt:
a.txt
// Microsoft (R) .NET Framework IL Disassembler. Version 1.0.2204.21
// Copyright (C) Microsoft Corp. 1998-2000
// VTableFixup Directory:
// No data.
.subsystem 0x00000003
.corflags 0x00000001
.assembly extern mscorlib
{
.originator = (03 68 91 16 D3 A4 AE 33 ) // .h..3
.hash = (52 44 F8 C9 55 1F 54 3F 97 D7 AB AD E2 DF 1D E0
F2 9D 4F BC ) // RD..U.T?.O.
.ver 1:0:2204:21
}
.assembly extern Microsoft.VisualBasic
{
.originator = (03 68 91 16 D3 A4 AE 33 ) // .h..3
.hash = (5B 42 1F D2 5E 1A 42 83 F5 90 B2 29 9F 35 A1 BE
E5 5E 0D E4 ) // [B..^.B.).5.
.ver 1:0:0:0
}
.assembly one as "one"
{
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module one.exe
// MVID: {1ED19820-F5C2-11D4-A55A-96B5C7D61E7B}
.class public auto ansi modmain
extends [mscorlib]System.Object
{
.custom instance void [Microsoft.VisualBasic] Microsoft.VisualBasic.Globals/Globals$StandardModuleAttribute::.ctor() = ( 01 00 00 00 )
.method public static void Main() il managed
{
// Code size 11 (0xb)
.maxstack 1
.locals init (class System.Object[] V_0)
IL_0000: ldstr "hell"
IL_0005: call void [mscorlib]System.Console::WriteLine(class System.String)
IL_000a: ret
} // end of method modmain::Main
} // end of class modmain
.class private auto ansi _vbProject
extends [mscorlib]System.Object
{
.custom instance void [Microsoft.VisualBasic] Microsoft.VisualBasic.Globals/Globals$StandardModuleAttribute::.ctor() = ( 01 00 00 00 )
.method public static void _main(class System.String[] _s) il managed
{
.entrypoint
// Code size 6 (0x6)
.maxstack 8
IL_0000: call void modmain::Main()
IL_0005: ret
} // end of method _vbProject::_main
} // end of class _vbProject
//*********** DISASSEMBLY COMPLETE ***********************
你將驚訝地看到由 兩個不同的編譯器所生成的輸出幾乎是相同的。我向你展示了這個示例用以證實——語言的無 關性,最終,源代碼將會被轉換為IL代碼。無論我們使用VB.NET或C#,都會調用相同的WriteLine函數。
因此,程序語言間的不同現在是表面上的問題。無休止的爭論那個語言是最優的是沒有意義的。 從而,IL使得程序員可以自由使用他們所選擇的語言。
讓我們揭開上面給出的代碼的神秘面紗。
每個VB.NET程序都需要被包括在一個模塊中。我們稱之為modmain。Visual Basic中的所有模塊都 是以關鍵字End結束的,從而我們會看到End Module。這是VB在語法上不區別於C#的地方 ——C#不理解模塊是什麼。
在VB.NET中,函數被稱為子程序。我們需要子程序來標注 程序執行的開始位置。這個子程序被稱為Main。
VB.NET代碼不僅關聯到mscorlib.dll,還使用了 文件Microsoft.VisualBasic。
在IL中會創建一個名為_vbProject的類,因為在VB中類的名稱不是 必須的。
稱為_main的函數是子函數的開始,因為它具有entrypoint偽指令。它的名稱前面有一個 下劃線。這些名稱是由VB編譯器選擇用來生成IL代碼的。
這個函數會傳遞一個字符串數組作為參 數。它具有一個自定義偽指令來處理元數據的概念。
接下來,我們具有這個函數的完整原型,以 一系列可選的字節作為終結。這些字節是元數據規范中的一部分。
模塊modmain被轉換為一個具有 相同名稱的類。和之前一樣,這個類還具有相同的偽指令.custom和一個Main函數。該函數使用了名 為.locals的偽指令在棧上創建一個只能在這個方法中使用變量。這個變量只存在於方法執行期間,當方 法停止運行時,它就會“消亡”。
字段還存儲在內存中,但是需要更長的時間來為它 們分配內存。關鍵字init表示在創建期間,這些變量應該被初始化為它們的默認值。默認值依賴於變量的 類型。數值總是被初始化為值ZERO。關鍵字init之後是這些變量的數據類型和它的名稱。