CIL編程的本質
CIL是英文Common Intermediate Language的簡寫。CIL本質上其實就是.NET平台的母語。當開發人員選擇一種托管的編程語言(C#、VB、COBOL.NET等)構建.NET程序集時,同這個語言相關的編譯器就會把源代碼編譯成CIL。正如其他任何一種編程語言一樣,CIL提供了非常多的結構和實現標記。如果考慮到CIL其實也是一種.NET的編程語言,那麼通過直接使用.NET Framework 3.5 SDK提供的CIL和CIL編譯器(ilasm.exe)來開發和構建.NET程序集也就不會讓人吃驚了。
事實上很少有人會選擇完全使用CIL來構建一個.NET應用程序,不過CIL本身還是非常有趣的。簡單的說,對CIL語法理解的越多,那麼就越有可能進入.NET的高級領域。具體一點兒說,理解並掌握CIL的人應該能達到以下幾點。
n 清楚地理解不同的.NET編程語言是如何映射他們各自的關鍵字到CIL標記的。
n 反匯編一個已經存在的.NET程序集,直接編輯CIL代碼,最後重新編譯更改後的代碼到.NET二進制文件。
n 使用System.Reflection.Emit命名空間構建動態程序集。
n 理解那些存在於CIL層中,但是不被高級托管語言所支持的CTS的特性。要知道,CIL是唯一一種允許你訪問和使用所有CTS特性的.NET語言。例如,使用純粹的CIL,就可以定義全局的成員和字段,而這在C#中是不允許的。
最後,再次強調一下,即使你不想在CIL細節上糾纏,也完全有能力掌握.NET的基類庫。從很多方面來看,CIL對於.NET開發人員就好像匯編語言對於一個C++程序員一樣。那些精通底層細節的開發人員可以具備應用高級技術手段來解決手上問題的能力,同時對於編程(運行)環境底層也會有一個深入的理解。如果你願意接受挑戰,那麼就讓我們一同來揭開CIL的神秘面紗吧。
作為這個系列的開篇我們簡要介紹了一下CIL的本質,和學習CIL的價值所在。既然有必要學習CIL,下面就讓我們正式進入CIL的世界吧。
CIL指令、特性和操作碼
當研究CIL這樣的底層開發語言時,一定會發現一些非常熟悉的概念被套用上了新的名字。例如,如果這裡列出如下這些關鍵字:
{new, public, this, base, get, set, explicit, unsafe, enum, operator, partial}
你肯定知道它們是C#的關鍵字。然而,如果再仔細看看這些集合中的成員,也許會發現盡管它們是C#中的關鍵字,但是他們有著本質上的語義區別。比如,enum這個關鍵字定義了一個從System.Enum派生的類型,this 和base關鍵字允許分別引用當前的對象或者當前對象的父類。unsafe關鍵字被用來確認一個不可以直接被CLR監控的代碼段,operator則允許構建一個可以通過特定的C#運算符(例如加號)來訪問的隱藏的方法。
同C#這樣的高級語言完全不同的是,CIL不僅僅定義了一組通用的關鍵字,而且根據語義上的內涵不同,這些被CIL編譯器識別的標記進一步被劃分到3個類別中。
n CIL指令(directive)。
n CIL特性(attribute)。
n CIL操作碼(opcode)。
每一個類別的CIL標記都通過一個特別的語法來表示,這些標記組織到一起來就可以構建出一個有效的.NET程序集。
1.CIL指令的作用
在CIL的標記集中有這樣一組用於描述.NET程序集總體結構的標記,它們被稱作CIL指令。CIL指令用於通知CIL編譯器如何定義在程序集中用到的命名空間、類型和成員。
CIL指令語法上使用一個(.)的前綴來表示,例如.namespace、.class、.publickeytoken、.method、.assembly等。因此,如果你的*.il文件*(CIL代碼文件的拓展名)有1個.namespace指令和3個.class指令,那麼CIL編譯器將在生成的程序集裡產生一個包含有3個.NET類類型的命名空間。
2.CIL特性的作用
在很多情況下,僅僅CIL指令不足以來表示給出的.NET類型或者類型成員。基於這個原因,很多CIL指令可以同CIL特性結合起來使用,CIL特性可以限定應該如何處理一個CIL指令。例如,一個.class指令可以同pblic特性(建立這個類型的可見性)、extends特性(明確指定一個類型的基類)和implements特性(列出這個類型支持的一系列接口)相結合。
3.CIL操作碼的作用
一個.NET程序集、命名空間和類型通過CIL的指令和相關的特性來定義後,最後的任務就是提供類型的實現邏輯。這個就是CIL操作碼的職責范圍。沿用底層開發語言的習慣,CIL操作碼往往是無法念出來的。例如,如果你需要定義一個字符串變量,你使用的不是一個容易理解的操作名字LoadString,而是ldstr。
老實說,有些CIL操作碼是可以直接和C#中的運算符對應起來的,例如box、unbox、throw和sizeof。大家將看到,CIL操作碼總是在成員實現的作用域內使用,同CIL指令不同,它們並不需要點前。
4.區別CIL操作碼和CIL助記符
正如剛剛說明的,像ldstr這樣的操作碼被用來實現一個給定的類型的成員。不過在現實中,像ldstr這樣的標記其實是用來表示真正二進制操作碼的CIL助記符。為了說明區別,我們來看一段C#的代碼:
static int Add(int x, int y)
{
return x + y;
}
用來表示兩個數相加的CIL操作碼是0X58,用來表示兩個數相減的操作碼是0X59,在托管堆上分配一個新對象的操作碼是0X73。事實上,由JIT編譯器來處理的CIL代碼無外乎是一堆二進制數據。
幸好,每一個二進制的CIL操作碼都對應有一個助記符。例如,可以使用add而不是二進制的0X58,sub而不是0X59,newobj而不是0X73。明白了兩者的區別,就可以清楚像ildasm.exe這樣的CIL反編譯器就是將二進制的操作碼翻譯成它們所對應的助記符。例如,以下就是ildasm.exe為之前的Add()提供的CIL:
提醒初學者:ildsm.exe可以在:開始=》所有程序=》Microsoft Windows SDK v6.0A文件夾下找到,下圖中的閃電圖標:
除非需要構造非常底層的.NET軟件(比如定制的托管編譯器),否則不需要關心這些CIL的數字操作碼。實際上,當.NET開發人員在說“CIL操作碼”時,通常是指的那些助記符而不是底層的那些二進制數值。
好,今天就到這裡了。下一篇我們將談到:“入棧和出棧:CIL基於棧的本質”