《C# to IL》第二章 IL基礎

若是你真的想要理解C#代碼,那麼最好的方法就是經過理解由C#編譯器生成的代碼。本章和下面兩章將關注於此。程序員

      咱們將用一個短小的C#程序來揭開IL的神祕面紗,並解釋由編譯器生成的IL代碼。這樣,咱們就能夠「一舉兩得」:首先,咱們將揭示IL的奧妙,其次,咱們將會更加直觀地理解C#編程語言。編程

      咱們將首先展現一個.cs文件,並隨後經過C#編譯器以IL編寫一個程序。它的輸出和cs文件是相同的。輸出將會顯示IL代碼。這會加強咱們對C#和IL的理解。好吧,再也不囉嗦,這就開始咱們的冒險之旅。sass

a.cs編程語言

複製代碼

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

    
public static void abc()
    {
        System.Console.WriteLine(
"bye");
    }
}
ide

複製代碼

      c:\il>csc a.cs函數

      c:\il>ildasm /output=a.il a.exe學習

a.ilthis

複製代碼
//   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 "
{
  
//  --- The following custom attribute is added automatically, do not uncomment -------
   //   .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::
//
 .ctor(bool, bool) = ( 01 00 00 01 00 00 ) 
   .hash   algorithm   0x00008004
  
.ver   0 : 0 : 0 : 0
}
.module  a.exe
//  MVID: {3C938660-2A02-11D5-9089-9752D1D64E03}
.class   private   auto   ansi  zzz
       
