我在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_4
生成的,这是本地的,但是我对此不太清楚,我在这里也没有得到这些本地人的目的,我的意思是: .locals init (int32 V_0,
float64 V_1,
float64 V_2,
string V_3)
最佳答案
一些注意事项。
首先,这大概是一个调试版本,或者至少在编译中关闭了某些优化功能。我希望在这里看到的是:
.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
)。这些是其他指令的变体,它们采用一个字节的值(带符号或无符号,取决于指令),而另一种形式则采用一个2字节或4字节的值,通常是跳转的索引或相对距离。因此,我们可以使用stloc V_4
代替stloc.s V_4
,而该0x13 0x4
仅为stloc V_0
,因此更小。然后,有些变体在指令中包含特定值。因此,我们可以使用
stloc.s V_0
而不是stloc.0
或0x0A
,它只是单字节stloc.s
。当您考虑一次只使用少数几个本地人是很普遍的,因此使用
stloc.0
或(更好)使用stloc.1
,stloc.252
等之类的话,这很有意义。节省了很少的费用,总计很多。但是只有这么多。如果我们有例如
stloc.253
,stloc
等,那么会有很多这样的指令,并且每条指令所需的字节数必须更多,这总体上是一种损失。与本地相关的(ldloc
,ldarg
)和与参数相关的(3
)的超短格式仅上升到starg
。 (有一个starg.s
和starg.0
,但没有ldc.i4
等,因为存储到参数相对较少)。 ldc.i4.s
/ ldc.i4.0
(将恒定的32位带符号值推入堆栈)具有从ldc.i4.8
到lcd.i4.m1
的超短版本,对于-1
也是V_4
。还值得注意的是,
name
根本不在您的代码中。无论您使用哪个IL进行检查,都不知道您使用了变量名V_4
,所以只使用了name
。 (顺带一提,您在使用什么?我大部分使用ILSpy,如果您调试与该文件关联的信息,它会相应地称为stloc
)。因此,要使用更可比的名称生成方法的带注释的非短版本,我们可以编写以下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.
}
行为将与您的代码差不多,但会更大一些。因此,我们将
stloc.0
替换为stloc.3
…,在我们不能使用的地方使用stloc.s
,但仍然可以使用stloc.s
,并且在ldc.i4 4
中使用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_0017
等)都将添加到每一行。这样可以使反汇编程序的工作更加轻松,但是,除非跳转到标签,否则实际上并不是必需的。让我们删除所有未跳转到的标签:.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");
}
试试看,您会发现它做同样的事情。使用
goto
是因为CIL实际上没有像for
或while
这样的东西,甚至没有我们可以放在if
或else
之后的块,它仅具有跳转和条件跳转。但是,为什么不去存储值(我在C#重写中称为
shouldJump
)而不是仅仅对它起作用呢?如果您要调试的话,只是为了更轻松地检查每个点正在发生的事情。特别是,要使调试器能够在制定出
a > b
但尚未起作用的地方停止,则需要存储a > b
或相反的(a <= b
)。因此,调试版本倾向于编写CIL,而该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");
}
因此,您可以看到发行版构建如何更简洁地执行相同的操作。
关于c# - 再添加一个int变量时生成的不同IL,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/34091444/