2

我在 c# 中有这个程序:

using System;

class Program
{
    public static void Main()
    {
    int i = 4;
    double d = 12.34;
    double PI = Math.PI;
    string name = "Ehsan";


    }
}

当我编译它时,以下是编译器为 Main 生成的 IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       30 (0x1e)
  .maxstack  1
  .locals init (int32 V_0,
           float64 V_1,
           float64 V_2,
           string V_3)
  IL_0000:  nop
  IL_0001:  ldc.i4.4
  IL_0002:  stloc.0
  IL_0003:  ldc.r8     12.34
  IL_000c:  stloc.1
  IL_000d:  ldc.r8     3.1415926535897931
  IL_0016:  stloc.2
  IL_0017:  ldstr      "Ehsan"
  IL_001c:  stloc.3
  IL_001d:  ret
} // end of method Program::Main

很好,我明白了,现在如果我添加另一个整数变量,则会生成不同的东西,这是修改后的 c# 代码:

using System;

class Program
{
    public static void Main()
    {
    int unassigned;
    int i = 4;
    unassigned = i;
    double d = 12.34;
        double PI = Math.PI;
    string name = "Ehsan";


    }
}

这是针对上述 c# 代码生成的 IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       33 (0x21)
  .maxstack  1
  .locals init (int32 V_0,
           int32 V_1,
           float64 V_2,
           float64 V_3,
           string V_4)
  IL_0000:  nop
  IL_0001:  ldc.i4.4
  IL_0002:  stloc.1
  IL_0003:  ldloc.1
  IL_0004:  stloc.0
  IL_0005:  ldc.r8     12.34
  IL_000e:  stloc.2
  IL_000f:  ldc.r8     3.1415926535897931
  IL_0018:  stloc.3
  IL_0019:  ldstr      "Ehsan"
  IL_001e:  stloc.s    V_4  // what is happening here in this case
  IL_0020:  ret
} // end of method Program::Main

如果您现在注意到该stloc.s语句是使用V_4which is local 生成的,但我不清楚这一点,我也没有得到这些本地人的目的是什么,我的意思是这些:

 .locals init (int32 V_0,
               float64 V_1,
               float64 V_2,
               string V_3)
4

2 回答 2

5

有些事情要注意。

首先,这可能是一个调试版本,或者至少在编译中关闭了某些优化。我希望在这里看到的是:

.method public hidebysig static void Main () cil managed 
{
  .entrypoint

  IL_0000: ret
}

也就是说,由于没有使用这些本地变量,我希望编译器完全跳过它们。它不会在调试版本中,但这是一个很好的例子,说明 C# 所说的和 IL 所说的之间存在很大差异。

接下来要注意的是 IL 方法的结构。.locals您有一个由块定义的各种类型的局部值数组。这些通常与 C# 的内容非常接近,尽管经常会有捷径和重新安排。

最后,我们有一组指令,它们都作用于这些局部变量、任何参数,以及它可以推送到的堆栈,它可以从中弹出,并且各种指令将在该堆栈上进行交互。

接下来要注意的是,您在这里看到的 IL 是一种字节码组合:这里的每条指令都与一个或两个字节进行一对一的映射,并且每个值也消耗一定数量的字节。因此,例如,stloc V_4(实际上并未出现在您的示例中,但我们会谈到这一点)将映射到0xFE 0x0E 0x04 0x00编码0xFE 0x0E的位置stloc以及0x04 0x00相关4本地的索引。它的意思是“弹出栈顶的值,并将其存储在第5个(索引4)本地”。

现在,这里有一些缩写。其中之一是.s几个指令的“短”形式(_S以等效System.Reflection.Emit.OpCode值的名义)。这些是其他指令的变体,它们采用一个字节的值(有符号或无符号,取决于指令),而另一种形式采用两个或四个字节的值,通常是索引或跳转的相对距离。因此,stloc V_4我们可以拥有stloc.s V_4which is only 0x13 0x4,而 so is 更小。

然后有一些变体在指令中包含特定值。因此,stloc V_0我们stloc.s V_0可以使用stloc.0which is just the single byte而不是 any 0x0A

当您考虑到通常一次只使用少数本地人时,这很有意义,因此使用stloc.s或(更好)之类的stloc.0,stloc.1等)可以节省很少的钱,加起来相当可观很多。

