程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 輕松讀懂IL代碼

輕松讀懂IL代碼

編輯:關於.NET
30分鐘?不需要,輕松讀懂IL

先說說學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:

源代碼:

 using System;
 
 namespace ILLearn
 {
     class Program
     {
         const int WEIGHT = 60;
 
         static void Main(string[] args)
         {
             var height = 170;
 
             People people = new Developer("brook");
 
             var vocation = people.GetVocation();
 
             var healthStatus = People.IsHealthyWeight(height, WEIGHT) ? "healthy" : "not healthy";
 
             Console.WriteLine($"{vocation} is {healthStatus}");
 
             Console.ReadLine();
         }
     }
 
     abstract class People
     {
         public string Name { get; set; }
 
         public abstract string GetVocation();
 
         public static bool IsHealthyWeight(int height, int weight)
         {
             var healthyWeight = (height - 80) * 0.7;
             return weight <= healthyWeight * 1.1 && weight >= healthyWeight * 0.9; //標准體重是 (身高-80) *  0.7,區間在10%內都是正常范圍
         }
     }
 
     class Developer : People
     {
         public Developer(string name)
         {
             Name = name;
         }
 
         public override string GetVocation()
         {
             return "Developer";
         }
     }
 }

 

在命令行裡輸入: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的構造函數:

 .method public hidebysig specialname rtspecialname 
         instance void  .ctor(string name) cil managed
 {
   // 代碼大小       14 (0xe)
   .maxstack  8
   IL_0000:  ldarg.0  //加載第1個參數,因為是實例,而實例的第1個參數始終是this
   IL_0001:  call       instance void ILLearn.People::.ctor()  //調用基類People的構造函數,而People也會調用Object的構造函數
   IL_0006:  ldarg.0  //加載this
   IL_0007:  ldarg.1  //加載第二個參數也就是name
   IL_0008:  call       instance void ILLearn.People::set_Name(string)  //調用this的 set_Name, set_Name這個函數是編譯時為屬性生成的
   IL_000d:  ret  //return
 } // end of method Developer::.ctor

 

Developer的GetVocation:

 .method public hidebysig virtual instance string //虛函數
         GetVocation() cil managed
 {
   // 代碼大小       6 (0x6)
   .maxstack  8  //最大計算棧,默認是8
   IL_0000:  ldstr      "Developer"  //加載string "Developer"
   IL_0005:  ret //return
 } // end of method Developer::GetVocation

 

People的IsHealthyWeight:

 .method public hidebysig static bool  IsHealthyWeight(int32 height,  //靜態函數
                                                       int32 weight) cil managed
 {
   // 代碼大小       52 (0x34)
   .maxstack  3  //最大計算棧大小
   .locals init ([0] float64 healthyWeight) //局部變量
   IL_0000:  ldarg.0  //加載第1個參數,因為是靜態函數,所以第1個參數就是height
   IL_0001:  ldc.i4.s   80  //ldc 加載數值, 加載80
   IL_0003:  sub  //做減法,也就是 height-80,把結果放到計算棧上,前面兩個已經移除了
   IL_0004:  conv.r8  //轉換成double,因為下面計算用到了double,所以要先轉換
   IL_0005:  ldc.r8     0.69999999999999996  //加載double數值 0.7, 為什麼是0.69999999999999996呢, 二進制存不了0.7,只能找個最相近的數
   IL_000e:  mul  //計算棧上的兩個相乘,也就是(height - 80) * 0.7
   IL_000f:  stloc.0  //存到索引為0的局部變量(healthyWeight)
   IL_0010:  ldarg.1  //加載第1個參數 weight
   IL_0011:  conv.r8  //轉換成double
   IL_0012:  ldloc.0  //加載索引為0的局部變量(healthyWeight)
   IL_0013:  ldc.r8     1.1000000000000001  //加載double數值 1.1, 看IL_0010到IL_0013,加載了3次,這個函數最多也是加載3次,所以maxstack為3
   IL_001c:  mul  //計算棧上的兩個相乘,也就是 healthyWeight * 1.1, 這時計算棧上還有兩個,第一個是weight,第二個就是這個計算結果
   IL_001d:  bgt.un.s   IL_0032  //比較這兩個值,第一個大於第二個就跳轉到 IL_0032,因為第一個大於第二個表示第一個條件weight <= healthyWeight * 1.1就是false,也操作符是&&,後面沒必要再算,直接return 0
   IL_001f:  ldarg.1  //加載第1個參數 weight
   IL_0020:  conv.r8  //轉換成double
   IL_0021:  ldloc.0  //加載索引為0的局部變量(healthyWeight)
   IL_0022:  ldc.r8     0.90000000000000002  //加載double數值 0.9
   IL_002b:  mul  //計算棧上的兩個相乘,也就是 healthyWeight * 0.9, 這時計算棧上還有兩個,第一個是weight,第二個就是這個計算結果
   IL_002c:  clt.un  //比較大小,第一個小於第二個則把1放上去,否則放0上去
   IL_002e:  ldc.i4.0 //加載數值0
   IL_002f:  ceq  //比較大小,相等則把1放上去,否則放0上去
   IL_0031:  ret  //return 棧頂的數,為什麼沒用blt.un.s,因為IL_0033返回的是false
   IL_0032:  ldc.i4.0  //加載數值0
   IL_0033:  ret  //return 棧頂的數
 } // end of method People::IsHealthyWeight

 