extends  [mscorlib]System.Object
{
  
.method   public   hidebysig  static  void  Main() il  managed
  {
    
.entrypoint
    
//  Code size       16 (0x10)
     .maxstack    8
    
IL_0000:    ldstr        " hi "
    
IL_0005:    call         void  [mscorlib]System.Console::WriteLine( class  System.String)
    
IL_000a:    call         void  zzz::abc()
    
IL_000f:    ret
  } 
//  end of method zzz::Main
 
  
.method   public   hidebysig  static  void  abc() il  managed
  {
    
//  Code size       11 (0xb)
     .maxstack    8
    
IL_0000:    ldstr        " bye "
    
IL_0005:    call         void  [mscorlib]System.Console::WriteLine( class  System.String)
    
IL_000a:    ret
  } 
//  end of method zzz::abc
 
  
.method   public   hidebysig   specialname   rtspecialname  
          
instance   void  .ctor() il  managed
  {
    
//  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 zzz::.ctor
 
//  end of class zzz
 
// *********** DISASSEMBLY COMPLETE ***********************
複製代碼

      上面的代碼是由IL反彙編器生成的。spa

      在exe文件上執行ildasm後,咱們觀察一下該程序所生成的IL代碼。先排除一部分代碼——它們對咱們理解IL是沒有任何幫助的——包括一些註釋、僞指令和函數。剩下的IL代碼,則和原始的代碼儘量的保持同樣。設計

Edited a.il

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

      c:\il>ilasm a.il

Output

      hi

      bye

      經過研究IL代碼自己來掌握IL這門技術的好處是,咱們從C#編譯器那裏學習到如何編寫至關好的IL代碼。找不到比C#編譯器更權威的「大師」來教導咱們關於IL的知識。

      建立靜態函數abc的規則,與建立其它函數是相同的,諸如Main或vijay。由於abc是一個靜態函數,因此咱們必須在.method僞指令中使用修飾符static。

      當咱們想調用一個函數時,必須依次提供如下信息:

  • 返回的數據類型
  • 類的名稱
  • 被調用的函數名稱
  • 參數的數據類型

      一樣的規則還適用於當咱們調用基類的.ctor函數的時候。在函數名稱的前面寫出類的名稱是必須的。在IL中,不能作出類的名稱事先已經創建的假設。類的默認名稱是咱們在調用函數時所在的類。

      所以,上面的程序首先使用WriteLine函數來顯示hi,並隨後調用靜態函數abc。這個函數還使用了WriteLine函數來顯示bye。

a.cs

複製代碼
     class  zzz
    {
        
public   static   void  Main()
        {
            System.Console.WriteLine(
" hi " );
        }
        
static  zzz()
        {
            System.Console.WriteLine(
" bye " );
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
ldstr   " hi "
call   void  System.Console::WriteLine( class  System.String)
ret
}
.method   private   hidebysig   specialname   rtspecialname  static  void  .cctor() il  managed
{
ldstr   " bye "
call   void  [mscorlib]System.Console::WriteLine( class  System.String)
ret
}
}
複製代碼

Output

      bye

      hi

      靜態構造函數老是在任何其它代碼執行以前被調用。在C#中,靜態函數只是一個和類具備相同名稱的函數。在IL中,函數名稱改變爲.cctor。所以,你可能注意到在先前的例子中,咱們使用了一個名爲ctor的函數(而不須要事先定義)。

      不管咱們什麼時候調用一個無構造函數的類時,都會自動建立一個沒有參數的構造函數。這個自動生成的構造函數具備給定的名稱.ctor。這一點,應該加強咱們做爲C#程序員的能力,由於咱們如今正處在一個較好的位置上來理解那些深刻實質的東西。

      靜態函數會被首先調用,以後,帶有entrypoint僞指令的函數會被調用。

a.cs

複製代碼
     class  zzz
    {
        
public   static   void  Main()
        {
            System.Console.WriteLine(
" hi " );
            
new  zzz();
        }
        zzz()
        {
            System.Console.WriteLine(
" bye " );
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
ldstr   " hi "
call   void  System.Console::WriteLine( class  System.String)
newobj   instance   void  zzz::.ctor()
pop
ret
}
.method   private   hidebysig   specialname   rtspecialname   instance   void  .ctor() il  managed
{
ldarg.0
call   instance   void  [mscorlib]System.Object::.ctor()
ldstr   " bye "
call   void  [mscorlib]System.Console::WriteLine( class  System.String)
ret
}
}
複製代碼

Output

      hi

      bye

      在C#中的關鍵字new,被轉換爲彙編器指令newobj。這就爲IL不是一門低級彙編語言而且還能夠在內存中建立對象提供了證據。指令newobj在內 存中建立了一個新的對象。即便在IL中,咱們也不會知道new或newobj真正作了些什麼。這就證明了IL並非另外一門高級語言,而是被設計爲其它現代 語言都可以編譯爲IL這樣一種方式。

      使用newobj的規則和調用一個函數的規則是相同的。函數名稱的完整原型是必需的。在這個例子中,咱們調用了無參數的構造函數,從而函數.ctor會被調用。在構造函數中,WriteLine函數會被調用。

      正如咱們先前承諾的,這裏,咱們將要解釋指令ldarg.0。不管什麼時候建立一個對象——一個類的實例,都會包括兩個基本的實體:

  • 函數
  • 字段或變量,如data

      當一個函數被調用時,它並不知道也不關心誰調用了它或它在哪裏被調用。它從棧上檢索它的全部參數。沒有必要在內存中有一個函數的兩份複製。這是由於,若是一個類包括了1兆的代碼,那麼每當咱們對其進行new操做時,都會佔據額外的1兆內存。

      當new被首次調用時,會爲代碼和變量分配內存。可是以後,在new上的每一次調用,只會爲變量分配新的內存。從而,若是咱們有類的5個實例,那麼就只有代碼的一份複製,可是會有變量的5份獨立的複製。

      每一個非靜態的或實例函數都傳遞了一個句柄,它表示調用這個函數的對象的變量位置。這個句柄被稱爲this指針。this由ldarg.0表示。這個句柄老是被傳遞爲每一個實例函數的第1個參數。因爲它老是被默認傳遞,因此在函數的參數列表中沒有說起。

      全部的操做都發生在棧上。pop指令移出棧頂的任何元素。在這個例子中,咱們使用它來移除一個zzz的實例,它是經過newobj指令被放置在棧頂的。

a.cs

複製代碼
     class  zzz
    {
        
public   static   void  Main()
        {
            System.Console.WriteLine(
" hi " );
            
new  zzz();
        }
        zzz()
        {
            System.Console.WriteLine(
" bye " );
        }
        
static  zzz()
        {
            System.Console.WriteLine(
" byes " );
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
ldstr   " hi "
call   void  System.Console::WriteLine( class  System.String)
newobj   instance   void  zzz::.ctor()
pop
ret
}
.method   private   hidebysig   specialname   rtspecialname   instance   void  .ctor() il  managed
{
ldarg.0
call   instance   void  [mscorlib]System.Object::.ctor()
ldstr   " bye "
call   void  [mscorlib]System.Console::WriteLine( class  System.String)
ret
}
.method   private   hidebysig   specialname   rtspecialname  static  void  .cctor() il  managed
{
ldstr   " byes "
call   void  [mscorlib]System.Console::WriteLine( class  System.String)
ret
}
}
複製代碼

Output

      byes

      hi

      bye

      儘管實例構造函數只在new以後被調用,但靜態構造函數老是會首先被調用。IL會強制這個執行的順序。對基類構造函數的調用不是必須的。所以,爲了節省本書的篇幅,咱們不會展現程序的全部代碼。

      在某些狀況中,若是咱們不包括構造函數的代碼,那麼程序就不會工做。只有在這些狀況中,構造函數的代碼纔會被包括進來。靜態構造函數不會調用基類的構造函數,this也不會被傳遞到靜態函數中。

a.cs

複製代碼
     class  zzz
    {
        
public   static   void  Main()
        {
            
int  i  =   6 ;
            
long  j  =   7 ;
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object 
{
    .method   public   hidebysig  static  void  vijay() il  managed  
    {
        .entrypoint
        .locals  ( int32  V_0, int64  V_1)
        ldc.i4.6
        stloc.0
        ldc.i4.7
        conv.i8
        stloc.1
        ret  
    } 
}
複製代碼

      在C#程序中,咱們在Main函數中建立了2個變量i和j。它們是局部變量,是在棧上建立的。請注意,在轉換到IL的過程當中,變量的名稱會被丟棄。

      在IL中,變量經過locals僞指令來建立,它會把自身的名稱分配給變量,以V_0和V_1等等做爲開始。數據類型也會被修改——從int修改成 int32以及從long修改成int64。C#中的基本類型都是別名。它們都會被轉換爲IL所能理解的數據類型。

      當前的任務是將變量i初始化爲值6。這個值必須位於磁盤上或計算棧上。作這個事情的指令是ldc.i4.value。i4就是從內存中獲取4個字節。

      在上面語法中提到的value,是必需要放置到棧上的常量。在值6被放置到棧上以後,咱們如今須要將變量i初始化爲這個值。變量i會被重命名爲V_0,它是locals指令中的第一個變量。

      指令stloc.0獲取位於棧頂的值,也就是6,並將變量V_0初始化爲這個值。初始化一個變量的過程是至關複雜的。

      第2個ldc指令將7這個值複製到棧上。在32位的機器上,內存只能以32字節的塊(Chunk)來分配。一樣,在64位的機器上,內存是以64字節的塊來分配的。

      數值7被存儲爲一個常量並只須要4個字節,可是long須要8個字節。所以,咱們須要把4字節轉換爲8字節。指令conv.i8就是用於這個意圖的。它把 一個8字節數字放在棧上。只有在這麼作以後,咱們才能使用stloc.1來初始化第2個變量V_1爲值7。從而會有stloc.1指令。

      所以,ldc系列用於放置一個常量數字到棧上,而stloc用於從棧上獲取一個值,並將一個變量初始化爲這個值。

a.cs

複製代碼
     class  zzz
    {
        
static   int  i  =   6 ;
        
public   long  j  =   7 ;
        
public   static   void  Main()
        {
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
    .field   private  static  int32  i
    .field   public   int64  j
    .method   public   hidebysig  static  void  vijay() il  managed
    {
        .entrypoint
        ret
    }
    .method   public   hidebysig   specialname   rtspecialname  static  void  .cctor() il  managed
    {
        ldc.i4.6
        stsfld   int32  zzz::i
        ret
    }
    .method   public   hidebysig   specialname   rtspecialname   instance   void  .ctor() il  managed
    {
        ldarg.0
        ldc.i4.7
        conv.i8
        stfld        int64  zzz::j
        ldarg.0
        call   instance   void  [mscorlib]System.Object::.ctor()
        ret
    }
}
複製代碼

      歷經艱難以後,如今,你終於看到了成功,並明白咱們爲何想要你首先閱讀本書了。

      讓咱們理解上面的代碼,每次一個字段。咱們建立了一個靜態變量i,並將其初始化爲值6。因爲沒有爲變量i指定一個訪問修飾符,默認值就是private。C#中的修飾符static也適用於IL中的變量。

      實際的操做如今纔開始。變量須要被分配一個初始值。這個值必須只能在靜態改造函數中分配,由於變量是靜態的。咱們使用ldc來把值6放到棧上。注意到這裏並無使用到locals指令。

      爲了初始化i,咱們使用了stsfld指令,用於在棧頂尋找值。stsfld指令的下一個參數是字節數量,它必須從棧上取得,用來初始化靜態變量。在這個例子中,指定的字節數量是4。

      變量名稱位於類的名稱以前。這與局部變量的語法正好相反。

      對於實例變量j,因爲它的訪問修飾符是C#中的public,轉換到IL,它的訪問修飾符保留爲public。因爲它是一個實例變量,因此它的值會在實例變量中初始化。這裏使用到的指令是stfld而不是stsfld。這裏咱們須要棧上的8個字節。

      剩下的代碼和從前保持一致。所以,咱們能夠看到stloc指令被用於初始化局部變量,而stfld指令則用於初始化字段。

a.cs

複製代碼
     class  zzz
    {
        
static   int  i  =   6 ;
        
public   long  j  =   7 ;
        
public   static   void  Main()
        {
            
new  zzz();
        }
        
static  zzz()
        {
            System.Console.WriteLine(
" zzzs " );
        }
        zzz()
        {
            System.Console.WriteLine(
" zzzi " );
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.field   private  static  int32  i
.field   public   int64  j
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
newobj   instance   void  zzz::.ctor()
pop
ret
}
.method   public   hidebysig   specialname   rtspecialname  static  void  .cctor() il  managed
{
ldc.i4.6
stsfld   int32  zzz::i
ldstr   " zzzs "
call   void  [mscorlib]System.Console::WriteLine( class  System.String)
ret
}
.method   public   hidebysig   specialname   rtspecialname   instance   void  .ctor() il  managed
{
ldarg.0
ldc.i4.7
conv.i8
stfld   int64  zzz::j
ldarg.0
call    instance   void  [mscorlib]System.Object::.ctor()
ldstr   " zzzi "
call   void  [mscorlib]System.Console::WriteLine( class  System.String)
ret
}
}
複製代碼

Output

      zzzs

      zzzi

      上面這個例子的主要意圖是,驗證首先初始化變量仍是首先調用包含在構造函數中的代碼。IL輸出很是清晰地證明了——首先初始化全部的變量,而後再調用構造函數中的代碼。

      你可能還會注意到,基類的構造函數會被首先執行,隨後,也只能是隨後,在構造函數中編寫的代碼纔會被調用。

      這種收穫確定會加強你對C#和IL的理解。

a.cs

複製代碼
     class  zzz
    {
        
public   static   void  Main()
        {
            System.Console.WriteLine(
10 );
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
ldc.i4.s   10
call   void  [mscorlib]System.Console::WriteLine( int32 )
ret
}
}
複製代碼

Output

      10

      經過重載WriteLine函數,咱們可以打印出一個數字而不是字符串。

      首先,咱們使用ldc語句把值10放到棧上。仔細觀察,如今這個指令是ldc.i4.s,那麼值就是10。任何指令都在內存中獲取4個字節,可是當以.s結尾時則只獲取1個字節。

      隨後,C#編譯器調用正確的WriteLine函數的重載版本,它從棧上接受一個int32值。

      這相似於打印出來的字符串:

a.cs

複製代碼
     class  zzz
    {
        
public   static   void  Main()
        {
            System.Console.WriteLine(
" {0} " 20 );
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
.locals  ( int32  V_0)
ldstr   " {0} "
ldc.i4.s   20
stloc.0
ldloca.s  V_0
box  [mscorlib]System.Int32
call   void  [mscorlib]System.Console::WriteLine( class  System.String, class  System.Object)
ret
}
}
複製代碼

Output

      20

      如今咱們將研究如何在屏幕上打印一個數字。

      WriteLine函數接受一個字符串,以後是可變數量的對象。{0}打印逗號後面的第1個對象。即便在C#代碼中沒有任何變量,在轉換爲IL代碼時,就會建立一個int32類型的變量。

      使用ldstr指令把字符串{0}加載到棧上。而後,咱們把做爲參數傳遞到WriteLine函數的數字放到棧上。爲了作到這樣,咱們使用 ldc.i4.s來加載常量值到棧上。在這以後,咱們使用stloc.0指令將V_0初始化爲20,而後使用ldloca.s加載局部變量的地址到棧上。

      這裏咱們面臨的主要難題是,WriteLine函數接受一個字符串(做爲一個參數),以後是一個對象,做爲下一個參數。在這個例子中,變量是值類型而不是引用類型。

      int32是一個值類型變量,可是WriteLine函數想要一個「合格的」引用類型的對象。

      咱們如何解決把一個值類型轉換爲一個引用類型所面臨的困難選擇呢?正如前面提到的那樣,咱們使用指令ldloca.s來加載局部變量V_0的地址到棧上,咱們的棧包括一個字符串,位於值類型變量V_0的前面。

      接下來,咱們調用box指令。引用類型和值類型是.NET中僅有的兩種變量類型。裝箱是.NET用來將一個值類型變量轉換爲引用類型變量的方法。box指 令獲取一個未裝箱的或值類型的變量,並將它轉換爲一個裝箱的或引用類型的變量。box指令須要棧上的值類型的地址,並在堆上爲其相匹配的引用類型分配空 間。

      堆是一塊內存區域,用來存儲引用類型。棧上的值會隨着函數的結束而消失,可是堆會在至關長的一段時間是有效的。

      一旦這個空間被分配了,box指令就會初始化引用對象的實例字段。而後,在堆中分配這個新建立的對象的內存位置到棧上。box指令須要棧上的局部變量的一塊內存位置。

      存儲在棧上的常量是沒有物理地址的。所以,變量V_0會被建立,以提供內存位置。

      堆上的這個裝箱版本相似於咱們所熟悉的引用類型變量。它實際上不具備任何類型,從而看起來像System.Object。爲了訪問它的特定值,咱們須要首先對它進行拆箱。WriteLine會在內部作這件事情。

      被裝箱的參數的數據類型,必須和那些地址位於棧上的變量的數據類型相同。咱們隨後將解釋這些細節。

a.cs

複製代碼
     class  zzz
    {
        
static   int  i  =   10 ;
        
public   static   void  Main()
        {
            System.Console.WriteLine(
" {0} " , i);
        }
    }
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.field   private  static  int32  i
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
ldstr   " {0} "
ldsflda   int32  zzz::i
box   [mscorlib]System.Int32
call   void  [mscorlib]System.Console::WriteLine( class  System.String,  class  System.Object)
ret
}
.method   public   hidebysig   specialname   rtspecialname  static  void  .cctor() il  managed
{
ldc.i4.s   10
stsfld   int32  zzz::i
ret
}
}
複製代碼

Output

      10

      上面的代碼用來顯示靜態變量的值。.cctor函數將靜態變量初始化爲值10。而後,字符串{0}會被存儲到棧上。

      ldsldfa函數加載棧上某個數據類型的靜態變量的地址。而後,和往常同樣,進行裝箱。上面給出的關於box功能的解釋,在這裏也是相關的。

      IL中的靜態變量的工做方式和實例變量相同。惟一的區別是它們有本身的一套指令。像box這樣的指令須要棧上的一塊內存位置,這在靜態變量和實例變量之間是沒有區別的。

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.field   private  static  int32  i
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
ldstr   " {0} "
ldsflda   int32  zzz::i
box   [mscorlib]System.Int32
call   void  [mscorlib]System.Console::WriteLine( class  System.String,  class  System.Object)
ret
}
.method   public   hidebysig   specialname   rtspecialname   instance   void  .ctor() il  managed  {
ldarg.0
call   instance   void  [mscorlib]System.Object::.ctor()
ret

}
複製代碼

Output

      0

      在前面的程序中,惟一的變化是咱們移除了靜態構造函數。全部的靜態變量和實例變量都會被初始化爲ZERO。所以。IL不會生成任何錯誤。在內部,甚至在靜態函數被調用以前,字段i就會被分配一個初始值ZERO。

a.cs

複製代碼
class  zzz
{
public   static   void  Main()
{
int  i  =   10 ;
System.Console.WriteLine(i);
}
}
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
.locals  ( int32  V_0)
ldc.i4.s     10
stloc.0
ldloc.0
call   void  [mscorlib]System.Console::WriteLine( int32 )
ret
}
}
複製代碼

