为什么 IL.Emit 方法要添加额外的 nop 指令?



我有这段代码,它发出一些IL指令,在null对象上调用string.IndexOf

MethodBuilder methodBuilder = typeBuilder.DefineMethod(
"Foo",
MethodAttributes.Public,
typeof(void), Array.Empty<Type>());
var methodInfo = typeof(string).GetMethod("IndexOf", new[] {typeof(char)});
ILGenerator ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldnull);
ilGenerator.Emit(OpCodes.Ldc_I4_S, 120);
ilGenerator.Emit(OpCodes.Call, methodInfo);
ilGenerator.Emit(OpCodes.Ret);

这是生成的IL代码:

.method public instance int32  Foo() cil managed
{
// Code size       12 (0xc)
.maxstack  2
IL_0000:  ldnull
IL_0001:  ldc.i4.s   120
IL_0003:  nop
IL_0004:  nop
IL_0005:  nop
IL_0006:  call       instance int32 [mscorlib]System.String::IndexOf(char)
IL_000b:  ret
} // end of method MyDynamicType::Foo

如您所见,在call指令之前有三个nop说明。

首先,我想到了调试/发布构建,但这不是编译器生成的代码,我正在发出原始 IL 代码,并希望按原样查看它。

所以我的问题是,为什么在我没有发出任何指令的情况下有三个nop指令?

ILGenerator不是很高级,如果你使用Emit(OpCode, Int32)重载,它会将整个int32放在指令流中,无论操作码是Ldc_I4(实际上需要 4 个字节的即时)还是Ldc_I4_S(不需要)。

因此,请确保使用正确的重载:

ilGenerator.Emit(OpCodes.Ldc_I4_S, (byte)120);

文档中操作码的引理指定了正确使用的Emit重载。


在引用源中,带有int参数的Emit执行以下操作:

public virtual void Emit(OpCode opcode, int arg) 
{
// Puts opcode onto the stream of instructions followed by arg
EnsureCapacity(7);
InternalEmit(opcode);
PutInteger4(arg);
}

其中,PutInteger4将四个字节写入构建 IL 的字节数组。

Emit的文档说额外的字节将是指令Nop,但这只是当它们实际上为零时。如果传递的值"更错误"(高字节与零不同),则效果可能会更糟,从无效的操作码到微妙地破坏结果的操作。

IlGenerator.Emit 的文档提到了这一点:

说明 如果操作码参数需要参数,则调用方必须 确保参数长度与声明的长度匹配 参数。否则,结果将是不可预测的。例如,如果 发出指令需要一个 2 字节的操作数,调用方提供 一个 4 字节的操作数,运行时将向 指令流。这些额外的字节将是 Nop 指令。

指令值在操作码中定义。

文档提到了您的说明


Ldc_I4_S 将提供的int8值作为int32短格式推送到评估堆栈上。

似乎三个额外的nops来自int8而不是int32。

最新更新