程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> 《C# to IL》第一章 IL入門

《C# to IL》第一章 IL入門

編輯:關於C#

1-

我們用C#、VB.NET語言編寫的代碼最終都會被編譯成程序集或IL。因此用VB.NET編寫的代碼 可以在C#中修改,隨後在COBOL中使用。因此,理解IL是非常有必要的。

一旦熟悉了IL,理解.NET 技術就不會有障礙了,因為所有的.NET語言都會編譯為IL。IL是一門中性語言。IL是先發明的,隨後才有 了C#、VB.NET等語言。

我們將在一個短而精辟的程序中展示IL。我們還假設讀者至少熟悉一 門.NET語言。

a.il

.method void vijay()
{

}

隨後,我們用IL編寫了一個非常短小的IL程序——它顯然是不能工作的,並 將它命名為a.il。那麼我們怎麼才能把它編譯為一個可執行程序呢?不需要為此而焦急,Microsoft提供 了一個ilasm程序,它的唯一任務就是從IL文件中創建可執行文件。

在允許這個命令之前,要確保 你的變量路徑被設置為framework中的bin子目錄。如果不是,請輸入命令如下:

set path=c:\progra~1\microsoft.net\frameworksdk\bin;%PATH%

現在,我們使用如下命令:

c:\il>ilasm /nologo /quiet a.il

這樣做會生成下面的錯誤:

Source file is ANSI

Error: No entry point declared for executable

***** FAILURE *****

將來,我們將不會顯示由ilasm生成的輸出的第一行 和最後一行。我們還將移除非空白行之間的空白行。

在IL中,允許我們使用句點.作為一行的開始 ,這是一條指令,要求編譯器執行某個功能,如創建一個函數或類,等等。任何開始於句點的語句都是一 條實際俄編譯器指令。

.method表示創建一個名為vijay的函數(或方法),並且這個函數返回 void,即它不返回任何值。因為缺少較好的命名法則,函數名稱vijay顯得很隨意。

匯編器顯然理 解不了這個程序,從而會顯示“no entry point”的消息。這個錯誤信息的生成是因為IL文件 能夠包括無數的函數,而匯編器無法區分哪個會被首先被執行。

在IL中,首先被執行的函數被稱 為進入點(entrypoint)函數。在C#中,這個函數是Main。函數的語法是,名稱之後是一對圓括號()。 函數代碼的開始和結束用花括號{}來表示。

a.il

.method void vijay()
{
      .entrypoint
}

c:\il>ilasm /nologo /quiet a.il

Source file is ANSI

Creating PE file

Emitting members:

Global Methods: 1;

Writing PE file

Operation completed successfully

現在不會生成任何錯誤了 。偽指令(directive)entrypoint表示程序執行必須開始於這個函數。在這個例子中,我們不得不使用 這個偽指令,雖然事實上這個程序只有一個函數。當在DOS提示符中給出dir命令後,我們看到有3個文件 會被創建。a.exe是一個可執行文件,現在可以執行它來看到程序的輸出。

C:\il>a

Exception occurred: System.BadImageFormatException: Exception from HRESULT: 0x8007000B. Failed to load C:\IL\A.EXE.

當我們試圖執行上面的程序時,我們的運氣似乎不太好,因為會生成上面的運行時錯誤。一個可能的 原因是,這個函數是不完整的,每個函數都應當具有一個“函數結束”指令在函數體中。我們 匆忙之中顯然沒有注意到這個事實。

a.il

.method void vijay()
{
      .entrypoint
      ret
}

“函數結束”指令被稱為ret。前面所有的函數都必須以這個指令作為結束。

Output

Exception occurred: System.BadImageFormatException: Exception from HRESULT: 0x8007000B. Failed to load C:\IL\A.EXE.

在執行這個程序時,我們再次得到了相同的錯誤。這次我們的問題又在哪裡呢?

a.il

.assembly mukhi {}
.method void vijay()
{
      .entrypoint
      ret
}

