Why is IL.Emit method adding additional nop instructions?

711 views Asked by At

I have this code that emits some IL instructions that calls string.IndexOf on a null object:

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);

This is the generated IL code:

.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

As you can see there are three nop instructions before the call instruction.

First I thought about Debug/Release build but this is not compiler generated code, I am emitting raw IL code and expect to see it as is.

So my question is why are there three nop instruction when I hadn't emitted any?

2

There are 2 answers

1
harold On BEST ANSWER

ILGenerator is not very advanced, if you use the Emit(OpCode, Int32) overload it will put the entire int32 in the instruction stream, no matter if the opcode is Ldc_I4 (which actually takes 4 bytes of immediate) or Ldc_I4_S (which doesn't).

So make sure to use the right overload:

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

The lemmas for the opcodes in the documentation specify which overload of Emit is the right one to use.


In the reference source, Emit with an int argument does this:

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

Where PutInteger4 writes four bytes to the byte array in which the IL is built up.

The documentation of Emit says that the extra bytes will be Nop instructions, but that's only if they are actually zero. If the value being passed is "more wrong" (with the high bytes different from zero) then the effects can be worse, from invalid opcodes to operations that subtly corrupt results.

0
Arend On

The documentation of IlGenerator.Emit mentions this:

Remarks If the opcode parameter requires an argument, the caller must ensure that the argument length matches the length of the declared parameter. Otherwise, results will be unpredictable. For example, if the Emit instruction requires a 2-byte operand and the caller supplies a 4-byte operand, the runtime will emit two additional bytes to the instruction stream. These extra bytes will be Nop instructions.

The instruction values are defined in OpCodes.

And the documentation mentions about your instruction

Ldc_I4_S
Pushes the supplied int8 value onto the evaluation stack as an int32, short form.

It seems the three extra nops are coming from the int8 instead of the int32.