研究MSIL純屬於個人喜好,說在前面MSIL應用於開發的地方很少,但是很大程度上能夠幫着我們理解底層的原理,這是我了解MSIL的主要原因。托管代碼表示應用程序的方法的功能,它們以微軟的中間語言(Microsoft intermediate language,MSIL)或公共語言運行(common intermediate language,CIL)的抽象二進制形式進行編碼。
MSIL代碼由CLR“托管”。CLR托管至少包括三個主要的活動:類型控制,結構化異常和垃圾收集。類型控制設計在執行期間項類型的驗證和轉換。托管異常處理在功能上與“非托管的”結構化異常處理類似,但它是由CLR執行的而不是有操作系統執行的。垃圾收集涉及對不再使用的對象進行自動標識和釋放。
在CLR環境下,.NET應用程序由一個或多個托管可執行體組成,其中每一個都攜帶着源數據和托管代碼。托管的可執行體成為模塊,主要包括兩個組件:元數據和MSIL代碼。CLR處理這兩個組件的主要子系統是加載程序(Loader)和JIT(Just-in-time,即時)編譯器。
接下來我們主要講的是托管可執行體里面的MSIL(中間語言),再講MSIL時,先把微軟的整個框架體系簡單概括下:(見下圖一)
簡要概述下整個過程,首先是我們編寫的C#源文件hello.cs通過C#編譯器進行編譯,編譯成.NET 的PE文件結構,也就是exe文件格式,當程序運行時,Windows的Loader加載器不會負責該程序的內存分配,線程管理等工作,而是只負責跳轉到CLR的執行引擎(EE)中,將控制權交由CLR,由CLR進行分配內存,線程管理,異常處理等。
通過查看.NET 的PE文件導入表中只有一個API,exe對應mscoree.dll的_CorExeMain;而dll對應mscoree.dll的_CorDllMain。這就說明windows的loader加載器載入.NET PE后,只負責跳轉到相應的DLL,隨后改程序邊運行在EE的監管中。查看導入表如下圖所示:
我覺得學習每一項知識時,一些技巧性的東西是靠一步一步去積累的,但是我認為底層的探索也是學習當中必不可缺的一部分。有些時候底層的知識可以呈現出原理性的東西,越接近底層的知識就越靠近實現部分,好了廢話也不多說接下來我們就來探討MSIL中間語言,MSIL中間語言是基於堆棧的面向對象語言。
借助一個簡單的例子進行分析MSIL中間語言,每個編程技術人員都是從Hello World開始那我們也從Hello World開始。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 string hello = "Hello World"; 6 Console.WriteLine(hello); 7 Console.Read(); 8 } 9 }
輸出結果很明顯是:Hello World,如下圖所示:
接下來我們將要分析該程序的MSIL代碼,通過ILDASM.EXE工具將exe文件進行反編譯成MSIL中間語言。如下圖所示:
簡要概述:
關鍵字:.method表示方法的意思,.method private hidebysig static void Main(string[] args) cil managed表示的意思就是static void main(string[] args)
.entrypoint標志方法的入口
.maxstack表示分配堆棧大小
.locals init中存放的是當前方法的局部變量,這里面是string類型,它的名稱叫hello。Init指令表示對變量應以對應的類型默認值進行初始化,通常情況下變量名可以省略,在代碼中將以零基索引來引用
例如:stloc.0表示將Envaluation Stack中的一個棧頂數值保存到局部變量0(Call Stack)中。
先介紹幾個關於MSIL內部知識點:
①.Managed Heap:這是動態配置(Dynamic Allocation)的記憶體,由 Garbage Collector(GC)在執行時自動管理,整個 Process 共用一個 Managed Heap,可以理解為引用類型的東西都放在這個Managed Heap中。
②.Call Stack:這是由 .NET CLR 在執行時自動管理的記憶體,每個Thread都有自己的Call Stack堆棧。每調用一次method,就會使得Call Stack上多了一個Record Frame;調用完畢之后,此Record Frame會被丟棄。一般來說,Record Frame內記錄着method參數(Parameter)、返回位址(Return Address)、以及局部變量(Local Variable)。.NET CLR都是使用0, 1, 2…編號的方式來識別局部變量。
③.Evaluation Stack:這是由.NET CLR在執行時自動管理的記憶體,每個Thread都有自己專屬的Evaluation Stack。壓入的到Evaluation Stack的值,當方法調用結束時必須保持這個堆棧的平衡,這里面存放例如局部變量值,以及引用類型的地址。
指令ldc是將參數存儲至堆棧Evaluation Stack
指令stloc是將變量存儲至堆棧Call Stack
技巧:ld開頭就是加載數據到Evaluation Stack中,而st開頭就是將Envaluation Stack中的數據保存到Call Stack,Call Stack存放局部變量值。
接下來我們將演示代碼的堆棧情況。
首先進入的是IL_0000段的代碼為nop,這段代碼表明了沒有任何操作。
接下來就要到了IL_0001段代碼為ldstr “hello World”,ldstr加載字符串是將字符串的引用放在了Envaluation Stack中,而真正的字符串放在了Managed Heap中,詳情請見下圖:
接下來就要運行到了stloc.0這條指令的意思就是講參數保存到局部變量中。
將Envaluation Stack中的值保存到 Call Stack中,因為Envaluation Stack中存放的是“hello World”字符串的地址,所以V0存放的也是字符串的地址。
接下來要運行到了ldloc.0加載到Envaluation Stack中局部變量0的地址。
接下來運行MSIL的call語句,從 Evaluation Stack 中取出一個值,此值為 Reference Type,調用 mscorlib.dll 所提供的 System.Console::WriteLine(string),注意這里用的call,因為這個是靜態方法(static method),而不能用CallVirt方法。結構圖如下所示:
接下來就要調用靜態方法System.Console::Read()等待用戶輸入之后,將輸入值放入到Envaluation中去,最后再用pop指令將數值從Envaluation Stack中彈出來,最后就到達了ret這個地方,此指令的意思是:結束此次調用(也就是 Main 的調用)。此時會檢查 Evaluation Stack 內剩下的資料,由於 Main() 告知不需要傳出值(void),所以 Evaluation Stack 內必須是空的,本范例符合這樣的情況,所以此時可以順利結束此次調用。而 Main 的調用一結束,程序也隨之結束。
通過這篇文章可以清晰的了解MSIL中間語言的運行機制,是基於堆棧的形式操作。再次聲明學習MSIL只是由於個人興趣,希望各位能夠提出寶貴的意見以及上述有錯的地方能夠指正,小丁虛心求教。