一 前言
寫了兩篇關於IL指令相關的文章,分別把值類型與引用類型在 堆與棧上的操作區別詳細的寫了一遍
這第三篇也是最后一篇,之所以到第三篇就結束了,是因為以我現在的層次,能理解到的都寫完了,而且個人認為,重要的地方都差不多
寫到了,
最后一篇決定把之前的內容全部整合起做一個綜合的例子,然后簡單的解釋下IL指令的含義,及在內存中的變化
如果你沒有看前兩篇請狂點這里
IL指令大全 :IL指令詳解
IL反編譯工具: ILDasm
注:因本人水平有限,難免有理解錯誤之處,如有發現,望及時指出,我會立馬更正。
二 IL指令詳解 (基本介紹)
這次把 類 委托 方法 字段都集合起來,這樣的環境就與實際的項目比較接近了,也算接地氣了
先看C#代碼
1 public delegate void MyDele(string name); 2 class Program 3 { 4 static void Main(string[] args) 5 { 6 7 UserInfo userInfo = new UserInfo(); 8 9 PeopleStruct peopleStruct = new PeopleStruct(); 10 11 //定義委托 12 MyDele myDele = userInfo.PrintName; 13 //調用委托 14 myDele("Delegate"); 15 16 userInfo.PrintName("PrintName"); 17 userInfo.PrintField(); 18 //靜態方法 19 UserInfo.ContactStr("UserInfo", "ContactStr"); 20 //結構的方法 21 peopleStruct.PrintInfo("Color is Yellow"); 22 23 //靜態類中的靜態方法 24 StaticUserInfo.PrintName("Static Class Static Method"); 25 26 Console.Read(); 27 } 28 } 29 30 internal class UserInfo 31 { 32 public string Name = "UserInfo Field"; 33 34 public void PrintName(string name) 35 { 36 Console.WriteLine(name); 37 } 38 39 public void PrintField() 40 { 41 Console.WriteLine(Name); 42 } 43 44 public static void ContactStr(string Str, string Str2) 45 { 46 Console.WriteLine(Str + Str2); 47 } 48 49 } 50 51 struct PeopleStruct 52 { 53 54 public void PrintInfo(string color) 55 { 56 Console.WriteLine(color); 57 } 58 59 } 60 61 static class StaticUserInfo 62 { 63 public static void PrintName(string name) 64 { 65 Console.WriteLine(name); 66 } 67 }
IL 代碼
call可以調用靜態方法,實例方法和虛方法
callvirt只能調用實例方法和虛方法,不能調用靜態方法
1 .method private hidebysig static void Main(string[] args) cil managed 2 { 3 .entrypoint 4 // Code size 106 (0x6a) 5 .maxstack 2 6 .locals init (class ILDeom3.UserInfo V_0, //只定義變量並不做任何初始化操作 7 valuetype ILDeom3.PeopleStruct V_1, 8 class ILDeom3.MyDele V_2) 9 IL_0000: nop 10 //創建一個值類型的新對象或新實例,並將對象引用推送到計算堆棧上 11 IL_0001: newobj instance void ILDeom3.UserInfo::.ctor() 12 //把棧中頂部的元素彈出(UserInfo 的實例)並賦值給局部變量表中第0個位置的元素(V_0) 13 IL_0006: stloc.0 14 //將位於特定索引處的局部變量的 "地址" 加載到計算堆棧上(將指向結構的地址壓入棧中) 15 IL_0007: ldloca.s V_1 16 //初始化結構中的屬性 17 IL_0009: initobj ILDeom3.PeopleStruct 18 //將局部變量列表中第0個位置(V_0 UerInfo的實例地址)的值壓入棧中 19 IL_000f: ldloc.0 20 //將指向實現特定方法的本機代碼的非托管指針(native int 類型)推送到計算堆棧上。 21 //也就是指的將方法指針壓入棧中 22 IL_0010: ldftn instance void ILDeom3.UserInfo::PrintName(string) 23 //創建委托的實例並壓入棧中 24 //這一步會調用委托的構造器,這個構造器需要兩個參數,一個對象引用,就是IL_000f: ldloc.0壓入的UserInfo的實例,一個方法的地址。 25 IL_0016: newobj instance void ILDeom3.MyDele::.ctor(object,native int) 26 //彈出棧中值(委托的實例)保存到局部變量表第2個位置(V_2) 27 IL_001b: stloc.2 28 //獲取局部變量列表中第2個位置上的值上一步保存的值(委托實例),並壓入棧中 29 IL_001c: ldloc.2 30 //加載字符串 31 IL_001d: ldstr "Delegate" 32 //調用綁定給委托的PrintName方法 33 IL_0022: callvirt instance void ILDeom3.MyDele::Invoke(string) 34 IL_0027: nop 35 //獲取局部變量列表中第0個位置上的值(UserInfo的實例) 36 IL_0028: ldloc.0 37 IL_0029: ldstr "PrintName" 38 //調用PrintName方法 39 IL_002e: callvirt instance void ILDeom3.UserInfo::PrintName(string) 40 IL_0033: nop 41 //獲取局部變量列表中第0個位置上的值(UserInfo的實例) 42 IL_0034: ldloc.0 43 //調用PrintField方法 44 IL_0035: callvirt instance void ILDeom3.UserInfo::PrintField() 45 IL_003a: nop 46 IL_003b: ldstr "UserInfo" 47 IL_0040: ldstr "ContactStr" 48 //因為ContactStr是靜態方法所以不需要先加載實例可以直接調用 49 IL_0045: call void ILDeom3.UserInfo::ContactStr(string, 50 string) 51 IL_004a: nop 52 //將位於特定索引處的局部變量的 "地址" 加載到計算堆棧上 (將指向結構的地址壓入棧中) 53 IL_004b: ldloca.s V_1 54 IL_004d: ldstr "Color is Yellow" 55 //調用結構中的PrintInfo方法 56 IL_0052: call instance void ILDeom3.PeopleStruct::PrintInfo(string) 57 IL_0057: nop 58 IL_0058: ldstr "Static Class Static Method" 59 IL_005d: call void ILDeom3.StaticUserInfo::PrintName(string) 60 IL_0062: nop 61 IL_0063: call int32 [mscorlib]System.Console::Read() 62 IL_0068: pop 63 IL_0069: ret 64 } // end of method Program::Main
相信有注釋,大家應該都是能夠看懂的,IL其實並不難,也並不算底層,只是把C#編譯成了中間語言,並非機器語言,CPU照樣還是讀不懂,
三 IL指令詳解 (深入了解)
因這次IL指令,有點長,要畫圖確實有點扛不住,所以只畫重要的地方,還望見諒.
另外 跟園子里的 @冰麟輕武 探討了跟IL相關的三個內存塊 Managed Heap ,Evaluation Stack,Call Stack 了解到了很多之前不明白的知識點,
也糾正了自己以前的一些誤區,最后一致認可我們自己的討論結果,討論結果如下,
1 Managed Heap(托管堆) 程序運行時會動態的在其中開辟空間來存儲變量的值,如new class 時,回收由GC 根據 代齡,和可達對象,來回收相應的內存資源。整個程序共用一個ManagedHeap
2 Evaluation Stack(計算棧):每個線程都有一個獨立的 評估棧,用於程序相關的運算,
3 Call Stack(調用棧):討論的重點就在這里,之前認為Call Stack並不是一個棧,而是一個局部變量列表,用於存放方法的參數,可是我一直有疑問就是值類型應該是存在棧中的,如果Call Stack是個棧,那取值時Call Stack並沒有按FILO的原則來,那如果 Call Stack不是個棧那值類型的值 是存在哪里的,然后我與@冰麟輕武就這一問題,討論起來了
先看官方對Call Stack的解釋: 這是由.NET CLR在執行時自動管理的存儲單元,每個Thread都有自己專門的Call Stack。每呼叫一次method,就會使得Call Stack上多一個Record Frame;方法執行完畢之后,此Record Frame會被丟棄。重點就在紅色這一句中的 Record Frame又是個什么東西他里邊有什么東西?然后開始各種假設,最終我們認為這一種理論是比較靠譜一點的如下:
Call Stack本身就是一個棧,每調用一個方法時就會在棧頂部加載一個Record Frame,這個Record Frame里包含了方法所需要的參數(Params),返回地址(Return Address)和區域變量(Local Variable),當調用的方法結束時,就自動會把這個Record Frame從棧頂彈出。如此一來,我之前的疑問就可以得到相應的解釋了
值類型是存在棧中的,當調用方法里會把方法需要的值重棧中取出,然后在棧中創建一個Record Frame並把賦值給Record Frame中的參數,在這個Record Frame中取數據並不是按FILO原則來的,而可以按索引,也可以按地址 對應IL指令 Ldloc stLoc 等取值與賦值都是針對的Record Frame 。而且我們認為Call Stack是對線程棧的一個統稱。
上圖
下面圖解一下實例化一個類,並調用類中的方法在內存中是如何變化的
.locals init (class ILDeom3.UserInfo V_0,valuetype ILDeom3.PeopleStruct V_1,class ILDeom3.MyDele V_2)
IL_0001: newobj instance void ILDeom3.UserInfo::.ctor()
IL_0006: stloc.0
IL_0028: ldloc.0
IL_0029: ldstr "PrintName"
IL_002e: callvirt instance void ILDeom3.UserInfo::PrintName(string)
四 總結
IL系列終於寫完了,也算給自己一個交代了,寫文章真的很花時間,就以我這三篇為例,光只是寫和畫圖都有花十幾個小時,而且如果是晚上寫一般都會超過12點才能完成,更不用說前期的自己學習所用的時間,
但是我覺得真的很值得,充分的把自己的業余時間利用起來了,對於IL也有了一個相對深入的了解,
在此要感謝 園子里朋友的支持,也感謝 @冰麟輕武對我的指點,更要感謝dudu能建立博客園這么好的一個環境。
如果您覺得本文有給您帶來一點收獲,不妨點個推薦,為我的付出支持一下,謝謝~
如果希望在技術的道路上能有更多的朋友,那就關注下我吧,讓我們一起在技術的路上奔跑