錯誤在於我們忘記在名稱後面使用必不可少的偽指令assembly。我們將其合成在上面的 代碼中,並在一對空的花括號之後使用了名稱mukhi。這個程序集偽指令用於給出程序的名稱。它又被稱 為一個部署單元。

上面的代碼是可以匯編而沒有任何錯誤的最小的程序,雖然它在執行時並沒有 做什麼有用的事情。它沒有任何名為Main的函數。它只有一個帶有entrypoint偽指令的函數vijay。現在 匯編這個程序並運行而根本不會有任何錯誤。

在.NET中,程序集的概念是極其重要的,應該對其 有徹底的認識。我們將在本章後半部分使用這個偽指令。

a.il

.assembly mukhi  {}
.method void vijay()
{
      .entrypoint
      ret
}
.method void vijay1()
{
      .entrypoint
      ret
}

Error

***** FAILURE *****

上面錯誤信息的原因是,上面的程序有2個函 數,vijay和vijay1,每個函數都包括了.entrypoint偽指令。正如前面提到的那樣,這個指令指定了關於 那個函數會被首先執行。

因此,在功能上,它類似於C#中的Main函數。當C#代碼被轉換為IL代碼 時,在Main函數中包含的代碼會被轉換為IL中的函數中並包括.entrypoint偽指令。例如,如果在COBOL程 序中執行的第一個函數被稱為abc,那麼在IL中生成的代碼就會在這個函數中插入.entrypoint偽指令。

在常規的程序語言中,首先被執行的函數必須有一個特定的名稱,例如Main,但是在IL中,只需 要一個.entrypoint偽指令。因此,因為一個程序只能由一個開始點,所以在IL代碼中只允許一個函數包 括.entrypoint偽指令。

迫切地看到,沒有生成任何錯誤消息編號或說明,使得調試這個錯誤非常 困難。

a.il

.assembly mukhi {}
.method void vijay()
{
      ret
      .entrypoint
}

.entrypoint偽指令需要被定位為函數中的第一個指令或最後一個指令。它僅出現在函數 體中,從而將它的狀態宣布為第一個被執行的函數。偽指令不是程序集指令,甚至可以被放置在任何ret 指令之後。提醒你一下,ret表示函數代碼的結束。

a.il

.assembly mukhi {}
.method void vijay()
{
      .entrypoint
      call void System.Console::WriteLine()
      ret
}

我們可能有一個用C#、VB.NET編寫的函數,但是在IL中執行這個函數的機制是相同的。 如下所示:

我們必須使用匯編指令調用。調用指令之後,按照給定的順序,為以下詳細內容:

函數的返回類型(void)

命名空間(System)

類 (Console)

函數名稱 (WriteLine())

函數被調用但不會生成任何輸出。因為,我們傳遞一個參數到WriteLine函數中。

a.il

.assembly mukhi {}
.method void vijay()
{
      .entrypoint
      call void System.Console::WriteLine(class System.String)
      ret
}

上面的代碼有一處“閃光點”。當一個函數在IL中被調用時,除了它的返回 類型之外,被傳遞的參數的數據類型,也必須被指定。我們將Writeline設置為——希望得到 一個System.String類型作為參數,但是由於沒有字符串被傳遞到這個函數中,所以它會生成一個運行時 錯誤。

因此,在調用一個函數時,在IL和其他程序語言之間有一個明顯的區別。在IL中,當我們 調用一個函數,我們必須指定關於該函數我們所知道的任何內容,包括它的返回類型和它的參數的數據類 型。通過在運行期間進行恰當的檢查,保證了匯編器能夠在語法上驗證代碼的有效性。

現在我們 將看到如何將參數傳遞到一個函數中。

a.il

.assembly mukhi {}
.method void vijay()
{
      .entrypoint
      ldstr "hell"
      call void System.Console::WriteLine(class System.String)
      ret
}

Output

hell

匯編器指令ldstr把字符串放到棧上。Ldstr的名稱是文本 "load a string on the stack"的縮寫版本。棧是一塊內存區域,它用來傳遞參數到函數中。 所有的函數從棧上接收它們的參數。因此,像ldstr這樣的指令是必不可少的。

