咱們用C#、VB.NET語言編寫的代碼最終都會被編譯成程序集或IL。所以用VB.NET編寫的代碼能夠在C#中修改,隨後在COBOL中使用。所以,理解IL是很是有必要的。程序員
一旦熟悉了IL,理解.NET技術就不會有障礙了,由於全部的.NET語言都會編譯爲IL。IL是一門中性語言。IL是先發明的,隨後纔有了C#、VB.NET等語言。算法
咱們將在一個短而精闢的程序中展現IL。咱們還假設讀者至少熟悉一門.NET語言。編程
a.il數組
隨後,咱們用IL編寫了一個很是短小的IL程序——它顯然是不能工做的,並將它命名爲a.il。那麼咱們怎麼才能把它編譯爲一個可執行程序呢?不須要爲此而焦急,Microsoft提供了一個ilasm程序,它的惟一任務就是從IL文件中建立可執行文件。sass
在容許這個命令以前,要確保你的變量路徑被設置爲framework中的bin子目錄。若是不是,請輸入命令以下:cors
set path=c:\progra~1\microsoft.net\frameworksdk\bin;%PATH%ide
如今,咱們使用以下命令:函數
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
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
「函數結束」指令被稱爲ret。前面全部的函數都必須以這個指令做爲結束。
Output
Exception occurred: System.BadImageFormatException: Exception from HRESULT: 0x8007000B. Failed to load C:\IL\A.EXE.
在執行這個程序時,咱們再次獲得了相同的錯誤。此次咱們的問題又在哪裏呢?
a.il
錯誤在於咱們忘記在名稱後面使用必不可少的僞指令assembly。咱們將其合成在上面的代碼中,並在一對空的花括號以後使用了名稱mukhi。這個程序集僞指令用於給出程序的名稱。它又被稱爲一個部署單元。
上面的代碼是能夠彙編而沒有任何錯誤的最小的程序,雖然它在執行時並無作什麼有用的事情。它沒有任何名爲Main的函數。它只有一個帶有entrypoint僞指令的函數vijay。如今彙編這個程序並運行而根本不會有任何錯誤。
在.NET中,程序集的概念是極其重要的,應該對其有完全的認識。咱們將在本章後半部分使用這個僞指令。
a.il
Error
***** FAILURE *****
上面錯誤信息的緣由是,上面的程序有2個函數,vijay和vijay1,每一個函數都包括了.entrypoint僞指令。正如前面提到的那樣,這個指令指定了關於那個函數會被首先執行。
所以,在功能上,它相似於C#中的Main函數。當C#代碼被轉換爲IL代碼時,在Main函數中包含的代碼會被轉換爲IL中的函數中幷包 括.entrypoint僞指令。例如,若是在COBOL程序中執行的第一個函數被稱爲abc,那麼在IL中生成的代碼就會在這個函數中插 入.entrypoint僞指令。
在常規的程序語言中,首先被執行的函數必須有一個特定的名稱,例如Main,可是在IL中,只須要一個.entrypoint僞指令。所以,由於一個程序只能由一個開始點,因此在IL代碼中只容許一個函數包括.entrypoint僞指令。
迫切地看到,沒有生成任何錯誤消息編號或說明,使得調試這個錯誤很是困難。
a.il
.entrypoint僞指令須要被定位爲函數中的第一個指令或最後一個指令。它僅出如今函數體中,從而將它的狀態宣佈爲第一個被執行的函數。僞指令不是程序集指令,甚至能夠被放置在任何ret指令以後。提醒你一下,ret表示函數代碼的結束。
a.il
咱們可能有一個用C#、VB.NET編寫的函數,可是在IL中執行這個函數的機制是相同的。以下所示:
咱們必須使用匯編指令調用。調用指令以後,按照給定的順序,爲如下詳細內容:
函數被調用但不會生成任何輸出。由於,咱們傳遞一個參數到WriteLine函數中。
a.il
上面的代碼有一處「閃光點」。當一個函數在IL中被調用時,除了它的返回類型以外,被傳遞的參數的數據類型,也必須被指定。咱們將Writeline設置 爲——但願獲得一個System.String類型做爲參數,可是因爲沒有字符串被傳遞到這個函數中,因此它會生成一個運行時錯誤。
所以,在調用一個函數時,在IL和其餘程序語言之間有一個明顯的區別。在IL中,當咱們調用一個函數,咱們必須指定關於該函數咱們所知道的任何內容,包括 它的返回類型和它的參數的數據類型。經過在運行期間進行恰當的檢查,保證了彙編器可以在語法上驗證代碼的有效性。
如今咱們將看到如何將參數傳遞到一個函數中。
a.il
Output
hell
彙編器指令ldstr把字符串放到棧上。Ldstr的名稱是文本"load a string on the stack"的縮寫版本。棧是一塊內存區域,它用來傳遞參數到函數中。全部的函數從棧上接收它們的參數。所以,像ldstr這樣的指令是必不可少的。
a.il
Output
hell
咱們在方法vijay上添加了一些特性。接下來咱們將逐個講解它們。
public:被稱爲可訪問特性,它決定了都有誰能夠訪問一個方法。public意味着這個方法能夠被程序的其餘任何部分所訪問。
hidebysig:類能夠從其它多個類中派生。hidebysig特性保證了父類中的函數在具備相同名稱或簽名的派生類中會被隱藏。在這個例子中,它保證了若是函數vijay出如今基類中,那麼它在派生類中就是不可見的。
static:方法能夠是靜態的或非靜態的。靜態方法屬於一個類而不屬於一個實例。所以, 就像咱們只有一個單獨的類,咱們不能擁有一個靜態函數的多份複製。靜態函數能夠在哪裏建立是沒有約束的。帶有entrypoint指令的函數必須是靜態 的。靜態函數必須具備相關聯的實體或者源代碼,而且使用類型名稱而不是實例名稱來引用它們。
il managed:因爲它的複雜性質,咱們將關於這個特性的解釋延後。當時機成熟時,它的功能將會被解釋清楚。
上面涉及的特性並無修改函數的輸出。 稍後,你將明白爲何咱們要提供這些特性的解釋。
不管什麼時候咱們用C#語言編寫一個程序,咱們首先在類的名稱前指定關鍵字class,隨後,咱們將源代碼封閉在一對花括號內。示範以下:
a.cs
讓咱們引進稱爲class的IL指令:
a.il
注意到,彙編器輸出中的改變: Class 1 Methods: 1;
Output
hell
僞指令.class以後是類的名稱。它在IL中是可選的,讓咱們經過添加一些類的特性來加強這個類的功能。
a.il
Output
hell
咱們添加了 3個特性到類的僞指令中。
以諸如C語言編寫的代碼被稱爲非託管代碼或不可信任的代碼。咱們須要一個特性來處理非託管代碼和託管代碼之間的互操做。例如,當咱們想要在託管和非託管代碼之間轉移字符串時,這個特性會被使用到。
若是咱們跨越託管代碼的邊界並鑽進非託管代碼的領域,那麼一個字符串——由2字節Unicode字符組成的數組,將會被轉換爲一個ANSI字符串——由1字節ANSI字符組成的數組;反之亦然。修飾符ansi用於消除託管和非託管代碼之間的轉換。
a.il
Output
hell
類zzz從System.Object中派生。在.NET中,爲了定義類型的一致性,全部的類型最終都派生於System.Object。所以,全部的對 象都有一個共同的基類Object。在IL中,類從其它類中派生,與C++、C#和Java的表現方式相同,
a.il
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
(b)而後咱們使用下面的命令運行C#編譯器。
>csc a.cs
所以,會建立名爲a.exe的exe文件。
(c)在可執行體中,咱們運行一個名爲ildasm的程序,它是由Microsoft提供的:
>ildasm /out=a.txt a.exe
這就建立了一個txt文件,具備下面的內容:
a.txt
當咱們閱讀上面的文件時,你將明白它的全部內容都已經在前面解釋過了。咱們開始於一個簡單的C#程序,而後將它編譯到一個可執行文件中。在正常的環境下, 它將被轉換爲機器語言或這個程序運行在所在的計算機/微處理器的彙編程序。一旦建立了可執行體,咱們就使用ildasm來反彙編它。反彙編輸出被保存到一 個新的文件a.txt中。這個文件可能被命名爲a.il,而後咱們能夠經過對其運行ilasm反過來再次建立這個可執行體。
讓咱們看一下最小的VB.NET程序。咱們將它命名爲one.vb,而它的源代碼以下所示:
one.vb
在編寫完上述的代碼後,咱們運行Visual.Net編譯器vbc以下:
>vbc one.vb
這就產生了文件one.exe。
下面,咱們執行ildasm以下所示:
>ildasm /out=a.txt one.exe
這就生成了下面的文件a.txt:
a.txt
你將驚訝地看到由兩個不一樣的編譯器所生成的輸出幾乎是相同的。我向你展現了這個示例用以證明——語言的無關性,最終,源代碼將會被轉換爲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以後是這些變量的數據類型和它的名稱。