Output

      10

      咱們將局部變量i初始化爲值0。這是不能在構造函數中完成的,由於變量i尚未在棧上被建立。而後,使用stloc.0來分配值10到V_0。以後,使用ldloc.0來把變量V_0放到棧上,從而它對於WriteLine函數是可用的。

      以後,Writeline函數在屏幕上顯示這個值。字段和本地變量具備相似的行爲,只有一點不一樣——它們使用不一樣的指令。

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
.locals  ( int32  V_0)
ldloc.0
call         void  [mscorlib]System.Console::WriteLine( int32 )
ret
}
}
複製代碼

Output

      51380288

      全部的局部變量都必須被初始化,不然,編譯器就會生成一個莫名其妙的錯誤信息。這裏,即便咱們註釋了ldc和stloc指令,也不會有錯誤在運行時生成。然而,會顯示一個很是巨大的數字。

      變量V_0沒有被初始化爲任何值,它是在棧上建立的,幷包括在內存位置上分配給它的任何可用的值。在你我機器上的輸出會有很大不一樣。

      在相似的狀況中,C#編譯器將丟給你一個錯誤,而且不容許你進一步繼續下去,由於變量尚未被初始化。另外一方面,IL是一個「怪胎」。它的要求是很寬鬆 的。它生成很是少的錯誤或在源代碼上進行很是少的健康檢查。但也存在缺點,就是說,程序員在使用IL時不得不更加當心和盡職盡責。