a.il

.assembly mukhi {}
.method public hidebysig static void vijay()il managed
{
      .entrypoint
      ldstr "hell"
      call void System.Console::WriteLine(class System.String)
      ret
}

Output

hell

我們在方法vijay上添加了一些特性。接下來我們將逐個講解 它們。

public:被稱為可訪問特性,它決定了都有誰可以訪問一個方法。public意味著這個方法 可以被程序的其他任何部分所訪問。

hidebysig:類可以從其它多個類中派生。hidebysig特性保 證了父類中的函數在具有相同名稱或簽名的派生類中會被隱藏。在這個例子中,它保證了如果函數vijay 出現在基類中,那麼它在派生類中就是不可見的。

static:方法可以是靜態的或非靜態的。靜態 方法屬於一個類而不屬於一個實例。因此, 就像我們只有一個單獨的類,我們不能擁有一個靜態函數的 多份復制。靜態函數可以在哪裡創建是沒有約束的。帶有entrypoint指令的函數必須是靜態的。靜態函數 必須具有相關聯的實體或者源代碼,並且使用類型名稱而不是實例名稱來引用它們。

il managed: 由於它的復雜性質,我們將關於這個特性的解釋延後。當時機成熟時,它的功能將會被解釋清楚。

上面涉及的特性並沒有修改函數的輸出。 稍後,你將明白為什麼我們要提供這些特性的解釋。

無論何時我們用C#語言編寫一個程序,我們首先在類的名稱前指定關鍵字class,隨後,我們將源 代碼封閉在一對花括號內。示范如下:

a.cs

class zzz
{

}

讓我們引進稱為class的IL指令:

a.il

.assembly mukhi {}
.class zzz
{
      .method public hidebysig static void vijay()il managed
      {
            .entrypoint
            ldstr "hell"
            call void System.Console::WriteLine(class System.String)
            ret
      }
}

注意到,匯編器輸出中的改變: Class 1 Methods: 1;

Output

hell

偽指令.class之後是類的名稱。它在IL中是可選的,讓我們通過添 加一些類的特性來增強這個類的功能。

a.il

.assembly mukhi {}
.class private auto ansi zzz
{
      .method public hidebysig static void vijay()il managed
      {
            .entrypoint
            ldstr "hell"
            call void System.Console::WriteLine(class System.String)
            ret
      }
}

Output

hell

我們添加了 3個特性到類的偽指令中。

private:這 表示了對類的成員的訪問被約束為只能在當前類中。

auto:這表示類在內存中的布局將只由運行 時來決定,而不是由我們的程序決定。

ansi:源代碼通常被劃分為兩個主要的類別:托管代碼和 非托管代碼。

以諸如C語言編寫的代碼被稱為非托管代碼或不可信任的代碼。我們需要一個特性來 處理非托管代碼和托管代碼之間的互操作。例如,當我們想要在托管和非托管代碼之間轉移字符串時,這 個特性會被使用到。

如果我們跨越托管代碼的邊界並鑽進非托管代碼的領域,那麼一個字符串 ——由2字節Unicode字符組成的數組,將會被轉換為一個ANSI字符串——由1字節 ANSI字符組成的數組;反之亦然。修飾符ansi用於消除托管和非托管代碼之間的轉換。

a.il

.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
      .method public hidebysig static void vijay()il managed
      {
            .entrypoint
            ldstr "hell"
            call void System.Console::WriteLine(class System.String)
            ret
      }
}

Output

hell

類zzz從System.Object中派生。在.NET中,為了定義類型的 一致性,所有的類型最終都派生於System.Object。因此,所有的對象都有一個共同的基類Object。在IL 中,類從其它類中派生,與C++、C#和Java的表現方式相同,

a.il

.module  aa.exe
.subsystem 3
.corflags 1

.assembly extern mscorlib
{
      .originator = (03 68 91 16 D3 A4 AE 33 )
      .hash = (52 44 F8 C9 55 1F 54 3F 97 D7 AB AD E2 DF 1D E0
            F2 9D 4F BC )
      .ver 1:0:2204:21
}

