30分钟?不需要,轻松读懂IL
先說(shuō)說(shuō)學(xué)IL有什么用,有人可能覺得這玩意平常寫代碼又用不上,學(xué)了有個(gè)卵用。到底有沒(méi)有卵用呢,暫且也不說(shuō)什么學(xué)了可以看看一些語(yǔ)法糖的實(shí)現(xiàn),或?qū)?net理解更深一點(diǎn)這些虛頭巴腦的東西。最重要的理由就是一個(gè):當(dāng)面試官看你簡(jiǎn)歷上寫著精通C#時(shí),問(wèn)你一句:
"懂不懂IL?"
怎么回答?
"不好意思,那東西沒(méi)什么卵用,所以我沒(méi)學(xué)。"
還是
"還行,可以探討一下。"
你覺得哪個(gè)回答好呢,答得好才更有底氣要到更多的薪資,多個(gè)幾千塊也說(shuō)不定,而這只不過(guò)花上不到半小時(shí)學(xué)習(xí)就可以跟面試官吹上一陣了,很實(shí)用,有沒(méi)有。
?
為什么取這個(gè)標(biāo)題呢,記得很久之前看過(guò)一篇文章,叫"正則表達(dá)式30分鐘入門教程",學(xué)正則最重要的就是記住各個(gè)符號(hào)的含義。個(gè)人覺得相比難以直接看出實(shí)際意義的正則符號(hào)如"\w","\d","*","?","{}[]"等,IL的指令要容易得多。很多人見到IL一大堆的指令,和匯編一樣,就感覺頭大不想學(xué)了。其實(shí)IL本身邏輯很清楚,主要是把指令的意思搞明白就好辦了。記指令只要記住幾個(gè)規(guī)律就好,我把它們分為三類。
?
第一類 :直觀型
這一類的特點(diǎn)是一看名字就知道是干嘛的,不需要多講,如下:
| 名稱 | 說(shuō)明 |
| Add? | 將兩個(gè)值相加并將結(jié)果推送到計(jì)算堆棧上。 |
| Sub? | 從其他值中減去一個(gè)值并將結(jié)果推送到計(jì)算堆棧上。 |
| Div? | 將兩個(gè)值相除并將結(jié)果作為浮點(diǎn)(F 類型)或商(int32 類型)推送到計(jì)算堆棧上。 |
| Mul? | 將兩個(gè)值相乘并將結(jié)果推送到計(jì)算堆棧上。 |
| Rem? | 將兩個(gè)值相除并將余數(shù)推送到計(jì)算堆棧上。 |
| Xor? | 計(jì)算位于計(jì)算堆棧頂部的兩個(gè)值的按位異或,并且將結(jié)果推送到計(jì)算堆棧上。 |
| And? | 計(jì)算兩個(gè)值的按位"與"并將結(jié)果推送到計(jì)算堆棧上。 |
| Or? | 計(jì)算位于堆棧頂部的兩個(gè)整數(shù)值的按位求補(bǔ)并將結(jié)果推送到計(jì)算堆棧上。 |
| Not? | 計(jì)算堆棧頂部整數(shù)值的按位求補(bǔ)并將結(jié)果作為相同的類型推送到計(jì)算堆棧上。 |
| Dup? | 復(fù)制計(jì)算堆棧上當(dāng)前最頂端的值,然后將副本推送到計(jì)算堆棧上。 |
| Neg? | 對(duì)一個(gè)值執(zhí)行求反并將結(jié)果推送到計(jì)算堆棧上。 |
| Ret? | 從當(dāng)前方法返回,并將返回值(如果存在)從調(diào)用方的計(jì)算堆棧推送到被調(diào)用方的計(jì)算堆棧上。 |
| Jmp? | 退出當(dāng)前方法并跳至指定方法。 |
| Newobj? | New Object創(chuàng)建一個(gè)值類型的新對(duì)象或新實(shí)例,并將對(duì)象引用推送到計(jì)算堆棧上。 |
| Newarr? | New Array將對(duì)新的從零開始的一維數(shù)組(其元素屬于特定類型)的對(duì)象引用推送到計(jì)算堆棧上。 |
| Nop? | 如果修補(bǔ)操作碼,則填充空間。盡管可能消耗處理周期,但未執(zhí)行任何有意義的操作。Debug下的 |
| Pop? | 移除當(dāng)前位于計(jì)算堆棧頂部的值。 |
| Initobj? | Init Object將位于指定地址的值類型的每個(gè)字段初始化為空引用或適當(dāng)?shù)幕愋偷?/span> 0。 |
| Isinst? | Is Instance測(cè)試對(duì)象引用是否為特定類的實(shí)例。 |
| Sizeof? | 將提供的值類型的大小(以字節(jié)為單位)推送到計(jì)算堆棧上。 |
| Box | 將值類轉(zhuǎn)換為對(duì)象引用。 |
| Unbox? | 將值類型的已裝箱的表示形式轉(zhuǎn)換為其未裝箱的形式。 |
| Castclass? | 嘗試將引用傳遞的對(duì)象轉(zhuǎn)換為指定的類。 |
| Switch? | 實(shí)現(xiàn)跳轉(zhuǎn)表。 |
| Throw? | 引發(fā)當(dāng)前位于計(jì)算堆棧上的異常對(duì)象。 |
| Call? | 調(diào)用由傳遞的方法說(shuō)明符指示的方法。 |
| Calli? | 通過(guò)調(diào)用約定描述的參數(shù)調(diào)用在計(jì)算堆棧上指示的方法(作為指向入口點(diǎn)的指針)。 |
| Callvirt? | 對(duì)對(duì)象調(diào)用后期綁定方法,并且將返回值推送到計(jì)算堆棧上。 |
強(qiáng)調(diào)一下,有三種call,用的場(chǎng)景不太一樣:
Call:常用于調(diào)用編譯時(shí)就確定的方法,可以直接去元數(shù)據(jù)里找方法,如靜態(tài)函數(shù),實(shí)例方法,也可以call虛方法,不過(guò)只是call這個(gè)類型本身的虛方法,和實(shí)例的方法性質(zhì)一樣。另外,call不做null檢測(cè)。
Calli: MSDN上講是間接調(diào)用指針指向的函數(shù),具體場(chǎng)景沒(méi)見過(guò),有知道的朋友望不吝賜教。
Callvirt: 可以調(diào)用實(shí)例方法和虛方法,調(diào)用虛方法時(shí)以多態(tài)方式調(diào)用,不能調(diào)用靜態(tài)方法。Callvirt調(diào)用時(shí)會(huì)做null檢測(cè),如果實(shí)例是null,會(huì)拋出NullReferenceException,所以速度上比call慢點(diǎn)。
第二類:加載(ld)和存儲(chǔ)(st)
我們知道,C#程序運(yùn)行時(shí)會(huì)有線程棧把參數(shù),局部變量放上來(lái),另外還有個(gè)計(jì)算棧用來(lái)做函數(shù)里的計(jì)算。所以把值加載到計(jì)算棧上,算完后再把計(jì)算棧上的值存到線程棧上去,這類指令專門干這些活。
比方說(shuō) ldloc.0:
這個(gè)可以拆開來(lái)看,Ld打頭可以理解為L(zhǎng)oad,也就是加載;loc可以理解為local variable,也就是局部變量,后面的 .0表示索引。連起來(lái)的意思就是把索引為0的局部變量加載到計(jì)算棧上。對(duì)應(yīng)的 ldloc.1就是把索引為1的局部變量加載到計(jì)算棧上,以此類推。
知道了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 則表示加載數(shù)值,如ldc.i4.0,
?
關(guān)于后綴?
.i[n]:[n]表示字節(jié)數(shù),1個(gè)字節(jié)是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)則表示會(huì)進(jìn)行溢出檢查,溢出時(shí)會(huì)拋出異常;
.un (unsigned)表示無(wú)符號(hào)數(shù);
.ref (reference)表示引用;
.s (short)表示短格式,比如說(shuō)正常的是用int32,加了.s的話就是用int8;
.[n]?比如 .1,.2 等,如果跟在i[n]后面則表示數(shù)值,其他都表示索引。如 ldc.i4.1就是加載數(shù)值1到計(jì)算棧上,再如ldarg.0就是加載第一個(gè)參數(shù)到計(jì)算棧上。
?
?
ldarg要特別注意一個(gè)問(wèn)題:如果是實(shí)例方法的話ldarg.0加載的是本身,也就是this,ldarg.1加載的才是函數(shù)的第一個(gè)參數(shù);如果是靜態(tài)函數(shù),ldarg.0就是第一個(gè)參數(shù)。
?
與ld對(duì)應(yīng)的就是st,可以理解為store,意思是把值從計(jì)算棧上存到變量中去,ld相關(guān)的指令很多都有st對(duì)應(yīng)的,比如stloc, starg, stelem等,就不多說(shuō)了。
?
第三類:比較指令,比較大小或判斷bool值
有一部分是比較之后跳轉(zhuǎn)的,代碼里的 if 就會(huì)產(chǎn)生這些指令,符合條件則跳轉(zhuǎn)執(zhí)行另一些代碼:
以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就是計(jì)算棧上兩個(gè)值相等的話就跳轉(zhuǎn)到IL_0005, ble IL_0023是第一個(gè)值小于或等于第二個(gè)值就跳轉(zhuǎn)到IL_0023。
?
以br(break)開頭:br, brfalse, brtrue,
br是無(wú)條件跳轉(zhuǎn);
brfalse表示計(jì)算棧上的值為 false/null/0 時(shí)發(fā)生跳轉(zhuǎn);
brtrue表示計(jì)算棧上的值為 true/非空/非0 時(shí)發(fā)生跳轉(zhuǎn)
?
還有一部分是c開頭,算bool值的,和前面b開頭的有點(diǎn)像:
ceq 比較兩個(gè)值,相等則將 1 (true) 推到棧上,否則就把 0 (false)推到棧上
cgt?比較兩個(gè)值,第一個(gè)大于第二個(gè)則將 1 (true) 推到棧上,否則就把 0 (false)推到棧上
clt ?比較兩個(gè)值,第一個(gè)小于第二個(gè)則將 1 (true) 推到棧上,否則就把 0 (false)推到棧上
?
以上就是三類常用的,把這些搞明白了,IL指令也就理解得七七八八了。就像看文章一樣,認(rèn)識(shí)大部分字后基本就不影響閱讀了,不認(rèn)識(shí)的猜下再查下,下次再看到也就認(rèn)得了。
?
例子
下面看個(gè)例子,隨手寫段簡(jiǎn)單的代碼,是否合乎邏輯暫不考慮,主要是看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; //標(biāo)準(zhǔn)體重是 (身高-80) * 0.7,區(qū)間在10%內(nèi)都是正常范圍 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文件,如下:
雙擊節(jié)點(diǎn)就可以查看IL,如:
Developer的構(gòu)造函數(shù):
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個(gè)參數(shù),因?yàn)槭菍?shí)例,而實(shí)例的第1個(gè)參數(shù)始終是this 7 IL_0001: call instance void ILLearn.People::.ctor() //調(diào)用基類People的構(gòu)造函數(shù),而People也會(huì)調(diào)用Object的構(gòu)造函數(shù) 8 IL_0006: ldarg.0 //加載this 9 IL_0007: ldarg.1 //加載第二個(gè)參數(shù)也就是name 10 IL_0008: call instance void ILLearn.People::set_Name(string) //調(diào)用this的 set_Name, set_Name這個(gè)函數(shù)是編譯時(shí)為屬性生成的 11 IL_000d: ret //return 12 } // end of method Developer::.ctor?
Developer的GetVocation:
1 .method public hidebysig virtual instance string //虛函數(shù) 2 GetVocation() cil managed 3 { 4 // 代碼大小 6 (0x6) 5 .maxstack 8 //最大計(jì)算棧,默認(rèn)是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, //靜態(tài)函數(shù) 2 int32 weight) cil managed 3 { 4 // 代碼大小 52 (0x34) 5 .maxstack 3 //最大計(jì)算棧大小 6 .locals init ([0] float64 healthyWeight) //局部變量 7 IL_0000: ldarg.0 //加載第1個(gè)參數(shù),因?yàn)槭庆o態(tài)函數(shù),所以第1個(gè)參數(shù)就是height 8 IL_0001: ldc.i4.s 80 //ldc 加載數(shù)值, 加載80 9 IL_0003: sub //做減法,也就是 height-80,把結(jié)果放到計(jì)算棧上,前面兩個(gè)已經(jīng)移除了 10 IL_0004: conv.r8 //轉(zhuǎn)換成double,因?yàn)橄旅嬗?jì)算用到了double,所以要先轉(zhuǎn)換 11 IL_0005: ldc.r8 0.69999999999999996 //加載double數(shù)值 0.7, 為什么是0.69999999999999996呢, 二進(jìn)制存不了0.7,只能找個(gè)最相近的數(shù) 12 IL_000e: mul //計(jì)算棧上的兩個(gè)相乘,也就是(height - 80) * 0.7 13 IL_000f: stloc.0 //存到索引為0的局部變量(healthyWeight) 14 IL_0010: ldarg.1 //加載第1個(gè)參數(shù) weight 15 IL_0011: conv.r8 //轉(zhuǎn)換成double 16 IL_0012: ldloc.0 //加載索引為0的局部變量(healthyWeight) 17 IL_0013: ldc.r8 1.1000000000000001 //加載double數(shù)值 1.1, 看IL_0010到IL_0013,加載了3次,這個(gè)函數(shù)最多也是加載3次,所以maxstack為3 18 IL_001c: mul //計(jì)算棧上的兩個(gè)相乘,也就是 healthyWeight * 1.1, 這時(shí)計(jì)算棧上還有兩個(gè),第一個(gè)是weight,第二個(gè)就是這個(gè)計(jì)算結(jié)果 19 IL_001d: bgt.un.s IL_0032 //比較這兩個(gè)值,第一個(gè)大于第二個(gè)就跳轉(zhuǎn)到 IL_0032,因?yàn)榈谝粋€(gè)大于第二個(gè)表示第一個(gè)條件weight <= healthyWeight * 1.1就是false,也操作符是&&,后面沒(méi)必要再算,直接return 0 20 IL_001f: ldarg.1 //加載第1個(gè)參數(shù) weight 21 IL_0020: conv.r8 //轉(zhuǎn)換成double 22 IL_0021: ldloc.0 //加載索引為0的局部變量(healthyWeight) 23 IL_0022: ldc.r8 0.90000000000000002 //加載double數(shù)值 0.9 24 IL_002b: mul //計(jì)算棧上的兩個(gè)相乘,也就是 healthyWeight * 0.9, 這時(shí)計(jì)算棧上還有兩個(gè),第一個(gè)是weight,第二個(gè)就是這個(gè)計(jì)算結(jié)果 25 IL_002c: clt.un //比較大小,第一個(gè)小于第二個(gè)則把1放上去,否則放0上去 26 IL_002e: ldc.i4.0 //加載數(shù)值0 27 IL_002f: ceq //比較大小,相等則把1放上去,否則放0上去 28 IL_0031: ret //return 棧頂?shù)臄?shù),為什么沒(méi)用blt.un.s,因?yàn)镮L_0033返回的是false 29 IL_0032: ldc.i4.0 //加載數(shù)值0 30 IL_0033: ret //return 棧頂?shù)臄?shù) 31 } // end of method People::IsHealthyWeight?
主函數(shù)Main:
1 .method private hidebysig static void Main(string[] args) cil managed 2 { 3 .entrypoint //這是入口 4 // 代碼大小 67 (0x43) 5 .maxstack 3 //大小為3的計(jì)算棧 6 .locals init (string V_0, 7 string V_1) //兩個(gè)string類型的局部變量,本來(lái)還有個(gè)people的局部變量,被release方式優(yōu)化掉了,因?yàn)橹皇钦{(diào)用了people的GetVocation,后面沒(méi)用,所以可以不存 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一個(gè)Developer并把棧上的brook給構(gòu)造函數(shù) 11 IL_000f: callvirt instance string ILLearn.People::GetVocation() //調(diào)用GetVocation 12 IL_0014: stloc.0 //把上面計(jì)算的結(jié)果存到第1個(gè)局部變量中,也就是V_0 13 IL_0015: ldc.i4.s 60 //加載int型60 14 IL_0017: call bool ILLearn.People::IsHealthyWeight(int32, //調(diào)用IsHealthyWeight,因?yàn)槭庆o態(tài)函數(shù),所以用call 15 int32) 16 IL_001c: brtrue.s IL_0025 //如果上面返回true的話就跳轉(zhuǎn)到IL_0025 17 IL_001e: ldstr "not healthy" //加載string "not healthy" 18 IL_0023: br.s IL_002a //跳轉(zhuǎn)到IL_002a 19 IL_0025: ldstr "healthy" //加載string "healthy" 20 IL_002a: stloc.1 //把結(jié)果存到第2個(gè)局部變量中,也就是V_1, IL_0017到IL_002a這幾個(gè)指令加在一起用來(lái)計(jì)算三元表達(dá)式 21 IL_002b: ldstr "{0} is {1}" //加載string "{0} is {1}" 22 IL_0030: ldloc.0 //加載第1個(gè)局部變量 23 IL_0031: ldloc.1 //加載第2個(gè)局部變量 24 IL_0032: call string [mscorlib]System.String::Format(string, //調(diào)用string.Format,這里也可以看到C# 6.0的語(yǔ)法糖 $"{vocation} is {healthStatus}",編譯后的結(jié)果和以前的用法一樣 25 object, 26 object) 27 IL_0037: call void [mscorlib]System.Console::WriteLine(string) //調(diào)用WriteLine 28 IL_003c: call string [mscorlib]System.Console::ReadLine() //調(diào)用ReadLine 29 IL_0041: pop 30 IL_0042: ret 31 } // end of method Program::Main很簡(jiǎn)單吧,當(dāng)然,這個(gè)例子也很簡(jiǎn)單,沒(méi)有事件,沒(méi)有委托,也沒(méi)有async/await之類,這些有興趣的可以寫代碼跟一下,這幾種都會(huì)在編譯時(shí)插入也許你不知道的代碼。
就這么簡(jiǎn)單學(xué)一下,應(yīng)該差不多有底氣和面試官吹吹牛逼了。
?
結(jié)束
IL其實(shí)不難,有沒(méi)有用則仁者見仁,智者見智,有興趣就學(xué)一下,也花不了多少時(shí)間,確實(shí)也沒(méi)必要學(xué)多深,是吧。
當(dāng)然,也是要有耐心的,復(fù)雜的IL看起來(lái)還真是挺頭痛。好在有工具ILSpy,可以在option里選擇部分不反編譯來(lái)看會(huì)比較簡(jiǎn)單些。
?
參考:
IL指令表: http://www.cnblogs.com/zery/p/3368460.html
轉(zhuǎn)載于:https://www.cnblogs.com/brookshi/p/5225801.html
總結(jié)
以上是生活随笔為你收集整理的30分钟?不需要,轻松读懂IL的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ubuntu lamp配置多域名服务器
- 下一篇: 2013计算机视觉代码合集二