VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > Objective-C编程 >
  • c# to IL 第二章 IL基础

制作者:剑锋冷月 单位:无忧统计网,www.51stat.net
 

  -2-

  如果你真的想要理解C#代码,那么最好的方法就是通过理解由C#编译器生成的代码。本章和下面两章将关注于此。

   我们将用一个短小的C#程序来揭开IL的神秘面纱,并解释由编译器生成的IL代码。这样,我们就可以“一箭双雕”:首先,我们将揭示IL的奥妙,其次,我们将会更加直观地理解C#编程语言。

   我们将首先展示一个.cs文件,并随后通过C#编译器以IL编写一个程序。它的输出和cs文件是相同的。输出将会显示IL代码。这会增强我们对C#和IL的理解。好吧,不再啰嗦,这就开始我们的冒险之旅。

  a.cs

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

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

      c:il>csc a.cs

      c:il>ildasm /output=a.il a.exe

  a.il

//  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反汇编器生成的。

 

   在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);
        }
    }

 

   在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中,不能做出类的名称事先已经建立的假设。类的默认名称是我们在调用函数时所在的类。



相关教程