a.cs

複製代碼
class  zzz 
{
static   int  i;
public   static   void  Main() 
{
=   10 ;
System.Console.WriteLine(
" {0} " ,i);
}
}
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.field   private  static  int32  i
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
ldc.i4.s     10
stsfld       int32  zzz::i
ldstr        " {0} "
ldsflda      int32  zzz::i
box   [mscorlib]System.Int32
call   void  [mscorlib]System.Console::WriteLine( class  System.String, class  System.Object)
ret
}
}
複製代碼

Output

      10

      在上面的例子中,一個靜態變量會在函數中被初始化,而不是在它建立的時候,就像前面看到的那樣。函數vijay會調用存在於靜態函數中的代碼。

      上面給出的處理是初始化靜態變量或實例變量的惟一方式:

a.cs

複製代碼
class  zzz
{
public   static   void  Main()
{
zzz a 
=   new  zzz();
a.abc(
10 );
}
void  abc( int  i) 
{
System.Console.WriteLine(
" {0} " ,i);

}
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
.locals  ( class  zzz V_0)
newobj   instance   void  zzz::.ctor()
stloc.0
ldloc.0
ldc.i4.s   10
call   instance   void  zzz::abc( int32 )
ret
}
.method   private   hidebysig   instance   void  abc( int32  i) il  managed
{
ldstr   " {0} "
ldarga.s  i
box  [mscorlib]System.Int32
call   void  [mscorlib]System.Console::WriteLine( class  System.String, class  System.Object)
ret
}
}
複製代碼