.assembly a as "a"
{
      .hash algorithm 0x00008004
      .ver 0:0:0:0
}

.class private auto ansi zzz extends System.Object
{
      .method public hidebysig static void vijay() il managed
      {
            .entrypoint
            ldstr "hell"
            call void System.Console::WriteLine(class System.String)
            ret
}

      .method public hidebysig specialname rtspecialname instance void .ctor()  il managed
      {
            .maxstack 8
            ldstr "hell1"
            call void System.Console::WriteLine(class System.String)
            ldarg.0
            call instance void [mscorlib]System.Object::.ctor()
      ret
      }
}

Output

hell

你一定想知道為什麼我們會編寫出這麼難看的程序。在迷霧 驅散之前你需要保持耐心,所有的一切就要開始有意義了。我們將逐個解釋新引進的函數和特性。

.ctor: 我們引進了一個新的函數.ctor,它調用了WriteLine函數來顯示hell1,但是它沒有被調 用。.ctor涉及到了構造函數。

rtspecialname: 這個特性會告訴運行時——函數的名 稱是特殊的,它會以一種特殊的方式被對待。

specialname: 這個特性會提示編譯器和工具 ——函數是特殊的。運行時可能選擇忽略這個特性。

instance: 一個常規的函數會被 一個實例函數調用。這樣一個函數與一個對象關聯,不同於靜態方法,後者關聯到一個類。

在合 適的時候,為函數選擇特定名稱的原因會變得明朗。

ldarg.0: 這是一個匯編器指令,它加載this 指針或第0個參數的地址到執行棧上。我們隨後將詳細解釋ldarg.0。

mscorlib: 在上面的程序中 ,函數.ctor會被基類System.Object調用。通常,函數的名稱以包括代碼的庫的名稱作為前綴。這個庫的 名稱被放置在方括號中。在這個例子中,它是可選的——因為mscorlib.dll是默認的庫,並且 它包括了.NET所需要的大部分類。

.maxstack: 這個偽指令指定了在一個方法被調用時,能夠出現 在計算棧上的元素的最大數量。

.module: 所有的IL文件必須是一個邏輯實體的一部分,或它們的 組合體,我們將這些實體稱為模塊(module)。文件被添加到使用了.module偽指令的模塊中。模塊的名 稱可能被規定為aa.exe,但是可執行文件的名稱和前面保持一樣,即a.exe。

.subsystem: 這個指 令用於指定可執行體運行在什麼操作系統上。這是另一種指定可執行體所代表的種類的方式。一些數字值 和它們對應的操作系統如下所示:

2 - A Windows Character 子系統。

3 - A Windows GUI 子系統。

5 – 像OS/2這樣的老系統。

.corsflags: 這個偽指令用於指定對於64 位計算機唯一的標志。值1表示它是從il中創建的可執行文件,而值64表示一個庫。

.assembly: 在前面,我們曾經簡單涉及過一個名為.assembly的指令。現在讓我們進行深入的研究。

無論我們 創建了什麼,都是一個稱為清單(manifest)的實體的一部分。.assembly偽指令標注了一個清單的開始 位置。在層次上,模塊是清單最小的實體。.assembly偽指令指定了這個模塊屬於哪個程序集。模塊只能 包括一個單獨的.assembly偽指令。

對於exe文件,這個偽指令的存在是必須的,但是,對於.dll 中的模塊,則是可選的。這是因為,我們需要使用這個偽指令來創建一個程序集。這是.NET的基本需要。 程序集偽指令包括了其它偽指令。

.hash: 散列計算是一門在計算機世界中通用的技術,這裡有大 量使用到的散列方法或算法。這個偽指令用於散列計算。

.ver: .ver:偽指令包括了4個由冒號分 割的數字。按照下面給定的順序,它們代表了下面的信息:

主版本編號

次版本編號

內部版本號

修訂版本號

extern: 如果有涉及到其它程序集的需求,就要使用到 extern偽指令。.NET核心類的代碼位於mscorlib.dll中。除了這個dll之外,當我們的程序需要涉及到大 量其它的dll時,extern偽指令就要排上用場了。

