C#基礎之IL

1.實例解析IL程序員

  做爲C#程序員,IL的做用不言而喻,首先來看一個很是簡單的程序和它的IL解釋圖,經過這個程序的IL指令來簡單的瞭解常見的IL指令是什麼意思。數據庫

    class Program { static void Main(string[] args) { int i = 2; string str= "C#"; Console.WriteLine("hello "+str); } }
View Code

接下來要明確一個概念,.NET運行時任何有意義的操做都是在堆棧上完成的,而不是直接操做寄存器。這就爲.NET跨平臺打下了基礎,經過設計不一樣的編譯器編譯相同的IL代碼來實現跨平臺。對於堆棧咱們的操做無非就是壓棧和出棧,在IL中壓棧一般以ld開頭,出棧則以st開頭。知道這個後再看上面的指令感受一會兒就豁然開朗了,接下來繼續學習的步伐,下面的表格是對於一些常見ld指令。st指令則是將ld指令換成st,功能有壓棧變爲出棧,有時候會看到在st或ld後加.s這表示只取一個字節。再來看看流程控制,知道壓出棧和流程控制後,基本上看出IL的大概意思那就冒悶踢啦。流程控制主要就是循環和分支,下面我寫了個有循環和分支的小程序。其中咱們用到了加法和比較運算,爲此得在這裏介紹最基本的三種運算:算術運算(add、sub、mul乘法、div、rem求餘);比較運算(cgt大於、clt小於、ceq等於);位運算(not、and、or、xor異或、左移shl、右移shr)。要注意在比較運算中,當執行完指令後會直接將結果1或0壓棧,這個過程是自動完成的。對於流程控制,主要是br、brture和brfalse這3條指令,其中br是直接進行跳轉,brture和brture則是進行判斷再進行跳轉。小程序

ldarg 加載成員的參數,如上面的ldarg.0
ldarga 裝載參數的地址,注意通常加個a表示取地址
ldc 將數字常量壓棧,如上面的ldc.i4.2
ldstr 將字符串的引用壓棧
ldloc/ldloca ldloc將一個局部變量壓棧,加a表示將這個局部變量的地址壓棧
Ldelem 表示將數組元素壓棧
ldlen 將數組長度壓棧
ldind 將地址壓棧,以地址來訪問或操做數據內
 class Program { static void Main(string[] args) { int count = 2; string strName= "C#"; if (strName == "C#") { for(int i=0;i<count;i++) Console.WriteLine("hello C#"); } else Console.WriteLine("ha ha"); } }
View Code

2.面向對象的IL數組

  有了前面的基礎後,基本上看通常的IL代碼不會那麼方了。若是咱們在程序中聲明一個類並建立對象,則在IL中能夠看到newobj、class、instance、static等關鍵字。看IL指令會發現外部是類,類裏面有方法,雖然方法裏面是指令不過這和C#代碼的結構是很類似的。從上面的這些現象能夠很明顯的感覺到IL並非簡單的指令,它是面向對象的。當咱們在C#中使用new建立一個對象時則在IL中對應的是newobj,另外還有值類型也是能夠經過new來建立的,不過在IL中它對應的則是initobj。newobj用來建立一個對象,首先會分配這個對象所需的內存,接着初始化對象附加成員同步索引塊和類型對象指針而後再執行構造函數進行初始化並返回對象引用。initobj則是完成棧上已經分配好的內存的初始化工做,將值類型置0引用類型置null便可。另外string是引用類型,從上面的例子能夠看到通常是使用ldstr來將元數據中的字符串引用加載到棧中而不是newobj。可是若是在代碼中建立string變量不是直接賦值而是使用new關鍵字來獲得string對象,那麼在IL中將會看到newobj指令。當建立一維零基數組時還會看到newarr指令,它會建立數組並將首地址壓棧。不過若是數組不是一維零基數組的話仍將仍是會看到咱們熟悉的newobj。ide

  既然是面向對象的,那麼繼承中的虛方法或抽象方法在IL中確定會有相應的指令去完成方法的調用。調用方法主要是call、callvirt、calli,call主要用來調用靜態方法,callvirt則用來調用普通方法和須要運行時綁定的方法(也就是用instance標記的實例方法),calli是經過函數指針來進行調用的。不過也存在特殊狀況,那就是call去調用虛方法,好比在密封類中的虛方法由於必定不可能會被重寫所以使用call可提升性能。爲何會提升性能呢?不知道你是否還記得建立一個對象去調用這個對象的方法時,咱們常常會判斷這個對象是否爲null,若是這個對象爲null時去調用方法則會報錯。之因此出現這種狀況是由於callvirt在調用方法時會進行類型檢測,此外判斷是否有子類方法覆蓋的狀況從而動態綁定方法,而採用call則直接去調用了。另外當調用基類的虛方法時,好比調用object.ToString方法就是採用call方法,若是採用callvirt的話由於有可能要查看子類(一直查看到最後一個繼承父類的子類)是否有重寫方法,從而下降了性能。不過說到底call用來調用靜態方法,而callvirt調用與對象關聯的動態方法的核心思想是能夠確定的,那些採用call的特殊狀況都是由於在這種狀況下根本不須要動態綁定方法而是能夠直接使用的。calli的意思就是拿到一個指向函數的引用,經過這個引用去調用函數,不過在個人學習中沒有使用到這個,這個具體是如何拿到引用的我也不清楚,感興趣者請自行百度。函數

3.IL的角色性能

  你們都知道C#代碼編譯後就會生成元數據和IL,但是咱們常見的exe這樣的程序集是如何生成的呢,它與IL是什麼關係呢?首先有一點是能夠確定的,那就是程序集中確定會包含元數據和IL,由於這2樣東西是程序集中的核心。下面是一個描述程序集和內部組成圖,從圖中能夠看出一個程序集是有多個託管模塊組成的,一個模塊能夠理解爲一個類或者多個類一塊兒編譯後生成的程序集。程序集清單指的是描述程序集的相關信息,PE文件頭描述PE文件的文件類型、建立時間等。CLR頭描述CLR版本、CPU信息等,它告訴系統這是一個.NET程序集。而後最主要的就是每一個託管模塊中的元數據和IL了。元數據用來描述類、方法、參數、屬性等數據,.NET中每一個模塊包含44個元數據表,主要包括定義表、引用表、指針表和堆。定義表包括類定義表、方法表等,引用表描述引用到類型或方法之間的映射記錄,指針表裏存放着方法指針、參數指針等。能夠看到元數據表就至關於一個數據庫,多張表之間有相似於主外鍵之間的關係。學習

由前面的知識能夠總結出IL是獨立於CPU且面向對象的指令集。.NET平臺將其之上的語言全都編譯成符合CLS(公共語言規範)的IL指令集,接着再由不一樣的編譯器翻譯成本地代碼,好比咱們常見的JIT編譯器,若是在Mac上運行C#可經過Mac上的特定編譯器來將IL翻譯成Mac系統可以執行的機器碼。也就是說IL正如它的名字同樣是做爲一種中間語言來執行動態程序,好比咱們調用一個方法表中的方法,這個方法會指向一個觸發JIT編譯器地址和方法對應的IL地址,因而JIT編譯器便將這個方法指向的IL編譯成本地代碼。生成本地代碼後這個方法將會有一條引用指向本地代碼首地址,這樣下次調用這個方法的時候將直接執行指向的本地代碼。spa

 聲明:本文原創發表於博客園,做者爲方小白,若有錯誤歡迎指出 。本文未經做者許可不準轉載,不然視爲侵權。翻譯

相關文章
相關標籤/搜索