我刚开始研究IL,我很好奇我从编译器输出中删除多余代码的尝试(如下所示)是否有任何意外的副作用。
关于结果的几个问题:
- 原文中nop操作的目的是什么
- 原文中方法末尾的br.s的目的是什么
- 重写的版本在任何方面都不合适吗
原始C#代码:
class Program {
public static int Main() {
return Add(1, 2);
}
public static int Add(int a, int b) {
return a + b;
}
}
用csc.exe
编译,用ildasm.exe
分解(原件):
.method public hidebysig static int32 Main() cil managed
{
.entrypoint
.maxstack 2
.locals init (int32 V_0)
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: ldc.i4.2
IL_0003: call int32 Program::Add(int32, int32)
IL_0008: stloc.0
IL_0009: br.s IL_000b
IL_000b: ldloc.0
IL_000c: ret
}
.method public hidebysig static int32 Add(int32 a,
int32 b) cil managed
{
.maxstack 2
.locals init (int32 V_0)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: add
IL_0004: stloc.0
IL_0005: br.s IL_0007
IL_0007: ldloc.0
IL_0008: ret
}
重写(产生相同的输出):
.method public hidebysig static int32 Main() cil managed
{
.entrypoint
.maxstack 2
ldc.i4.1
ldc.i4.2
call int32 Program::Add(int32, int32)
ret
}
.method public hidebysig static int32 Add(int32 a, int32 b) cil managed
{
.maxstack 2
ldarg.0
ldarg.1
add
ret
}
您看到的所有"多余"代码都是特定于调试构建的(通常会针对发布构建进行优化),并允许您执行在发布构建中通常无法执行的操作。
调试构建代码允许在调试会话期间最大限度地独立设置断点和更改/检查堆栈值。此外,IL代码应尽可能模仿更高级别的代码,以便每个"原因"one_answers"影响"都可以映射到更高级别代码行。
现在具体针对您的问题:
原文中nop操作的目的是什么?
NOP允许您在未"执行"的位置设置断点。例如,方法、循环或if语句的大括号。在这些不可执行的指令中,在大括号处中断允许您在块开始前修改/检查堆栈(尽管无可否认,您可以通过在块执行的第一行中断而不是在大括号中中断来非常容易地实现这一点,但它仍然允许您独立于在大括号中断)
原始方法末尾的br.s的目的是什么?
查看原始代码,您可能会发现"跳"到下一行而不是让代码自然地"落"到下下一行是荒谬的。但读作:
"在调试构建中,每当方法需要返回时,跳到方法的末尾,从堆栈中读取返回值,然后返回值">
那么它对调试有什么好处呢?
如果代码中有多个返回语句,那么在从堆栈读取返回值之前,这两个语句都将"跳转"到代码的末尾。这只允许您在一个地方(方法的右大括号)放置断点,并在返回值实际返回到调用方法之前修改它。很有帮助,不是吗?
重写的版本在任何方面都不合适吗?
您的代码中没有任何错误。事实上,如果您在发布模式下构建原始版本并检查生成的CIL,您会注意到它与您的版本基本相同。
免责声明:我无论如何都不是IL专家。
-
nop操作的目的是什么?
不久前,programmers.stackexchange.com上就x86 ASM对此进行了大量讨论,请参阅此处:x86程序集中NOP指令和align语句的用途。基本上是一样的。
-
原文中方法末尾的br.s的目的是什么?
这只是方法末尾的一个分支。如果你在这个函数中有多个返回路径,那么查看它会更有意义。目前,编译器已经包含了它,而不是优化它(可能编译器开关会优化它)。
-
重写的版本在任何方面都不合适吗?
我看不见。您刚刚剥离了编译器的大部分工作,这些工作对于这样一个简单的应用程序来说是不需要的。如果您要对此代码进行任何进一步的添加,则需要此处的额外IL来完成其任务。