originator: 在轉移到解釋上面程序的本質和意 義之前,這是我們要研究的最後一個偽指令。這個偽指令揭示了創建該dll的標識。它包括了dll的所有者 公鑰的8個字節。它顯然是一個散列值。

讓我們以一種不同的方式一步一步地溫習到目前為止我們 所做的事情。

(a)我們開始於一個我們能夠編寫的最簡單的程序。這個程序被稱為a.cs,並包括 了下面的代碼:

a.cs

class zzz
{
      public static void Main()
      {
            System.Console.WriteLine("hi");
      }
}

(b)然後我們使用下面的命令運行C#編譯器。

>csc a.cs

因此,會創建名為a.exe的exe文件。

(c)在可執行體中,我們運行一個名為 ildasm的程序,它是由Microsoft提供的:

>ildasm /out=a.txt a.exe

這就創建了一個txt文件,具有下面的內容:

a.txt

//  Microsoft (R) .NET  Framework IL Disassembler.  Version 1.0.2204.21
//  Copyright (C) Microsoft Corp. 1998-2000

// VTableFixup Directory:
// No data.
.subsystem 0x00000003
.corflags 0x00000001
.assembly extern mscorlib
{
  .originator = (03 68 91 16 D3 A4 AE 33 )        // .h..3
  .hash = (52 44 F8 C9 55 1F 54 3F 97 D7 AB AD E2 DF 1D E0
           F2 9D 4F BC )                        //  RD..U.T?O.
  .ver 1:0:2204:21
}
.assembly a as "a"
{
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module aa.exe
// MVID: {89CFAD60-F5BD-11D4-A55A-96B5C7D61E7B}
.class private auto ansi zzz
       extends System.Object
{
  .method public hidebysig static void vijay() il managed
  {
    .entrypoint
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldstr      "hell"
    IL_0005:  call       void System.Console::WriteLine(class System.String)
    IL_000a:  ret
  } // end of method zzz::vijay

  .method public hidebysig specialname rtspecialname 
          instance void .ctor() il managed
  {
    // Code size       17 (0x11)
    .maxstack  8
    IL_0000:  ldstr      "hell"
    IL_0005:  call       void System.Console::WriteLine(class System.String)
    IL_000a:  ldarg.0
    IL_000b:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0010:  ret
  } // end of method zzz::.ctor

} // end of class zzz

//*********** DISASSEMBLY COMPLETE ***********************

當我們閱讀上面的 文件時,你將明白它的所有內容都已經在前面解釋過了。我們開始於一個簡單的C#程序,然後將它編譯到 一個可執行文件中。在正常的環境下,它將被轉換為機器語言或這個程序運行在所在的計算機/微處理器 的匯編程序。一旦創建了可執行體,我們就使用ildasm來反匯編它。反匯編輸出被保存到一個新的文件 a.txt中。這個文件可能被命名為a.il,然後我們可以通過對其運行ilasm反過來再次創建這個可執行體。

讓我們看一下最小的VB.NET程序。我們將它命名為one.vb,而它的源代碼如下所示:

one.vb

Public Module modmain
    Sub Main()
        System.Console.WriteLine("hell")
    End Sub
End Module

在編寫完上述的代碼後,我們運行Visual.Net編譯器vbc如下:

>vbc one.vb

這就產生了文件one.exe。

下面,我們執行ildasm 如下所示:

>ildasm /out=a.txt one.exe

這就生成了下面的文件 a.txt:

a.txt

//  Microsoft (R) .NET Framework IL Disassembler.   Version 1.0.2204.21
//  Copyright (C) Microsoft Corp. 1998-2000

// VTableFixup Directory:
// No data.
.subsystem 0x00000003
.corflags 0x00000001
.assembly extern mscorlib
{
  .originator = (03 68 91 16 D3 A4 AE 33 )              // .h..3
  .hash = (52 44 F8 C9 55 1F 54 3F 97 D7 AB AD E2 DF 1D E0
  F2 9D 4F BC )                                  //  RD..U.T?.O.
  .ver 1:0:2204:21
}
.assembly extern Microsoft.VisualBasic
{
  .originator = (03 68 91 16 D3 A4 AE 33 )      // .h..3
  .hash = (5B 42 1F D2 5E 1A 42 83 F5 90 B2 29 9F 35 A1 BE   
           E5 5E 0D E4 )                  // [B..^.B.).5.
  .ver 1:0:0:0
}
.assembly one as "one"
{
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}
.module one.exe
// MVID: {1ED19820-F5C2-11D4-A55A-96B5C7D61E7B}
.class public auto ansi modmain
       extends [mscorlib]System.Object
{
  .custom instance void [Microsoft.VisualBasic] Microsoft.VisualBasic.Globals/Globals$StandardModuleAttribute::.ctor() = ( 01 00 00 00  )
  .method public static void Main() il managed
  {
    // Code size       11 (0xb)
    .maxstack  1
    .locals init (class System.Object[] V_0)
    IL_0000:  ldstr      "hell"
    IL_0005:  call       void [mscorlib]System.Console::WriteLine(class  System.String)
    IL_000a:  ret
  } // end of method modmain::Main

} // end of class modmain

.class private auto ansi _vbProject
       extends [mscorlib]System.Object
{
  .custom instance void [Microsoft.VisualBasic] Microsoft.VisualBasic.Globals/Globals$StandardModuleAttribute::.ctor() = ( 01 00 00 00  )
.method public static void  _main(class System.String[] _s) il managed
  {
    .entrypoint
    // Code size       6 (0x6)
    .maxstack  8
    IL_0000:  call       void modmain::Main()
    IL_0005:  ret
  } // end of method _vbProject::_main
} // end of class _vbProject
//*********** DISASSEMBLY COMPLETE ***********************

你將驚訝地看到由 兩個不同的編譯器所生成的輸出幾乎是相同的。我向你展示了這個示例用以證實——語言的無 關性,最終,源代碼將會被轉換為IL代碼。無論我們使用VB.NET或C#,都會調用相同的WriteLine函數。

因此,程序語言間的不同現在是表面上的問題。無休止的爭論那個語言是最優的是沒有意義的。 從而,IL使得程序員可以自由使用他們所選擇的語言。

讓我們揭開上面給出的代碼的神秘面紗。

每個VB.NET程序都需要被包括在一個模塊中。我們稱之為modmain。Visual Basic中的所有模塊都 是以關鍵字End結束的,從而我們會看到End Module。這是VB在語法上不區別於C#的地方 ——C#不理解模塊是什麼。

在VB.NET中,函數被稱為子程序。我們需要子程序來標注 程序執行的開始位置。這個子程序被稱為Main。

VB.NET代碼不僅關聯到mscorlib.dll,還使用了 文件Microsoft.VisualBasic。

在IL中會創建一個名為_vbProject的類,因為在VB中類的名稱不是 必須的。

稱為_main的函數是子函數的開始,因為它具有entrypoint偽指令。它的名稱前面有一個 下劃線。這些名稱是由VB編譯器選擇用來生成IL代碼的。

這個函數會傳遞一個字符串數組作為參 數。它具有一個自定義偽指令來處理元數據的概念。

接下來,我們具有這個函數的完整原型,以 一系列可選的字節作為終結。這些字節是元數據規范中的一部分。

模塊modmain被轉換為一個具有 相同名稱的類。和之前一樣,這個類還具有相同的偽指令.custom和一個Main函數。該函數使用了名 為.locals的偽指令在棧上創建一個只能在這個方法中使用變量。這個變量只存在於方法執行期間,當方 法停止運行時,它就會“消亡”。

字段還存儲在內存中,但是需要更長的時間來為它 們分配內存。關鍵字init表示在創建期間,這些變量應該被初始化為它們的默認值。默認值依賴於變量的 類型。數值總是被初始化為值ZERO。關鍵字init之後是這些變量的數據類型和它的名稱。

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