但也只有这么多。如果我们有 egstloc.252等,stloc.253那么就会有很多这样的指令,并且每条指令所需的字节数必须更多,并且总体上会是一个损失。stloc局部相关 ( , ldloc) 和参数相关 ( )的超短形式ldarg仅上升到3. (有一个stargstarg.s但没有starg.0等,因为存储到参数是相对罕见的)。ldc.i4/ ldc.i4.s(将一个恒定的 32 位有符号值压入堆栈)具有超短版本,从ldc.i4.0ldc.i4.8lcd.i4.m1用于-1.

还值得注意的是,V_4您的代码中根本不存在 。无论你用什么检查 IL 都不知道你使用了变量名name,所以它只使用了V_4. (顺便说一句,您在使用什么?我大部分时间都使用ILSpy,如果您要调试与文件关联的信息,它会name相应地调用它)。

因此,要生成具有更多可比名称的方法的注释非缩短版本,我们可以编写以下 CIL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  .maxstack  1
  .locals init (int32 unassigned,
           int32 i,
           float64 d,
           float64 PI,
           string name)
  nop                           // Do Nothing (helps debugger to have some of these around).
  ldc.i4   4                    // Push number 4 on stack
  stloc    i                    // Pop value from stack, put in i (i = 4)
  ldloc    i                    // Push value in i on stack
  stloc    unassigned           // Pop value from stack, put in unassigned (unassigned = i)
  ldc.r8   12.34                // Push the 64-bit floating value 12.34 onto the stack
  stloc    d                    // Push the value on stack in d (d = 12.34)
  ldc.r8   3.1415926535897931   // Push the 64-bit floating value 3.1415926535897931 onto the stack.
  stloc PI                      // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI)
  ldstr    "Ehsan"              // Push the string "Ehsan" on stack
  stloc    name                 // Pop the value from stack, put in name
  ret                           // return.
}

这将与您的代码的行为非常相似,但会更大一些。所以我们stlocstloc.0...<code>stloc.3替换stloc.s我们可以使用的地方,我们不能使用那些但仍然可以使用的地方stloc.s,和ldc.i4 4with ldc.i4.4,我们将有更短的字节码来做同样的事情:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  .maxstack  1
  .locals init (int32 unassigned,
           int32 i,
           float64 d,
           float64 PI,
           string name)
  nop                           // Do Nothing (helps debugger to have some of these around).
  ldc.i4.4                      // Push number 4 on stack
  stloc.1                       // Pop value from stack, put in i (i = 4)
  ldloc.1                       // Push value in i on stack
  stloc.0                       // Pop value from stack, put in unassigned (unassigned = i)
  ldc.r8   12.34                // Push the 64-bit floating value 12.34 onto the stack
  stloc.2                       // Push the value on stack in d (d = 12.34)
  ldc.r8   3.1415926535897931   // Push the 64-bit floating value 3.1415926535897931 onto the stack.
  stloc.3                       // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI)
  ldstr    "Ehsan"              // Push the string "Ehsan" on stack
  stloc.s  name                 // Pop the value from stack, put in name
  ret                           // return.
}

现在我们的代码与您的反汇编代码完全相同,只是我们有了更好的名称。请记住,这些名称不会出现在字节码中,因此反汇编程序无法尽我们所能做好。


您在评论中提出的问题实际上应该是另一个问题,但它提供了一个机会来添加一些我在上面仅简要指出的重要内容。让我们考虑一下:

public static void Maybe(int a, int b)
{
  if (a > b)
    Console.WriteLine("Greater");
  Console.WriteLine("Done");
}

在调试中编译,你最终会得到类似的东西:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  .maxstack 2
  .locals init (
    [0] bool CS$4$0000
  )

  IL_0000: nop
  IL_0001: ldarg.0
  IL_0002: ldarg.1
  IL_0003: cgt
  IL_0005: ldc.i4.0
  IL_0006: ceq
  IL_0008: stloc.0
  IL_0009: ldloc.0
  IL_000a: brtrue.s IL_0017

  IL_000c: ldstr "Greater"
  IL_0011: call void [mscorlib]System.Console::WriteLine(string)
  IL_0016: nop

  IL_0017: ldstr "Done"
  IL_001c: call void [mscorlib]System.Console::WriteLine(string)
  IL_0021: nop
  IL_0022: ret
}

