我在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)
Jon Hanna.. 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_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
(推恒定32位有符号值压入堆栈)具有超级短版本从去ldc.i4.0
到ldc.i4.8
和也lcd.i4.m1
对-1
.
值得注意的是,V_4
您的代码中根本不存在.无论你检查什么IL都不知道你使用了变量名,name
所以它只是用它V_4
.(你在使用什么,BTW?我大部分都使用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. }
这将与您的代码一样,但有点大.所以我们更换stloc
与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
,它只是有跳跃和条件跳转.
但是为什么还要存储价值(我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"); }
因此,您可以看到发布版本如何更简洁地执行相同的操作.
有些事情需要注意.
首先,这可能是一个调试版本,或者至少在编译中关闭了某些优化.我期望在这里看到的是:
.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_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
(推恒定32位有符号值压入堆栈)具有超级短版本从去ldc.i4.0
到ldc.i4.8
和也lcd.i4.m1
对-1
.
值得注意的是,V_4
您的代码中根本不存在.无论你检查什么IL都不知道你使用了变量名,name
所以它只是用它V_4
.(你在使用什么,BTW?我大部分都使用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. }
这将与您的代码一样,但有点大.所以我们更换stloc
与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
,它只是有跳跃和条件跳转.
但是为什么还要存储价值(我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"); }
因此,您可以看到发布版本如何更简洁地执行相同的操作.
MSIL经过大量微优化,使存储尽可能小.转到Opcodes类并记下列出的Stloc
说明.它有6个版本,它们都完全相同.
Stloc_0
,Stloc_1
,Stloc_2
和Stloc_3
是最小的,他们只需要一个字节.他们使用的变量号是隐式的,0到3.当然非常常用.
然后是Stloc_S
,它是一个双字节操作码,第二个字节用于编码变量号.当方法具有4个以上的变量时,需要使用此方法.
最后Stloc
,它是一个三字节操作码,使用两个字节来编码变量号.当方法具有超过256个变量时必须使用.希望你永远不会那样做.当你编写一个超过65536个变量的怪物时,你运气不好,这是不受支持的.已经完成了btw,自动生成的代码可以超越这个限制.
很容易看到第二个片段中发生了什么,你添加了unassigned
变量并将局部变量的数量从4增加到5.由于没有Stloc_4
,编译器必须使用Stloc_S
来分配第5个变量.