先說說學IL有什么用,有人可能覺得這玩意平常寫代碼又用不上,學了有個卵用。到底有沒有卵用呢,暫且也不說什么學了可以看看一些語法糖的實現,或對.net理解更深一點這些虛頭巴腦的東西。最重要的理由就是一個:當面試官看你簡歷上寫着精通C#時,問你一句:
"懂不懂IL?"
怎么回答?
"不好意思,那東西沒什么卵用,所以我沒學。"
還是
"還行,可以探討一下。"
你覺得哪個回答好呢,答得好才更有底氣要到更多的薪資,多個幾千塊也說不定,而這只不過花上不到半小時學習就可以跟面試官吹上一陣了,很實用,有沒有。
為什么取這個標題呢,記得很久之前看過一篇文章,叫"正則表達式30分鍾入門教程",學正則最重要的就是記住各個符號的含義。個人覺得相比難以直接看出實際意義的正則符號如"\w","\d","*","?","{}[]"等,IL的指令要容易得多。很多人見到IL一大堆的指令,和匯編一樣,就感覺頭大不想學了。其實IL本身邏輯很清楚,主要是把指令的意思搞明白就好辦了。記指令只要記住幾個規律就好,我把它們分為三類。
第一類 :直觀型
這一類的特點是一看名字就知道是干嘛的,不需要多講,如下:
名稱 |
說明 |
Add |
將兩個值相加並將結果推送到計算堆棧上。 |
Sub |
從其他值中減去一個值並將結果推送到計算堆棧上。 |
Div |
將兩個值相除並將結果作為浮點(F 類型)或商(int32 類型)推送到計算堆棧上。 |
Mul |
將兩個值相乘並將結果推送到計算堆棧上。 |
Rem |
將兩個值相除並將余數推送到計算堆棧上。 |
Xor |
計算位於計算堆棧頂部的兩個值的按位異或,並且將結果推送到計算堆棧上。 |
And |
計算兩個值的按位"與"並將結果推送到計算堆棧上。 |
Or |
計算位於堆棧頂部的兩個整數值的按位求補並將結果推送到計算堆棧上。 |
Not |
計算堆棧頂部整數值的按位求補並將結果作為相同的類型推送到計算堆棧上。 |
Dup |
復制計算堆棧上當前最頂端的值,然后將副本推送到計算堆棧上。 |
Neg |
對一個值執行求反並將結果推送到計算堆棧上。 |
Ret |
從當前方法返回,並將返回值(如果存在)從調用方的計算堆棧推送到被調用方的計算堆棧上。 |
Jmp |
退出當前方法並跳至指定方法。 |
Newobj |
New Object創建一個值類型的新對象或新實例,並將對象引用推送到計算堆棧上。 |
Newarr |
New Array將對新的從零開始的一維數組(其元素屬於特定類型)的對象引用推送到計算堆棧上。 |
Nop |
如果修補操作碼,則填充空間。盡管可能消耗處理周期,但未執行任何有意義的操作。Debug下的 |
Pop |
移除當前位於計算堆棧頂部的值。 |
Initobj |
Init Object將位於指定地址的值類型的每個字段初始化為空引用或適當的基元類型的 0。 |
Isinst |
Is Instance測試對象引用是否為特定類的實例。 |
Sizeof |
將提供的值類型的大小(以字節為單位)推送到計算堆棧上。 |
Box |
將值類轉換為對象引用。 |
Unbox |
將值類型的已裝箱的表示形式轉換為其未裝箱的形式。 |
Castclass |
嘗試將引用傳遞的對象轉換為指定的類。 |
Switch |
實現跳轉表。 |
Throw |
引發當前位於計算堆棧上的異常對象。 |
Call |
調用由傳遞的方法說明符指示的方法。 |
Calli |
通過調用約定描述的參數調用在計算堆棧上指示的方法(作為指向入口點的指針)。 |
Callvirt |
對對象調用后期綁定方法,並且將返回值推送到計算堆棧上。 |
強調一下,有三種call,用的場景不太一樣:
Call:常用於調用編譯時就確定的方法,可以直接去元數據里找方法,如靜態函數,實例方法,也可以call虛方法,不過只是call這個類型本身的虛方法,和實例的方法性質一樣。另外,call不做null檢測。
Calli: MSDN上講是間接調用指針指向的函數,具體場景沒見過,有知道的朋友望不吝賜教。
Callvirt: 可以調用實例方法和虛方法,調用虛方法時以多態方式調用,不能調用靜態方法。Callvirt調用時會做null檢測,如果實例是null,會拋出NullReferenceException,所以速度上比call慢點。
第二類:加載(ld)和存儲(st)
我們知道,C#程序運行時會有線程棧把參數,局部變量放上來,另外還有個計算棧用來做函數里的計算。所以把值加載到計算棧上,算完后再把計算棧上的值存到線程棧上去,這類指令專門干這些活。
比方說 ldloc.0:
這個可以拆開來看,Ld打頭可以理解為Load,也就是加載;loc可以理解為local variable,也就是局部變量,后面的 .0表示索引。連起來的意思就是把索引為0的局部變量加載到計算棧上。對應的 ldloc.1就是把索引為1的局部變量加載到計算棧上,以此類推。
知道了Ld的意思,下面這些指令 也就很容易理解了。
ldstr = load string,
ldnull = load null,
ldobj = load object,
ldfld = load field,
ldflda = load field address,
ldsfld = load static field,
ldsflda = load static field address,
ldelem = load element in array,
ldarg = load argument,
ldc 則表示加載數值,如ldc.i4.0,
關於后綴
.i[n]:[n]表示字節數,1個字節是8位,所以是8*n的int,比如i1, i2, i4, i8,i1就是int8(byte), i2是int16(short),i4是int32(int),i8是int64(long)。
相似的還有.u1 .u2 .u4 .u8 分別表示unsigned int8(byte), unsigned int16(short), unsigned int32(int), unsigned int64(long);
.R4,.R8 表示的是float和double。
.ovf (overflow)則表示會進行溢出檢查,溢出時會拋出異常;
.un (unsigned)表示無符號數;
.ref (reference)表示引用;
.s (short)表示短格式,比如說正常的是用int32,加了.s的話就是用int8;
.[n] 比如 .1,.2 等,如果跟在i[n]后面則表示數值,其他都表示索引。如 ldc.i4.1就是加載數值1到計算棧上,再如ldarg.0就是加載第一個參數到計算棧上。
ldarg要特別注意一個問題:如果是實例方法的話ldarg.0加載的是本身,也就是this,ldarg.1加載的才是函數的第一個參數;如果是靜態函數,ldarg.0就是第一個參數。
與ld對應的就是st,可以理解為store,意思是把值從計算棧上存到變量中去,ld相關的指令很多都有st對應的,比如stloc, starg, stelem等,就不多說了。
第三類:比較指令,比較大小或判斷bool值
有一部分是比較之后跳轉的,代碼里的 if 就會產生這些指令,符合條件則跳轉執行另一些代碼:
以b開頭:beq, bge, bgt, ble, blt, bne
先把b去掉看看:
eq: equivalent with, ==
ge: greater than or equivalent with , >=
gt: greater than , >
le: less than or equivalent with, <=
lt: less than, <
ne: not equivalent with, !=
這樣是不是很好理解了,beq IL_0005就是計算棧上兩個值相等的話就跳轉到IL_0005, ble IL_0023是第一個值小於或等於第二個值就跳轉到IL_0023。
以br(break)開頭:br, brfalse, brtrue,
br是無條件跳轉;
brfalse表示計算棧上的值為 false/null/0 時發生跳轉;
brtrue表示計算棧上的值為 true/非空/非0 時發生跳轉
還有一部分是c開頭,算bool值的,和前面b開頭的有點像:
ceq 比較兩個值,相等則將 1 (true) 推到棧上,否則就把 0 (false)推到棧上
cgt 比較兩個值,第一個大於第二個則將 1 (true) 推到棧上,否則就把 0 (false)推到棧上
clt 比較兩個值,第一個小於第二個則將 1 (true) 推到棧上,否則就把 0 (false)推到棧上
以上就是三類常用的,把這些搞明白了,IL指令也就理解得七七八八了。就像看文章一樣,認識大部分字后基本就不影響閱讀了,不認識的猜下再查下,下次再看到也就認得了。
例子
下面看個例子,隨手寫段簡單的代碼,是否合乎邏輯暫不考慮,主要是看IL:
源代碼:
1 using System; 2 3 namespace ILLearn 4 { 5 class Program 6 { 7 const int WEIGHT = 60; 8 9 static void Main(string[] args) 10 { 11 var height = 170; 12 13 People people = new Developer("brook"); 14 15 var vocation = people.GetVocation(); 16 17 var healthStatus = People.IsHealthyWeight(height, WEIGHT) ? "healthy" : "not healthy"; 18 19 Console.WriteLine($"{vocation} is {healthStatus}"); 20 21 Console.ReadLine(); 22 } 23 } 24 25 abstract class People 26 { 27 public string Name { get; set; } 28 29 public abstract string GetVocation(); 30 31 public static bool IsHealthyWeight(int height, int weight) 32 { 33 var healthyWeight = (height - 80) * 0.7; 34 return weight <= healthyWeight * 1.1 && weight >= healthyWeight * 0.9; //標准體重是 (身高-80) * 0.7,區間在10%內都是正常范圍 35 } 36 } 37 38 class Developer : People 39 { 40 public Developer(string name) 41 { 42 Name = name; 43 } 44 45 public override string GetVocation() 46 { 47 return "Developer"; 48 } 49 } 50 }
在命令行里輸入:csc /debug- /optimize+ /out:program.exe Program.cs
打開IL查看工具:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\ildasm.exe,不同版本可能目錄不太一樣。打開剛編譯的program.exe文件,如下:
雙擊節點就可以查看IL,如:
Developer的構造函數:
1 .method public hidebysig specialname rtspecialname 2 instance void .ctor(string name) cil managed 3 { 4 // 代碼大小 14 (0xe) 5 .maxstack 8 6 IL_0000: ldarg.0 //加載第1個參數,因為是實例,而實例的第1個參數始終是this 7 IL_0001: call instance void ILLearn.People::.ctor() //調用基類People的構造函數,而People也會調用Object的構造函數 8 IL_0006: ldarg.0 //加載this 9 IL_0007: ldarg.1 //加載第二個參數也就是name 10 IL_0008: call instance void ILLearn.People::set_Name(string) //調用this的 set_Name, set_Name這個函數是編譯時為屬性生成的 11 IL_000d: ret //return 12 } // end of method Developer::.ctor
Developer的GetVocation:
1 .method public hidebysig virtual instance string //虛函數 2 GetVocation() cil managed 3 { 4 // 代碼大小 6 (0x6) 5 .maxstack 8 //最大計算棧,默認是8 6 IL_0000: ldstr "Developer" //加載string "Developer" 7 IL_0005: ret //return 8 } // end of method Developer::GetVocation
People的IsHealthyWeight:
1 .method public hidebysig static bool IsHealthyWeight(int32 height, //靜態函數 2 int32 weight) cil managed 3 { 4 // 代碼大小 52 (0x34) 5 .maxstack 3 //最大計算棧大小 6 .locals init ([0] float64 healthyWeight) //局部變量 7 IL_0000: ldarg.0 //加載第1個參數,因為是靜態函數,所以第1個參數就是height 8 IL_0001: ldc.i4.s 80 //ldc 加載數值, 加載80 9 IL_0003: sub //做減法,也就是 height-80,把結果放到計算棧上,前面兩個已經移除了 10 IL_0004: conv.r8 //轉換成double,因為下面計算用到了double,所以要先轉換 11 IL_0005: ldc.r8 0.69999999999999996 //加載double數值 0.7, 為什么是0.69999999999999996呢, 二進制存不了0.7,只能找個最相近的數 12 IL_000e: mul //計算棧上的兩個相乘,也就是(height - 80) * 0.7 13 IL_000f: stloc.0 //存到索引為0的局部變量(healthyWeight) 14 IL_0010: ldarg.1 //加載第1個參數 weight 15 IL_0011: conv.r8 //轉換成double 16 IL_0012: ldloc.0 //加載索引為0的局部變量(healthyWeight) 17 IL_0013: ldc.r8 1.1000000000000001 //加載double數值 1.1, 看IL_0010到IL_0013,加載了3次,這個函數最多也是加載3次,所以maxstack為3 18 IL_001c: mul //計算棧上的兩個相乘,也就是 healthyWeight * 1.1, 這時計算棧上還有兩個,第一個是weight,第二個就是這個計算結果 19 IL_001d: bgt.un.s IL_0032 //比較這兩個值,第一個大於第二個就跳轉到 IL_0032,因為第一個大於第二個表示第一個條件weight <= healthyWeight * 1.1就是false,也操作符是&&,后面沒必要再算,直接return 0 20 IL_001f: ldarg.1 //加載第1個參數 weight 21 IL_0020: conv.r8 //轉換成double 22 IL_0021: ldloc.0 //加載索引為0的局部變量(healthyWeight) 23 IL_0022: ldc.r8 0.90000000000000002 //加載double數值 0.9 24 IL_002b: mul //計算棧上的兩個相乘,也就是 healthyWeight * 0.9, 這時計算棧上還有兩個,第一個是weight,第二個就是這個計算結果 25 IL_002c: clt.un //比較大小,第一個小於第二個則把1放上去,否則放0上去 26 IL_002e: ldc.i4.0 //加載數值0 27 IL_002f: ceq //比較大小,相等則把1放上去,否則放0上去 28 IL_0031: ret //return 棧頂的數,為什么沒用blt.un.s,因為IL_0033返回的是false 29 IL_0032: ldc.i4.0 //加載數值0 30 IL_0033: ret //return 棧頂的數 31 } // end of method People::IsHealthyWeight
主函數Main:
1 .method private hidebysig static void Main(string[] args) cil managed 2 { 3 .entrypoint //這是入口 4 // 代碼大小 67 (0x43) 5 .maxstack 3 //大小為3的計算棧 6 .locals init (string V_0, 7 string V_1) //兩個string類型的局部變量,本來還有個people的局部變量,被release方式優化掉了,因為只是調用了people的GetVocation,后面沒用,所以可以不存 8 IL_0000: ldc.i4 0xaa //加載int型170 9 IL_0005: ldstr "brook" //加載string "brook" 10 IL_000a: newobj instance void ILLearn.Developer::.ctor(string) //new一個Developer並把棧上的brook給構造函數 11 IL_000f: callvirt instance string ILLearn.People::GetVocation() //調用GetVocation 12 IL_0014: stloc.0 //把上面計算的結果存到第1個局部變量中,也就是V_0 13 IL_0015: ldc.i4.s 60 //加載int型60 14 IL_0017: call bool ILLearn.People::IsHealthyWeight(int32, //調用IsHealthyWeight,因為是靜態函數,所以用call 15 int32) 16 IL_001c: brtrue.s IL_0025 //如果上面返回true的話就跳轉到IL_0025 17 IL_001e: ldstr "not healthy" //加載string "not healthy" 18 IL_0023: br.s IL_002a //跳轉到IL_002a 19 IL_0025: ldstr "healthy" //加載string "healthy" 20 IL_002a: stloc.1 //把結果存到第2個局部變量中,也就是V_1, IL_0017到IL_002a這幾個指令加在一起用來計算三元表達式 21 IL_002b: ldstr "{0} is {1}" //加載string "{0} is {1}" 22 IL_0030: ldloc.0 //加載第1個局部變量 23 IL_0031: ldloc.1 //加載第2個局部變量 24 IL_0032: call string [mscorlib]System.String::Format(string, //調用string.Format,這里也可以看到C# 6.0的語法糖 $"{vocation} is {healthStatus}",編譯后的結果和以前的用法一樣 25 object, 26 object) 27 IL_0037: call void [mscorlib]System.Console::WriteLine(string) //調用WriteLine 28 IL_003c: call string [mscorlib]System.Console::ReadLine() //調用ReadLine 29 IL_0041: pop 30 IL_0042: ret 31 } // end of method Program::Main
很簡單吧,當然,這個例子也很簡單,沒有事件,沒有委托,也沒有async/await之類,這些有興趣的可以寫代碼跟一下,這幾種都會在編譯時插入也許你不知道的代碼。
就這么簡單學一下,應該差不多有底氣和面試官吹吹牛逼了。
結束
IL其實不難,有沒有用則仁者見仁,智者見智,有興趣就學一下,也花不了多少時間,確實也沒必要學多深,是吧。
當然,也是要有耐心的,復雜的IL看起來還真是挺頭痛。好在有工具ILSpy,可以在option里選擇部分不反編譯來看會比較簡單些。
參考: