Почему метод IL.Emit добавляет дополнительные nop-команды?

У меня есть этот код, который испускает некоторые инструкции IL, вызывающие string.IndexOf для null объекта:

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.

Сначала я подумал о сборке Debug/Release, но это не сгенерированный компилятором код, я испускаю необработанный IL-код и ожидаю увидеть его как есть.

Так что мой вопрос, почему есть три nop инструкции, когда я не излучаюсь каким - либо?

Ответ 1

ILGenerator не очень продвинут, если вы используете перегрузку Emit(OpCode, Int32) он поместит весь int32 в поток команд, независимо от того, является ли код операции Ldc_I4 (который фактически занимает 4 байта немедленного) или Ldc_I4_S (что doesn ' т).

Поэтому обязательно используйте правильную перегрузку:

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

В леммах для кодов операций в документации указывается, какая перегрузка Emit является правильной.


В исходном источнике Emit с аргументом int делает следующее:

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 записывает четыре байта в массив байтов, в котором PutInteger4 IL.

Документация Emit говорит, что дополнительные байты будут инструкциями Nop, но это только если они фактически равны нулю. Если переданное значение является "более неправильным" (с большими байтами, отличными от нуля), то эффекты могут быть хуже, от недействительных кодов операций до операций, которые тонко искажают результаты.

Ответ 2

В документации IlGenerator.Emit упоминается следующее:

Заметки Если для параметра opcode требуется аргумент, вызывающий должен убедиться, что длина аргумента соответствует длине объявленного параметра. В противном случае результаты будут непредсказуемыми. Например, если для команды Emit требуется 2-байтовый операнд, а вызывающий объект отправляет 4-байтовый операнд, среда выполнения выдаст два дополнительных байта в поток команд. Эти дополнительные байты будут инструкциями Nop.

Значения инструкций определены в OpCodes.

В документации упоминается ваша инструкция

Ldc_I4_S
Толкает поставляемое значение int8 в стек оценки как int32, короткая форма.

Кажется, что три дополнительных nops исходят от int8 вместо int32.