Некоторые вещи, чтобы отметить.
Во-первых, это предположительно отладочная сборка, или, по крайней мере, некоторые оптимизации отключены при компиляции. Я ожидаю увидеть здесь следующее:
.method public hidebysig static void Main () cil managed
{
.entrypoint
IL_0000: ret
}
То есть, поскольку эти локальные переменные не используются, я ожидаю, что компилятор просто полностью их пропустит. Этого не будет в отладочной сборке, но это хороший пример того, как может быть значительная разница между тем, что говорит C #, и тем, что говорит IL.
Следующее, на что следует обратить внимание, это структура метода IL. У вас есть массив локальных значений различных типов, определенный блоком .locals
. Как правило, они будут очень близко соответствовать тому, что было в C#, хотя часто будут сделаны сокращения и перестановки.
Наконец, у нас есть набор инструкций, которые воздействуют на эти локальные переменные, любые аргументы и стек, в который он может помещаться, из которого он может извлекаться и с которым будут взаимодействовать различные инструкции.
Следующее, на что следует обратить внимание, это то, что IL, который вы видите здесь, является своего рода сборкой для байт-кода: каждая инструкция здесь имеет однозначное отображение на один или два байта, и каждое значение также потребляет определенное количество байтов. Так, например, stloc V_4
(на самом деле не присутствует в ваших примерах, но мы придем к этому) будет отображаться в 0xFE 0x0E 0x04 0x00
, где 0xFE 0x0E
— это кодировка stloc
, а 0x04 0x00
— кодировка 4
, которая является индексом рассматриваемого локального. Это означает «извлеките значение из вершины стека и сохраните его в 5-м (индекс 4) локальном».
Здесь есть несколько сокращений. Одним из них является .s
"короткая" форма нескольких инструкций (_S
в названии эквивалентно System.Reflection.Emit.OpCode
значению). Это варианты других инструкций, которые принимают однобайтовое значение (со знаком или без знака в зависимости от инструкции), где другая форма принимает двух- или четырехбайтовое значение, как правило, индексы или относительные расстояния для перехода. Таким образом, вместо stloc V_4
у нас может быть stloc.s V_4
, которое всего лишь 0x13 0x4
, и поэтому меньше.
Затем есть несколько вариантов, которые включают конкретное значение в инструкцию. Таким образом, вместо stloc V_0
или stloc.s V_0
мы можем просто использовать stloc.0
, который представляет собой всего лишь один байт 0x0A
.
Это имеет большой смысл, если учесть, что обычно одновременно используется только несколько местных жителей, поэтому использование либо stloc.s
, либо (еще лучше) подобных stloc.0
, stloc.1
и т. д.) дает крошечную экономию, которая в сумме на довольно много.
Но только так. Если бы у нас были, например, stloc.252
, stloc.253
и т. д., то таких инструкций было бы много, и количество байтов, необходимых для каждой инструкции, должно было бы быть больше, и это в целом было бы потерей. Суперкороткие формы слов, связанных с локальными (stloc
, ldloc
) и связанными с аргументами (ldarg
), доходят только до 3
. (Есть starg
и starg.s
, но нет starg.0
и т. д., так как сохранение в аргументах встречается относительно редко). ldc.i4
/ldc.i4.s
(поместить в стек постоянное 32-битное значение со знаком) имеет суперкороткие версии от ldc.i4.0
до ldc.i4.8
, а также lcd.i4.m1
для -1
.
Также стоит отметить, что V_4
вообще не существует в вашем коде. Что бы вы ни исследовали IL, не знали, что вы использовали имя переменной name
, поэтому оно просто использовало V_4
. (Кстати, что вы используете? По большей части я использую ILSpy, и если вы хотите отладить информацию, связанную с файл, который он назвал бы name
соответственно).
Итак, чтобы создать закомментированную неукороченную версию вашего метода с более сопоставимыми именами, мы могли бы написать следующий CIL:
.method public hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init (int32 unassigned,
int32 i,
float64 d,
float64 PI,
string name)
nop // Do Nothing (helps debugger to have some of these around).
ldc.i4 4 // Push number 4 on stack
stloc i // Pop value from stack, put in i (i = 4)
ldloc i // Push value in i on stack
stloc unassigned // Pop value from stack, put in unassigned (unassigned = i)
ldc.r8 12.34 // Push the 64-bit floating value 12.34 onto the stack
stloc d // Push the value on stack in d (d = 12.34)
ldc.r8 3.1415926535897931 // Push the 64-bit floating value 3.1415926535897931 onto the stack.
stloc PI // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI)
ldstr "Ehsan" // Push the string "Ehsan" on stack
stloc name // Pop the value from stack, put in name
ret // return.
}
Это будет вести себя почти так же, как ваш код, но будет немного больше. Поэтому мы заменяем stloc
на stloc.0
…stloc.3
там, где можем, stloc.s
там, где мы не можем их использовать, но можем использовать stloc.s
, и ldc.i4 4
на ldc.i4.4
, и у нас будет более короткий байт-код, который делает то же самое:
.method public hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init (int32 unassigned,
int32 i,
float64 d,
float64 PI,
string name)
nop // Do Nothing (helps debugger to have some of these around).
ldc.i4.4 // Push number 4 on stack
stloc.1 // Pop value from stack, put in i (i = 4)
ldloc.1 // Push value in i on stack
stloc.0 // Pop value from stack, put in unassigned (unassigned = i)
ldc.r8 12.34 // Push the 64-bit floating value 12.34 onto the stack
stloc.2 // Push the value on stack in d (d = 12.34)
ldc.r8 3.1415926535897931 // Push the 64-bit floating value 3.1415926535897931 onto the stack.
stloc.3 // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI)
ldstr "Ehsan" // Push the string "Ehsan" on stack
stloc.s name // Pop the value from stack, put in name
ret // return.
}
И теперь у нас точно такой же код, что и у вашего дизассемблирования, за исключением того, что у нас есть лучшие имена. Помните, что имена не появляются в байтовом коде, поэтому дизассемблер не может работать так же хорошо, как мы.
Ваш вопрос в комментарии действительно должен быть другим вопросом, но он дает возможность добавить что-то важное, что я лишь кратко отметил выше. Давайте рассмотрим:
public static void Maybe(int a, int b)
{
if (a > b)
Console.WriteLine("Greater");
Console.WriteLine("Done");
}
Скомпилируйте в отладке, и вы получите что-то вроде:
.method public hidebysig static
void Maybe (
int32 a,
int32 b
) cil managed
{
.maxstack 2
.locals init (
[0] bool CS$4$0000
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: cgt
IL_0005: ldc.i4.0
IL_0006: ceq
IL_0008: stloc.0
IL_0009: ldloc.0
IL_000a: brtrue.s IL_0017
IL_000c: ldstr "Greater"
IL_0011: call void [mscorlib]System.Console::WriteLine(string)
IL_0016: nop
IL_0017: ldstr "Done"
IL_001c: call void [mscorlib]System.Console::WriteLine(string)
IL_0021: nop
IL_0022: ret
}
Теперь следует отметить, что все метки, такие как IL_0017
и т. д., добавляются к каждой строке на основе индекса инструкции. Это облегчает жизнь дизассемблеру, но на самом деле в этом нет необходимости, если только не выполняется переход на метку. Давайте удалим все метки, на которые не перескочили:
.method public hidebysig static
void Maybe (
int32 a,
int32 b
) cil managed
{
.maxstack 2
.locals init (
[0] bool CS$4$0000
)
nop
ldarg.0
ldarg.1
cgt
ldc.i4.0
ceq
stloc.0
ldloc.0
brtrue.s IL_0017
ldstr "Greater"
call void [mscorlib]System.Console::WriteLine(string)
nop
IL_0017: ldstr "Done"
call void [mscorlib]System.Console::WriteLine(string)
nop
ret
}
Теперь давайте рассмотрим, что делает каждая строка:
.method public hidebysig static
void Maybe (
int32 a,
int32 b
) cil managed
{
.maxstack 2
.locals init (
[0] bool CS$4$0000
)
nop // Do nothing
ldarg.0 // Load first argument (index 0) onto stack.
ldarg.1 // Load second argument (index 1) onto stack.
cgt // Pop two values from stack, push 1 (true) if the first is greater
// than the second, 0 (false) otherwise.
ldc.i4.0 // Push 0 onto stack.
ceq // Pop two values from stack, push 1 (true) if the two are equal,
// 0 (false) otherwise.
stloc.0 // Pop value from stack, store in first local (index 0)
ldloc.0 // Load first local onto stack.
brtrue.s IL_0017 // Pop value from stack. If it's non-zero (true) jump to IL_0017
ldstr "Greater" // Load string "Greater" onto stack.
// Call Console.WriteLine(string)
call void [mscorlib]System.Console::WriteLine(string)
nop // Do nothing
IL_0017: ldstr "Done" // Load string "Done" onto stack.
// Call Console.WriteLine(string)
call void [mscorlib]System.Console::WriteLine(string)
nop // Do nothing
ret // return
}
Давайте запишем это обратно в C# очень буквально шаг за шагом:
public static void Maybe(int a, int b)
{
bool shouldJump = (a > b) == false;
if (shouldJump) goto IL_0017;
Console.WriteLine("Greater");
IL_0017:
Console.WriteLine("Done");
}
Попробуйте это, и вы увидите, что это делает то же самое. Использование goto
связано с тем, что в CIL на самом деле нет ничего похожего на for
или while
или даже блоков, которые мы могли бы поместить после if
или else
, в нем есть только переходы и условные переходы.
Но почему нужно сохранять значение (то, что я назвал shouldJump
в своем переписывании на C#), а не просто воздействовать на него?
Это просто для того, чтобы упростить изучение того, что происходит в каждой точке, если вы отлаживаете. В частности, чтобы отладчик мог остановиться в точке, где a > b
обработано, но еще не выполнено действие, необходимо сохранить либо a > b
, либо его противоположность (a <= b
).
По этой причине сборки отладки, как правило, пишут CIL, который тратит много времени на запись того, что он только что сделал. С релизной сборкой мы получим что-то вроде:
.method public hidebysig static
void Maybe (
int32 a,
int32 b
) cil managed
{
ldarg.0 // Load first argument onto stack
ldarg.1 // Load second argument onto stack
ble.s IL_000e // Pop two values from stack. If the first is
// less than or equal to the second, goto IL_000e:
ldstr "Greater" // Load string "Greater" onto stack.
// Call Console.WriteLine(string)
call void [mscorlib]System.Console::WriteLine(string)
// Load string "Done" onto stack.
IL_000e: ldstr "Done"
// Call Console.WriteLine(string)
call void [mscorlib]System.Console::WriteLine(string)
ret
}
Или сделать аналогичную построчную запись обратно в C#:
public static void Maybe(int a, int b)
{
if (a <= b) goto IL_000e;
Console.WriteLine("Greater");
IL_000e:
Console.WriteLine("Done");
}
Таким образом, вы можете видеть, как релизная сборка более лаконично делает то же самое.
person
Jon Hanna
schedule
04.12.2015
stloc.4
нет, поэтому вместо него нужно использоватьstloc.s local_variable_reference
. я также не понимаю, для чего здесь эти локальные переменные Это ваши локальные переменныеint i; double d; double PI; string name;
. - person user4003407   schedule 04.12.2015stloc.4
? - person Ehsan Sajjad   schedule 04.12.2015stloc
ярлыкаstloc.0
,stloc.1
,stloc.2
иstloc.3
. Для обращения к локальной переменной с индексом 4 или выше вы должны использоватьstloc.s
илиstloc
. - person user4003407   schedule 04.12.2015stloc
в случае индекса больше 4 - person Ehsan Sajjad   schedule 04.12.2015stloc
ldloc
это беспроигрышный вариант. Им это нужно для отладки :D - person leppie   schedule 04.12.2015