现在要注意的一件事是,所有标签(如IL_0017etc.)都根据指令的索引添加到每一行。这使反汇编程序的工作更轻松,但除非跳转到标签,否则这并不是必需的。让我们去掉所有没有跳转到的标签:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  .maxstack 2
  .locals init (
    [0] bool CS$4$0000
  )

  nop
  ldarg.0
  ldarg.1
  cgt
  ldc.i4.0
  ceq
  stloc.0
  ldloc.0
  brtrue.s IL_0017

  ldstr "Greater"
  call void [mscorlib]System.Console::WriteLine(string)
  nop

  IL_0017: ldstr "Done"
  call void [mscorlib]System.Console::WriteLine(string)
  nop
  ret
}

现在,让我们考虑每一行的作用:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  .maxstack 2
  .locals init (
    [0] bool CS$4$0000
  )

  nop                   // Do nothing
  ldarg.0               // Load first argument (index 0) onto stack.
  ldarg.1               // Load second argument (index 1) onto stack.
  cgt                   // Pop two values from stack, push 1 (true) if the first is greater
                        // than the second, 0 (false) otherwise.
  ldc.i4.0              // Push 0 onto stack.
  ceq                   // Pop two values from stack, push 1 (true) if the two are equal,
                        // 0 (false) otherwise.
  stloc.0               // Pop value from stack, store in first local (index 0)
  ldloc.0               // Load first local onto stack.
  brtrue.s IL_0017      // Pop value from stack. If it's non-zero (true) jump to IL_0017

  ldstr "Greater"       // Load string "Greater" onto stack.

                        // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
  nop                   // Do nothing

  IL_0017: ldstr "Done" // Load string "Done" onto stack.
                        // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
  nop                   // Do nothing
  ret                   // return
}

让我们以非常字面的方式一步一步将其写回 C#:

public static void Maybe(int a, int b)
{
  bool shouldJump = (a > b) == false;
  if (shouldJump) goto IL_0017;
  Console.WriteLine("Greater");
IL_0017:
  Console.WriteLine("Done");
}

试试看,你会看到它做同样的事情。使用 ofgoto是因为 CIL 没有任何类似fororwhile甚至可以放在ifor之后的块else,它只有跳转和条件跳转。

但是为什么要费心存储值(我shouldJump在 C# 重写中调用的)而不是仅仅对它采取行动呢?

如果您正在调试,这只是为了更容易检查每个点发生的情况。特别是,为了使调试器能够在a > b已解决但尚未采取行动的点处停止,则需要存储其中一个a > b或其相反的 ( )。a <= b

出于这个原因,调试版本倾向于编写 CIL,它会花费大量时间来记录它刚刚所做的事情。通过发布版本,我们会得到更多类似的东西:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  ldarg.0           // Load first argument onto stack
  ldarg.1           // Load second argument onto stack
  ble.s IL_000e     // Pop two values from stack. If the first is
                    // less than or equal to the second, goto IL_000e: 
  ldstr "Greater"   // Load string "Greater" onto stack.
                    // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
                    // Load string "Done" onto stack.
  IL_000e: ldstr "Done"
                    // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
  ret
}

或者做一个类似的逐行写回C#:

public static void Maybe(int a, int b)
{
  if (a <= b) goto IL_000e;
  Console.WriteLine("Greater");
IL_000e:
  Console.WriteLine("Done");
}

所以你可以看到发布版本是如何更简洁地做同样的事情的。

于 2015-12-04T17:06:09.010 回答
5

MSIL 进行了大量的微优化,以使存储尽可能小。转到Opcodes 类并注意列出的Stloc说明。它有 6 个版本,它们都做完全相同的事情。

Stloc_0, Stloc_1,Stloc_2Stloc_3是最小的,它们只占用一个字节。他们使用的变量数字是隐含的,从 0 到 3。当然非常常用。

然后是Stloc_S,它是一个双字节的操作码,第二个字节用于编码变量号。当一个方法有超过 4 个变量时,需要使用这个。

最后是Stloc,它是一个三字节的操作码,使用两个字节对变量号进行编码。当方法具有超过 256 个变量时必须使用。希望你永远不会那样做。当您编写一个具有超过 65536 个变量的怪物时,您不走运,这是不受支持的。顺便说一句,自动生成的代码可以突破这个限制。

很容易看出第二个片段中发生了什么,您添加了unassigned变量并将局部变量的数量从 4 个增加到 5 个。由于没有Stloc_4,编译器必须使用Stloc_S来分配第 5 个变量。

于 2015-12-04T18:12:58.597 回答