Output

      10

      上面的程序示範了關於咱們能如何調用具備一個參數的函數。把參數放在棧上的規則相似於WriteLine函數的規則。

      如今讓咱們理解關於一個函數是如何從棧上接受參數的。咱們經過在函數聲明中聲明數據類型和參數名稱來做爲開始。這就像在C#中工做同樣。

      接下來,咱們使用指令ldarga.s加載參數i的地址到棧上。隨後box將把這個對象的值類型轉換爲對象類型,最後WriteLine函數使用這些值在屏幕上顯示輸出。

a.cs

複製代碼
class  zzz
{
public   static   void  Main()
{
zzz a 
=   new  zzz();
a.abc(
10 );
}
void  abc( object  i)
{
System.Console.WriteLine(
" {0} " ,i);

}
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
.locals  ( class  zzz V_0, int32  V_1)
newobj   instance   void  zzz::.ctor()
stloc.0
ldloc.0
ldc.i4.s     10
stloc.1
ldloca.s    V_1
box  [mscorlib]System.Int32
call   instance   void  zzz::abc( class  System.Object)
ret
}
.method   private   hidebysig   instance   void  abc( class  System.Object i) il  managed  
{
ldstr   " {0} "
ldarg.1
call   void  [mscorlib]System.Console::WriteLine( class  System.String, class  System.Object)
ret

}
複製代碼