主函數Main:

 .method private hidebysig static void  Main(string[] args) cil managed
 {
   .entrypoint  //這是入口
   // 代碼大小       67 (0x43)
   .maxstack  3  //大小為3的計算棧
   .locals init (string V_0,
            string V_1)  //兩個string類型的局部變量,本來還有個people的局部變量,被release方式優化掉了,因為只是調用了people的GetVocation,後面沒用,所以可以不存
   IL_0000:  ldc.i4     0xaa  //加載int型170
   IL_0005:  ldstr      "brook"  //加載string "brook"
   IL_000a:  newobj     instance void ILLearn.Developer::.ctor(string)  //new一個Developer並把棧上的brook給構造函數
   IL_000f:  callvirt   instance string ILLearn.People::GetVocation()  //調用GetVocation
   IL_0014:  stloc.0  //把上面計算的結果存到第1個局部變量中,也就是V_0
   IL_0015:  ldc.i4.s   60  //加載int型60
   IL_0017:  call       bool ILLearn.People::IsHealthyWeight(int32,  //調用IsHealthyWeight,因為是靜態函數,所以用call
                                                             int32)
   IL_001c:  brtrue.s   IL_0025  //如果上面返回true的話就跳轉到IL_0025
   IL_001e:  ldstr      "not healthy"  //加載string "not healthy"
   IL_0023:  br.s       IL_002a  //跳轉到IL_002a
   IL_0025:  ldstr      "healthy"  //加載string "healthy"
   IL_002a:  stloc.1  //把結果存到第2個局部變量中,也就是V_1, IL_0017到IL_002a這幾個指令加在一起用來計算三元表達式
   IL_002b:  ldstr      "{0} is {1}"  //加載string "{0} is {1}"
   IL_0030:  ldloc.0  //加載第1個局部變量
   IL_0031:  ldloc.1  //加載第2個局部變量
   IL_0032:  call       string [mscorlib]System.String::Format(string,  //調用string.Format,這裡也可以看到C# 6.0的語法糖 $"{vocation} is {healthStatus}",編譯後的結果和以前的用法一樣
                                                               object,
                                                               object)
   IL_0037:  call       void [mscorlib]System.Console::WriteLine(string)  //調用WriteLine
   IL_003c:  call       string [mscorlib]System.Console::ReadLine()  //調用ReadLine
   IL_0041:  pop
   IL_0042:  ret
 } // end of method Program::Main

很簡單吧,當然,這個例子也很簡單,沒有事件,沒有委托,也沒有async/await之類,這些有興趣的可以寫代碼跟一下,這幾種都會在編譯時插入也許你不知道的代碼。

就這麼簡單學一下,應該差不多有底氣和面試官吹吹牛逼了。

 

結束

IL其實不難,有沒有用則仁者見仁,智者見智,有興趣就學一下,也花不了多少時間,確實也沒必要學多深,是吧。

當然,也是要有耐心的,復雜的IL看起來還真是挺頭痛。好在有工具ILSpy,可以在option裡選擇部分不反編譯來看會比較簡單些。

 

參考:

IL指令表:http://hovertree.com/h/bjaf/ba9p8uk2.htm

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved