先說說學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裡選擇部分不反編譯來看會比較簡單些。
參考:
IL指令表: http://www.cnblogs.com/zery/p/3368460.html