Output

      10

      在上面的例子中,咱們將一個整數轉換爲一個對象,由於WriteLine函數須要這個數據類型的參數。

      接受這種轉換的惟一方法是使用box指令。box指令將一個整數轉換爲一個對象。

      在函數abc中,咱們接受一個System.Object,並使用ldarg指令而不是ldarga。這樣作的緣由是,咱們須要該參數的值和它的地址。爲了把參數的值放到棧上,須要一個新的指令。

      所以,IL使用它們本身的一套指令來處理局部變量、字段和參數。

a.cs

複製代碼
class  zzz
{
public   static   void  Main()
{
int  i;
zzz a 
=   new  zzz();
=  zzz.abc();
System.Console.WriteLine(i);
}
static   int  abc()
{
return   20 ;
}
}
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
.locals  ( int32  V_0, class  zzz V_1)
newobj   instance   void  zzz::.ctor()
stloc.1
call   int32  zzz::abc()
stloc.0
ldloc.0
call   void  [mscorlib]System.Console::WriteLine( int32 )
ret
}
.method   private   hidebysig  static  int32  abc() il  managed
{
.locals  ( int32  V_0)
ldc.i4.s     20
ret
}
}
複製代碼

Output

      20

      函數返回值。這裏,靜態函數abc被調用。咱們從函數的簽名中瞭解到它返回一個整數。返回值會被存儲到棧上。

      所以,stloc.1指令從棧上獲取值並把它放在局部變量V_1中,在這個特定的例子中,它是函數的返回值。

      newobj也像一個函數。它返回一個對象——在咱們的例子中,它是類zzz的一個實例——並把它放到棧上。

      stloc指令被屢次重複使用來初始化咱們的局部變量。只是想再次提醒你一下,ldloc是這個過程的反轉。

      函數使用ldc指令把一個值放到棧上,並隨後使用ret指令終止執行。

      所以,棧扮演着雙重角色。

  • 用來把值放到棧上。
  • 它接受函數的返回值。

