Почему метод 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.