第二章 IL基础

掌握C#到IL的深度转换 本文还有配套的精品资源,点击获取 简介:《C# to IL》详细阐述了C#语言与.NET中的中间语言(IL)之间的转换关系,揭示了.NET运行时的工作机制。通过理解从C#源代码到IL代码的编译过程,以及IL代码到机器码的JIT编译过程,开发者能够深入理解代码优化、性能分析和异常调试。本书还介绍了C#特性在IL代码中的表示,并探讨了IL在反射机制中的应用。读者将学习到关键的.... 阅读详情

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

      因此,我们将用一个小的C#程序揭露并解释有编译器生成的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);
        }
    }

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个对象,在转换为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() 
{
i 
= 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();
i 
= 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,并用这个值初始化实例变量。

      会以类似的方式来处理局部变量和实例变量,唯一的区别是,它们的初始化指令是不同的。

      指令ldfld不是指令stfld的反转操作。它把一个实例变量的值放在栈上,使之可以被WriteLine函数使用。

C# to IL (IL语言入门书籍--重新上传) C# IL语言入门书籍 重新上传, 这是一本好书, 有兴趣的可以参考. 立即下载

相关推荐

C#反编译工具(IL反编译)

可以反编译C#程序。这个程序可以实现将已编译的程序以C#代码的形式呈现!

MSIL 教程(三):类和异常处理

续上文【翻译】MSIL 教程(二):数组、分支、循环、使用不安全代码和如何调用Win32 API ,本文继续讲解类和异常处理。谨以这三篇译文纪念29年前的今日,那个让母亲今生难以忘记的幸福而又痛苦的日子。类在前面的程序中,我们在Main函数中调用类函数,在本程序中,我们将徐希如何定义类。本程序包含2个类: Class1和SampleClass,Class1带有函数Main,在Main中生成Sa

TonyWang的专栏 785

机器学习预测材料性能:从特征工程到SHAP可解释性分析

机器学习在材料科学中的应用,核心在于将复杂的材料性能预测问题转化为数据驱动的建模任务。其基本原理是通过算法从历史数据中学习特征与目标属性之间的映射关系。这项技术的价值在于能够大幅加速新材料的发现与设计周期,降低传统“试错法”的高昂成本。在工程实践中,它广泛应用于预测如形成能、带隙、弹性模量以及居里温度等关键材料性能。具体到磁性材料领域,居里温度的准确预测对于永磁体和自旋电子器件设计至关重要。本项目深入探讨了如何利用特征工程从材料成分与结构中构建有效描述符,并借助SHAP这一可解释性工具,揭示模型决策背后的物

weixin_27155667的博客 650

.NET Emit 入门教程:第六部分:IL 指令:4:详解 ILGenerator 指令方法:参数存储指令

在 IL 中,除了参数存储指令starg和stloc之外,还有其他一些以 "st" 开头的指令,如stfld和stsfld,它们也用于存储值到特定位置。:将计算堆栈顶部的值存储到方法的参数中,参数索引由后续字节指定。:将计算堆栈顶部的值存储到方法的局部变量中,局部变量索引由后续字节指定。:将计算堆栈顶部的值存储到对象的字段中,字段由元数据标识指定。:用于将值存储到静态字段(static field)中。静态字段是属于类本身而不是类的实例的字段,它们在整个应用程序生命周期内只有一份拷贝,被所有实例共享。

路过秋天 - 理想很美好,现实很残酷,坚持在路上,只因初心仍存 1361

移远EC20模块USB模式切换的底层逻辑与驱动适配探秘

本文深入探讨了移远EC20模块USB模式切换的底层逻辑与驱动适配机制,详细解析了AT命令控制下的USB0与WWAN0模式差异。通过分析USB协议栈、内核驱动框架及网络子系统的交互,提供了模式切换的完整操作流程和常见问题解决方案,助力开发者优化4G通信模块在嵌入式设备中的稳定连接。

cc789的博客 844

Streamlit(九) widgets-input

st.text_input / st.number_input / st.date_input / st.time_input 源码路径:${python}\Lib\site-packages\streamlit\elements\text_widgets.py(number_input.py, time_widgets.py) st.text_input st.text_input(label, value="", max_chars=None, key=None,\

heianduck的博客 3354

由浅入深CIL系列:2.CIL的基本构成+CIL操作码速记表+CIL操作码大全速查

        一、CIL的基本构成          CIL由CIL指令(directive)、CIL特性(attribute)、CIL操作码(opcode)组成。           CIL指令         CIL指令是用于描述.NET程序集总体结构的标记,并且通知CIL编译器如何定义在程序集中用到的命名空间、类、成员的。它以(.)一个点号开头,例如:.namespace、.clas...

weixin_34232744的博客 1136

【脑肿瘤分类与迁移学习技术】Swin UNETR:当 Swin Transformer 遇见 3D 医学图像分割——从黑盒到白盒的逐层拆解

医学图像分割的终极目标是让算法成为医生的可靠副手,而非黑箱替代品。理解 Swin UNETR 的每一层如何流动、每一个窗口如何移位、每一次拼接如何融合,不仅是为了调参,更是为了在临床场景中。

u012133341的博客 55

查看C#的反编译语言IL

IL基础语法 : https://www.jb51.net/article/39635.htm

qq_35433081的博客 1280

【GB28181】wvp-GB28181-pro部署安装教程(Ubuntu平台)

初始环境安装。

废人一枚的博客 5080

MATLAB船舶开尔文尾迹三维仿真建模

开尔文尾迹介绍 Kelvin最早研究舰船在水面运动在水面上形成波纹的原理,主要针对静水中点源运动的尾迹问题,并给出尾迹的张角和波高分布情况。由 Kelvin 的理论,一般情况下假设由一个理想点源来代替舰船,开尔文尾迹由分歧波和横断波组成,分歧波向船的两侧扩散,而横断波主要向船的后方传播,在相遇的地方两波相干形成尖头波,且尖头波位于尾迹角16°到19.5°之间,又因为尖头波波长过短,以至于无法单独的区分出每个尖头波的波前,所有的波前在视觉效果上就连成一条直线表现为亮线,称为开尔文臂,开尔文臂以船身对称,且角度

qq_19512613的博客 3844

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

如果你真的想要理解C#代码,那么最好的方法就是通过理解由C#编译器生成的代码。本章和下面两章将关注于此。 我们将用一个短小的C#程序来揭开IL的神秘面纱,并解释由编译器生成的IL代码。这样,我们就可以“一箭双雕”:首先,我们将揭示IL的奥妙,其次,我们将会更加直观地理解C#编程语言。 我们将首先展示一个.cs文件,并随后通过C#编译器以IL编写一个程序。它的输出和cs文...

weixin_30279671的博客 138

通用抓取————6D位姿估计

这意味着算法在没有物体的先验知识(如 CAD 模型)的情况下,依然能够通过神经网络捕捉物体的外观、形状和几何特征,生成相关的 6D 位姿估计。通过输入物体类别的描述,算法能够推断出该类别物体的位姿和尺度,无需特定物体的 CAD 模型。在这个坐标空间中,物体的位姿和尺度都是已知的,基于这个标准化表示,算法可以对输入图像中的物体进行 6D 位姿和尺寸的估计。○优势: ICP 是一种非常有效的位姿优化算法,特别适合精细调整物体的位姿,确保最终估计的位姿与真实物体的点云或模型高度一致。

具身机器人和通用AI 2380

(翻译) 《C# to IL》第二章 IL基础

(翻译) 《C# to IL》第二章 IL基础  -2-       如果你真的想要理解C#代码,那么最好的方法就是通过理解由C#编译器生成的代码。本章和下面两章将关注于此。      我们将用一个短小的C#程序来揭开IL的神秘面纱,并解释由编译器生成的IL代码。这样,我们就可以“一箭双雕”:首先,我们将揭示IL的奥妙,其次,我们将会更加直观地理解C#编程语言。

zxg131x的专栏 1158

如何使用CPU-Z获取内存条频率信息(编程)

当我们需要获取计算机内存条的频率信息时,可以利用CPU-Z这个工具来实现。CPU-Z是一款专门用于获取计算机硬件信息的工具,它提供了API接口,我们可以通过编程的方式来获取内存条频率信息。你可以从CPU-Z官方网站下载适用于你的操作系统的CPU-Z程序,并从安装目录中找到。函数,我们可以初始化CPU-Z,如果返回值不为0,则表示初始化失败。函数来获取内存条频率信息,如果返回值小于等于0,则表示获取失败。函数来初始化CPU-Z并获取内存条频率信息。最后,我们将获取到的内存条频率信息打印输出,并使用。

ByteProwl的博客 457

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

Net复习笔记:第二章:IL基本指令

.assembly 用于指定编译目标或则外部加载库(.assembly只加载实际用到的目标或者库) .Ctor 指令表示构造函数 .Ver表示引用版本 .publicKeytoKen公钥 .Hash algorithm 表示现实安全性使用的哈希算法 .module程序集指令表明定义的模块的元数据,指当前模块 .imagebase 为影像基地址 .file alignme...

LL0006的博客 224

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Net复习笔记:第二章:IL看值类型和引用类型的一个区别

代码看值类型和引用类型的一个区别 public class MyClass1 { } public struct Mystruct { } public class ILDemo { MyClass1 Mc = new MyClass1(); Mystruct M...

LL0006的博客 204

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

不看后悔!Native AOT 项目必须启用的3个IL trimming黑科技

掌握Native AOT 的大小优化关键技巧,显著减小发布体积。针对C# Native AOT项目,详解3个高效的IL trimming优化手段,适用于Blazor、命令行工具等场景,提升构建效率与部署性能,值得收藏。

LiteTrans的博客 1101

匿名方法闭包内幕揭秘:IL代码级别解读闭包对象生成过程

深入解析匿名方法闭包的底层机制,通过IL代码揭示闭包对象生成过程。涵盖委托捕获、堆栈分配与性能优化等关键细节,适用于异步编程与事件处理场景。理解原理提升编码效率,值得收藏。

LiteCode的博客 395

C#自动属性支持字段探秘(从IL代码看编译器黑科技)

揭秘C# 3自动属性支持字段的底层实现,通过IL代码解析编译器如何自动生成私有字段。涵盖属性机制、适用场景与性能优势,助你深入理解语法糖背后的黑科技,值得收藏。

LogicGap的博客 360

初读CLR Via C# 之 IL、CTS、CLS

本章将对CLR中的IL、CTS和CLS进行一个大概的总结。下章将开始第二章的内容。 IL我们称之为高级汇编语言或者中间语言,在我们编写基于.NET框架下的程序(不论是C#、VB或者J#等)在被编译成本地CPU指令前,都将被编译成IL语言,再有CLR对IL语言进行编译和控制。比如用VB在控制台应用程序写入如下代码: Sub Main() Console.WriteLin...

weixin_30462049的博客 110

协变逆变在C#中为何受限?深入IL代码探寻设计本质

深入解析C#泛型的协变逆变限制,揭示IL底层实现原理与设计考量。涵盖接口、委托中的适用场景,通过ref和out关键字实现安全类型转换,提升代码复用性与灵活性。理解机制本质,规避运行时异常,值得收藏。

SimTrans的博客 858

从零实现C#跨平台方法拦截,深度解读IL注入与动态代理技术

掌握C#跨平台方法拦截核心技术,详解IL注入与动态代理实现原理。提供可运行的C#跨平台拦截器示例,涵盖AOP、日志监控等应用场景,高性能且兼容多环境,值得收藏。

CompiGlow的博客 597

为什么Span能大幅提升性能?深入IL揭示其底层实现原理

掌握C# Span高效处理数据的核心技巧,解决内存分配与拷贝性能瓶颈。适用于网络通信、大数据解析等场景,通过栈上内存操作避免GC压力,实现零分配高性能数据处理。深入IL揭示其原理,值得收藏。

ByteGlow的博客 918
上一篇: 出来混,迟早是要还的
下一篇: 第三章 选择和循环
CsToD
博客等级 码龄19年 91粉丝 35原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值