a.cs

複製代碼
class  zzz
{
int  i;
public   static   void  Main()
{
zzz a 
=   new  zzz();
a.i 
=  zzz.abc();
System.Console.WriteLine(a.i);
}
static   int  abc()
{
return   20 ;
}
}
複製代碼

a.il

複製代碼
.assembly  mukhi {}
.class   private   auto   ansi  zzz  extends  System.Object
{
.field   private   int32  i
.method   public   hidebysig  static  void  vijay() il  managed
{
.entrypoint
.locals  ( class  zzz V_0)
newobj       instance   void  zzz::.ctor()
stloc.0
ldloc.0
call   int32  zzz::abc()
stfld   int32  zzz::i
ldloc.0
ldfld   int32  zzz::i
call    void  [mscorlib]System.Console::WriteLine( int32 )
ret
}
.method   private   hidebysig  static  int32  abc() il  managed
{
.locals  ( int32  V_0)
ldc.i4.s     20
ret
}
}
複製代碼

Output

      20

      在上面的例子中,惟一的改變是函數abc的返回值被存儲在一個實例變量中。

  • stloc把棧上的值分配到一個局部變量中。
  • 另外一方面,ldloc把局部變量的值放到棧上。

      不理解的是——爲何這個看上去像zzz的對象必須被再次放在棧上,尤爲abc既然是一個靜態函數而不是實例函數。提示你一下,棧上的this指針是不會被傳遞到靜態函數的。

      此後,函數abc會被調用,它把值20放在了棧上。指令stfld接受棧上的值20,並用這個值初始化實例變量。

      IL彙編器會以相似的方式來處理局部變量和實例變量,惟一的區別是,它們的初始化指令是不一樣的。

      指令ldfld不是指令stfld的反轉操做。它把一個實例變量的值放在棧上,使之能夠被WriteLine函數使用。

相關文章
相關標籤/搜索