C#/.NET JIT和IL(MSIL或CIL)實現跨平台

2020-07-16 10:04:45
所有 .NET 支援的語言編寫出來的程式,在對應的編譯器編譯之後,會先產出程式集,其主要內容是中間語言 IL 和後設資料。

之後,JIT 再將 IL 翻譯為機器碼(不同機器實現方式不同)。

IL 使得跨平台成為可能,並且統一了各個框架語言編譯之後的形式,使得框架實現的代價大大降低了。

比如,.NET 框架有N種語言,那麼每種語言都必須有自己的編譯器。而 .NET 框架又決定跨 M 種平台,那麼,就需要有 M 種 JIT。

如果不存在 IL,則 .NET 框架為了支援 N 種語言跨 M 種平台,需要 MxN 個編譯器。

但如果所有 .NET 框架的 N 種語言經過編譯之後,都變成相同的形式,那麼只需要 M+N 個編譯器就可以了。因此,IL 大大降低了跨平台的代價。

什麼是 IL(CIL)

在 .NET 的開發過程中,IL 的官方術語是 MSIL 或 CIL(Common Intermediate Language, 公共中間語言)。

因此,IL、MSIL 和 CIL 指的是同一種東西,我們統一使用 IL 進行指代。

使用不同語言(例如 C# 和 VB)經過不同編譯器(例如 C# 編譯器和 VB 編譯器),編譯一段功能相似的程式碼(區別僅僅在於語法),其 IL 也基本相似。

可以通過反編譯工具載入任意的 .NET 程式集並分析它的內容,包括它所包含的 IL 程式碼和後設資料,以及反編譯之後的 C# 程式碼。

在 C# 沒有開源之前,這項技能是開發者進階的必備技能,這是因為有些性質是必須通過檢視 IL 或反編譯才能得知的,例如裝箱和拆箱發生了多少次(box 是 IL 指令),using 的本質實際上是一個 try-finally 塊,閉包和委託涉及密封類、疊代器和狀態機等等。

另外,也可以自己書寫 IL 程式碼,然後使用 .NET 自帶的 ilasm. exe 編譯為程式集。

初識 IL

IL 雖然比 C# 低階一些,但它實際上也擁有很多助記符和指令,這些指令使得 IL 的可讀性沒有想象中那麼差。

IL 中的關鍵字可以分為三類:指令、特性和操作碼。

IL 指令在語法上使用一個點字首來表示。

在 ILSpy 中,IL 指令是綠色的。這些指令用來描述程式碼檔案的結構,包括 .namespace.class、.method、.field、.property等等。例如,如果你的程式碼檔案包括了三個 .class,這意味著 C# 原始碼包含三個型別。

下面的 IL 程式碼中 包括四個欄位和一個方法:

// Fields
.field private object '<>2_current'
.field private int32 '<>1_state '
.field public class DataStructureLab.People '<>4_this'
.field public int32 '<i>5_1'
// Methods
.method private final hidebysig newslot virtual
instance bool MoveNext () cil managed
{

IL 特性和 IL 指令一起修飾成員。例如,一個 .class 可以被 public 修飾,指定它的可見性,也可以被 extend 修飾,指定它的父類別,也可以被 static 或 instance 修飾指定它是靜態還是範例成員等等。

IL 操作碼提供了可以在 IL 上實現的各種操作。

真正的操作碼是無法一眼理解的二進位制資料(例如相加的操作碼是 0x58),但是,每個操作碼都對應一個助記符(例如相加的助記符是 add),助記符的長度設計得較短,這使它們有時會讓人難以理解,例如建立一個新的字 符串,需要使用 Idstr 助記符。

1) IL 以棧為基礎

IL 實際上是完全以棧為基礎的。IL 提供了將變數壓入虛擬執行棧中(稱為載入,這會使棧的成員增加 1)的操作碼,然後,也提供了將棧頂的值拿出來移動到記憶體中(稱為儲存,這會使棧的成員減少 1 )的操作碼。

初始化區域性變數時,必須將它載入入棧,然後再彈岀來賦給本地變數。

因此,初始化完區域性變數之後,棧應當是空的。當使用區域性變數時,必須將其從棧頂彈出,不能直接存取。

載入的助記符中,最常見的是 ldloc/ldc/ldstr,儲存的助記符中,最常見的一個是 stloc。

我們先看一個非常簡單的例子:
class Program
{
    static void Main(string[] args)
    {
        int i = 999;
        int j = 888;
        Console.WriteLine( i + j);
    }
}
該段程式碼對應的未被優化的 IL 程式碼(存在很多 nop 指令,它是空指令):
.class private auto ansi beforefieldinit AssemblyLab.Program extends [mscorlib] System.Object
{
    // Methods
    .method private hidebysig static void Main (string[] args) cil managed
    {
        //Method begins at RVA 0x207c
        // Code size 15 ( Oxf)
        .maxstack 2
        .entrypoint
        .locals init (
            [0] int32 i,
            [1] int32 j
        )
        IL_0000: nop
        IL_0001: ldc.i4 999
        IL_0006: stloc.0
        IL_0007: ldc.i4 888
        IL_000c: stloc.1
        IL_000d: ldloc.0
        IL_000e: ldloc.1
        IL_000f: add
        IL_0010: call void [mscorlib]System.Console::WriteLine(int32)
        IL_0015: nop
        IL_0016: ret
} // end of method Program::Main
.method publie hidebysig specialname rtspecialname instance void .ctor () cil managed
{
        // Method begins at RVA 0x2 097
        // Code size 7 ( 0x7 )
        maxstack 8
        L_0000: ldarg.0
        L_0001: call instance void [mscorlib]System.Obj ect::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor
} // end of class AssemblyLab. Program
Main 方法的大部分程式碼都含有一個助記符。其中,nop 是編譯器在 Debug 模式下插入的方便我們偵錯設定斷點的空操作,所以,這裡我們就忽略 nop。

首先看看方法的定義:

.method private hidebysig static void Main (string[] args) cil managed

IL 指令 .method 指出後面的程式碼為一個方法。IL 特性 private 指出該方法是私有的 (如果一個方法在 C# 中,沒有顯式給出可見性關鍵字,則預設的關鍵字是 private)。

而 hidebysig 的意思是,這個方法會被隱藏,當且僅當其父類別存在同名且同簽名 (相同的輸入和輸出引數個數和型別 ) 的方法 (hide by name and signature)。

後面的 static、void 和 C# 的意思是一樣的。Main 方法接受一個字串陣列作為引數,cil managed 顧名思義是表示該方法為託管的。

下面的這一段程式碼中,我們看到了棧的身影:
.maxstack 2
.entrypoint
.locals init (
    [0] int32 i,
    [1] int32 j
)
由於程式碼僅僅有兩個變數,因此棧的最大空間為2。之後,.entrypoint 指令指示編譯器, 程式碼的入口點在此。

.local init 指令定義兩個 int 型別的變數 i 和 j。

使用類之前,如果沒有宣告建構函式,C# 自動提供一個建構函式,用來呼叫它的所有父類別的建構函式。

Program 類沒有顯式宣告父類別那麼它的父類別就是 System.Object。

Program 的建構函式以 .ctor 作為名稱 ( 這是範例建構函式,靜態建構函式以 .cctor 作為名稱):
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
{
    //Method begins at RVA 0x2097
    // Code size 7 ( 0x7)
    .maxstack 8
    IL_0000: ldarg.0
    IL_0001: call instance void [mscorlib]System.Object::.ctor()
    IL_0006: ret
} // end of method Program:: . ctor
Program類Main方法的IL程式碼主體如下:
IL_0000: nop
IL_0001: ldc.i4 999
IL_0006: stloc.0
IL_0007: ldc.i4 888
IL_000c: stloc.1
IL_000d: ldloc.0
IL_000e: ldloc.1
IL_000f: add
IL_0010: call void [mscotlib]System.Console::WriteLine(int32)
IL_0015: nop
IL_0016: ret
0001 行載入了第一個變數(通過 ldc.i4), 其中,i4 代表 int32 型別,而後面的 999 則是變數的值。

0006 行則把剛剛載入的變數從棧中第 0 個位置彈出,並賦值給第 0 個區域性變數 i。

0007 和 000c 行與前面的邏輯類似,如下圖所示。