C#基礎拾遺系列之一:先看懂IL程式碼
一、前言
首先,想說說為什麼要寫這樣系列的文章,有時候在和同事朋友聊天的時候,經常會聽到這樣的話題:
(1)在這家公司沒什麼長進,程式碼太爛,學不到東西。(你有沒有想想框架為什麼這樣寫,程式碼還可以怎麼去優化,比如公司使用Dapper,原始碼研究過沒以及這樣封裝原因是啥)
(2)現在只會Ctrl + C Ctrl +V ,不排除有時為了效率,包括我自己有時候也懶的寫直接複製貼上 (是不是感覺距離語言的本質越來越遠了)
(3)Ctrl + C Ctrl +V 時間長了,都有點懷疑自己是否有勇氣面試其他公司 (是不是總給自己找藉口,年齡大了,不敢瘋狂了,當然大家不要誤解,我沒鼓勵大家跳槽)
(4)幹了幾年沒什麼提高 (無論要精通那門技術,我們都應該從其本質出發)
最近也在反思自己,之前看到部落格園大神:fish-li 的一篇文章《Fish Li 該如何幫助您呢?》其中說到:如何做一個有追求的技術人員,受益匪淺。以及張善友老師分享的關於雷果果的技術之路,大家都羨慕這些大神,何曾想過他們背後的付出,不要再抱怨環境不好,環境好也是給這些有準備和有追求的人,很感謝有這樣的前輩,現在的社會確實很浮躁,但與我何干,好了毒雞湯就灌到這裡,大家如果有共鳴的話,不要表達出來了,默默想想。
二、IL指令
(1)什麼是IL
IL(Intermediate Language),它也稱為CIL或者MSIL,中文就是“中間語言”。IL由ECMA組織(ECMA-335標準)提供完整的定義和規範。我們可以直接把C#原始碼編譯為.exe或dll檔案,但是此時編譯出來的程式程式碼並不是CPU能直接執行的二進位制程式碼,而是IL程式碼。
(2)反編譯工具
在這裡使用ILDasm.exe,不適用其他的反編譯工具,這個工具在安裝完畢VS2010後就已經存在了,大家可以在開始選單中輸入“IL”關鍵字進行搜尋,如果沒有的話,到C:\Program Files(x86)\Microsoft SDKs\Windows\v7.0A\Bin\NETFX4.0Tools
(3)IL 指令表
三、使用工具檢視IL程式碼
案例一:
namespace ConsoleApplication1 { class Program { static void Main(string[] args) {string helloString = "Hello World"; Console.WriteLine(helloString); } } }
從上圖可以看到IL結構:包含MANIFEST檔案和ConsoleApplication1,其中MANIFEST是一個清單檔案,主要包括程式集的一些屬性,例如程式集名稱、版本號、雜湊演算法、程式集模組,以及對外部引用程式的引用專案。.Program類是我們要主要介紹的內容。
(1)MANIFEST清單檔案介紹
// Metadata version: v4.0.30319 .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. .ver 4:0:0:0 } .assembly ConsoleApplication1 { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) .custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78 // ....T..WrapNonEx 63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 ) // ceptionThrows. // --- 下列自定義屬性會自動新增,不要取消註釋 ------- // .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) .custom instance void [mscorlib]System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 13 43 6F 6E 73 6F 6C 65 41 70 70 6C 69 63 // ...ConsoleApplic 61 74 69 6F 6E 31 00 00 ) // ation1.. .custom instance void [mscorlib]System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Reflection.AssemblyConfigurationAttribute::.ctor(string) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 13 43 6F 6E 73 6F 6C 65 41 70 70 6C 69 63 // ...ConsoleApplic 61 74 69 6F 6E 31 00 00 ) // ation1.. .custom instance void [mscorlib]System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 12 43 6F 70 79 72 69 67 68 74 20 C2 A9 20 // ...Copyright .. 20 32 30 31 38 00 00 ) // 2018.. .custom instance void [mscorlib]System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Runtime.InteropServices.ComVisibleAttribute::.ctor(bool) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string) = ( 01 00 24 62 35 32 39 36 38 63 32 2D 64 66 63 33 // ..$b52968c2-dfc3 2D 34 65 38 31 2D 38 32 64 32 2D 64 39 66 35 62 // -4e81-82d2-d9f5b 62 33 32 38 33 64 37 00 00 ) // b3283d7.. .custom instance void [mscorlib]System.Reflection.AssemblyFileVersionAttribute::.ctor(string) = ( 01 00 07 31 2E 30 2E 30 2E 30 00 00 ) // ...1.0.0.0.. .custom instance void [mscorlib]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = ( 01 00 1C 2E 4E 45 54 46 72 61 6D 65 77 6F 72 6B // ....NETFramework 2C 56 65 72 73 69 6F 6E 3D 76 34 2E 35 2E 32 01 // ,Version=v4.5.2. 00 54 0E 14 46 72 61 6D 65 77 6F 72 6B 44 69 73 // .T..FrameworkDis 70 6C 61 79 4E 61 6D 65 14 2E 4E 45 54 20 46 72 // playName..NET Fr 61 6D 65 77 6F 72 6B 20 34 2E 35 2E 32 ) // amework 4.5.2 .hash algorithm 0x00008004 .ver 1:0:0:0 } .module ConsoleApplication1.exe // MVID: {A09A2101-9A49-483A-A224-5D2E14D231A6} .imagebase 0x00400000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00020003 // ILONLY 32BITREQUIRED // Image base: 0x01110000View Code
介紹:
指定當前程式集需要引用的外部程式集,如上面 ConsoleApplication1.exe就是引用了mscorlib.
.publickeytoken = (標記 ) 指定所引用程式集的實際公鑰標記。公鑰能唯一確定程式集。
.ver:指定引用程式集的版本
.assembly 程式集名稱 指定程式集名稱
.hash algorithm int32值 : 指定使用的hash演算法
.ver :指定程式集版本號
.module ConsoleApplication1.exe 指定組成程式集的模組名稱,在此示例中,程式集中只包含一個檔案
.subsystem 0x0003 // WINDOWS_CUI 指定程式要求的應用程式環境。在此示例中,0x0003表示該可執行檔案從控制檯執行
.corflags 0x00020003 // ILONLY 32BITREQUIRED 當前是元資料中的一個保留欄位
.class表示的Program是一個類,extends 代表Program類繼承於程式集mscorlib中的System.Object類,這就告訴我們,在C#中所有的類的父類都是Object。
private為訪問許可權,表明該類是私有的。
auto:表明程式載入的時候記憶體佈局是有CLR決定的,而不是由程式本身控制的。
ansi:表明類的編碼為ansi編碼
beforefieldinit :表明CLR可以在第一次訪問靜態欄位之前的任何時刻執行型別建構函式。型別建構函式也就是建構函式,而使用beforefieldinit屬性可以提高效能。
.ctor 表示建構函式
cil managed 表明方法體中的程式碼是IL程式碼,且是託管程式碼,即執行在CLR執行庫中的程式碼
.maxstack 表明執行建構函式時,評估堆疊可容納資料項的最大個數。評估堆疊是儲存方法中所需變數的值的一個記憶體區域,該區域在方法執行結束時會被清空,或者儲存一個返回值
IL_0000是程式碼行的開頭。一般在IL_標記之前的部分為變數的宣告和初始化操作。
ldarg.0表明載入第一個成員引數,其中ldarg是load argument 的縮寫
call 指令一般用於呼叫靜態方法,而這段程式碼中call指令並不是在呼叫靜態函式,而是呼叫System.Object建構函式。另外一個指令則一般用來呼叫例項方法,它的呼叫過程是:首先檢查被呼叫的函式是否為虛擬函式,
如果不是就直接呼叫,如果是則檢查子類是否重寫,如果有重寫就呼叫子類中的實現,如果沒有重寫就繼續呼叫原來函式。
ret 指令表示執行完畢,就是return的縮寫
最後是Main函式,它是應用程式的入口函式:
hidebysig :指令表示如果當前類作為父類,用該指令標記的方法將不會被子類繼承
.entrypoint :指令代表該函式是程式的入口函式,每個託管應用程式都有且只有一個入口函式,CLR載入的時候,首先從.entrypoint函式開始執行。
.locals init ([0] string helloString) 表示定義string 型別的變數,變數名成為:helloString
IL_0000: nop 表示不做任何操作 No Operation
ldstr "Hello World" : ldstr:推送對元資料中儲存的字串的新物件引用 表示:將字串“Hello World” 壓入評估棧,此時“Hello World” 處於評估棧的棧定,棧是一種資料結構,具有先進後出的特性。
stloc.0 :從計算堆疊的頂部彈出當前值並將其儲存到索引 0 處的區域性變數列表中(也就helloString) 在此示例中:就是把字串"Hello World" 賦值給變數helloString
ldloc.0 :將索引 0 處的區域性變數載入到計算堆疊上。也就是:把變數helloString 載入到計算堆疊上
(以ld為字首的指令表示:入棧操作 st為字首的指令則代表著出棧操作)
call :指令表示呼叫靜態函式, 這裡呼叫的是Console類中的WriteLine函式,把第0個區域性變數輸出到控制檯中
案例二:
(1)IL基本型別
任何都有其內建的型別,IL語言也不例外,因為C#語言最終都會編譯成IL程式碼,所以兩者必然存在一種對應關係:
(2)IL變數的宣告
.locals 指令代表變數的宣告,宣告語句放在IL_標記前面。例如在前面的程式中,
.locals init ([0] string helloString) 就聲明瞭一個名為helloString的變數,其中型別為string
(3)基本運算子
算數運算子:加法指令add 、乘法指令sub、出發指令div、以及求餘指令rem等
位運算子:包括一元指令not 、與指令and、或指令or,結果以1 和 0 分別表示真、假,運算結果壓入評估棧棧頂
比較運算:包括大於指令cgt、小於指令clt 和等於指令 ceq
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ILAdd { class Program { static void Main(string[] args) { int i = 2; int j = 3; int result = i + j; Console.WriteLine(result); } } }
IL程式碼分析如下:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 程式碼大小 17 (0x11) .maxstack 2 //宣告三個變數 .locals init ([0] int32 i, [1] int32 j, [2] int32 result) //不做任何操作 IL_0000: nop //將整數值 2以4位元組 作為 int32 推送到計算堆疊上 IL_0001: ldc.i4.2 //把評估棧頂的值彈出,並賦值給第0個區域性變數(即 i),等價於i = 2 IL_0002: stloc.0 //將整數值 3以4位元組 作為 int32 推送到計算堆疊上 IL_0003: ldc.i4.3 //把評估棧頂的值彈出,並賦值給第1個區域性變數(即 j),等價於j = 2 IL_0004: stloc.1 //把第0個變數壓入評估棧,即把變數 i 壓入評估棧 IL_0005: ldloc.0 //把第1個變數壓入評估棧,即把變數 j 壓入評估棧 IL_0006: ldloc.1 //執行add操作,之後將把變數i和j清空,並把操作結果儲存在評估棧站頂 IL_0007: add //把站頂的值彈出,並賦值給第二個區域性變數(即result) ,此時result即為i+j 的值,因為棧頂為兩個值的和 IL_0008: stloc.2 //將索引 2 處的區域性變數載入到計算堆疊上。就是result IL_0009: ldloc.2 //call呼叫靜態函式 IL_000a: call void [mscorlib]System.Console::WriteLine(int32) IL_000f: nop //返回 IL_0010: ret } // end of method Program::MainView Code
static void Main(string[] args) { int i = 2; if (i>0) { Console.WriteLine("i為正數"); } else { Console.WriteLine("i為0或負數"); } }
IL分析如下:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 程式碼大小 40 (0x28) .maxstack 2 .locals init ([0] int32 i, [1] bool V_1) IL_0000: nop IL_0001: ldc.i4.2 IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: ldc.i4.0 //執行大於指令,比較i與0,運算結果存在於評估棧棧頂,1表示真,即i>0為真 IL_0005: cgt //就是把比較後的值賦值個變數V_1 IL_0007: stloc.1 // 把變數V_1壓入評估棧 IL_0008: ldloc.1 //如果 value 為 false、空引用或零,則將控制轉移到目標指令 IL_0009: brfalse.s IL_001a IL_000b: nop //推送對元資料中儲存的字串的新物件引用。 IL_000c: ldstr bytearray (69 00 3A 4E 63 6B 70 65 ) // i.:Nckpe IL_0011: call void [mscorlib]System.Console::WriteLine(string) IL_0016: nop IL_0017: nop //無條件地將控制轉移到目標指令(短格式) IL_0018: br.s IL_0027 IL_001a: nop //推送對元資料中儲存的字串的新物件引用。 IL_001b: ldstr bytearray (69 00 3A 4E 30 00 16 62 1F 8D 70 65 ) // i.:N0..b..pe IL_0020: call void [mscorlib]System.Console::WriteLine(string) IL_0025: nop IL_0026: nop IL_0027: ret } // end of method Program::MainView Code
案例三:const的本質
namespace ConstIL { class Program { static void Main(string[] args) { Console.WriteLine(Person.num); } } public class Person { /// <summary> /// 這個就是一個所謂的const常量 /// </summary> public const int num = 10; } }View Code
IL程式碼分析:
為什麼可以直接類名.num?這種語法只有在該常量為static修飾是才可以,下面我們來看看IL:
.field public static literal int32 num = int32(0x0000000A)
看到沒,const其實就是一個static的變數,一個靜態的值,因為它是跟著類走的。而不是例項。所以 const的特徵如下:
(1)固定不變的值。
(2)在編譯的時候就已經確定了。
(3)在初始化的時候設定值
好了,先寫到這裡,回家前寫這一篇,希望對你有幫助。
參考書籍:《Learning Hard》
參考文章:http://www.cnblogs.com/flyingbirds123/archive/2011/01/29/1947626.html
作者:郭崢
出處:http://www.cnblogs.com/runningsmallguo/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。