.NET CIL манипулирование стека оценки
У меня есть эта последовательность кодов CIL, которые я ввел через использование Mono.Cecil
, Однако измененное приложение.NET C# не будет запущено.
Цель: вручную загрузить и выгрузить значения из стека для отображения в Console.WriteLine
for (int i = 0; i < 3; i++)
{
int z = some value popped manually from stack;
Console.WriteLine(z);
}
Это простая программа main(), которую я модифицировал:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 5
.locals init (
[0] int32 num,
[1] int32 num2)
L_0000: ldc.i4.6 //manually push value 6 to stack
L_0001: ldc.i4.5 //manually push value 5 to stack
L_0002: ldc.i4.4 //manually push value 4 to stack
L_0003: ldc.i4.0 //push int i initial value 0 to stack
L_0004: stloc.0 //pop and store to int i variable to variable num
L_0005: br.s L_0013
L_0007: nop
L_0008: stloc.1 //pop the pushed values 6,5 and 4 to variable num2
L_0009: ldloc.1 //load value of num2 to stack
L_000a: call void [mscorlib]System.Console::WriteLine(int32) //pop value of num2 and print
L_000f: ldloc.0 //load previous value in variable num to stack
L_0010: ldc.i4.1 //load incremental value 1 to stack
L_0011: add //pop and add the top 2 values, result is pushed to stack
L_0012: stloc.0 //store the new result to variable num. (int i)
L_0013: ldloc.0 //push int i variable value to stack
L_0014: ldc.i4.3 //push value 3 to stack as number of times to loop
L_0015: blt.s L_0007 //branch less than (pop and cmp the top 2 values in stack)
L_0017: ret
}
Однако приведенный выше код не может быть запущен. Я пытался изменить blt.s
в clt
а также br_true.s
но это тоже не работает. Кто-нибудь знает, возможно ли достичь моей цели? Благодарю.
РЕДАКТИРОВАТЬ: Согласно ECMA-335, III.1.7.5, может быть ограничение обратной ветвления. Не уверен, что это так.
В частности, если этот однопроходный анализ достигает инструкции, назовите ее местоположением X, которое следует сразу за безусловной ветвью, и где X не является целью более ранней инструкции ветвления, тогда состояние стека оценки в X, несомненно,, не может быть получено из существующей информации. В этом случае CLI требует, чтобы стек оценки в X был пустым.
1 ответ
Ваш IL-код выглядит нормально, но я думаю, что CLR не сможет проверить, не поврежден ли стек после завершения метода. Когда что-то помещается в стек, CLR проверяет, извлечено ли значение из стека.
Поэтому, если вы поместите 3 значения в стек, CLR не сможет проверить, выполняется ли ваш цикл три раза, поэтому CLR не будет знать, есть ли еще значения в стеке при возврате метода.
Очень интересный вопрос Вы пытаетесь использовать стек выполнения IL для хранения очереди произвольных элементов данных. Это вводит необычное условие по сравнению с обычным кодом IL, где правильный баланс стека критически зависит от числа итераций цикла времени выполнения, точно совпадающих с количеством элементов данных времени ILAsm, которые были записаны в IL. Как вы заметили, программа (повторяется здесь) не работает.
(на самом деле, в моей сборке, которая использует link.exe
с /LTCG
компоновщик даже не может произвести сборку, давая " fatal error C1352: Invalid or corrupt MSIL in function
".)
.method public static void ExecStackResidual() // !!! FAILS - BAD EXAMPLE - NO !!!
{
.locals init (int32 i, int32 cur)
ldc.i4.6 // enqueue item -- NO!
ldc.i4.5 // enqueue item -- NO!
ldc.i4.4 // enqueue item -- NO!
ldc.i4.0
stloc i
br _next
_more:
stloc cur // de-queue item -- NO!
ldloc cur
box int32
call void Debug::WriteLine(object)
ldloc i
ldc.i4.1
add
stloc i
_next:
ldloc i
ldc.i4.3
blt _more
ret
}
Проблема не в простом логическом недостатке или отдельной ошибке в вашем коде. Это может быть доказано тем фактом, что комментирование спорных частей следующим образом работает нормально, печатая 3 нуля.
//-- ldc.i4.6 // commented out
//-- ldc.i4.5 // commented out
//-- ldc.i4.4 // commented out
ldc.i4.0
stloc i
br _next
_more:
//-- stloc cur // commented out
ldloc cur
box int32
call void Debug::WriteLine(object)
ldloc i
ldc.i4.1
add
stloc i
_next:
ldloc i
ldc.i4.3
blt _more
ret
Оператор сделал некоторую задержку и обнаружил ECMA-335, III.1.7.5, который, кажется, здесь может иметь значение, поскольку основное различие между рабочим и ошибочным примером заключается в том, что последний экзистенциально требует наличия непустого стека оценки (ака "точка последовательности") в месте _more
и это место действительно, чтобы процитировать спецификации...
"... немедленно следуйте безусловной ветви [здесь,
br _next
], и где [_more
] не является целью более ранней инструкции перехода."
К сожалению, однако, это не совсем полное объяснение, потому что, пока вы удаляете поставленные в очередь элементы сбалансированным образом, который может быть определен статически, стек оценки, очевидно , не должен быть пустым на месте _more
, Это демонстрирует следующий код, который также отлично работает, печатая 3 нуля, несмотря на наличие нескольких элементов в стеке выполнения в уязвимом месте ECMA-335, III.1.7.5 _more
,
ldc.i4.6 // enqueue item -- ok
ldc.i4.5 // enqueue item -- ok
ldc.i4.4 // enqueue item -- ok
ldc.i4.0
stloc i
br _next
_more:
//-- stloc cur // de-queue item -- still commented out
ldloc cur
box int32
call void Debug::WriteLine(object)
ldloc i
ldc.i4.1
add
stloc i
_next:
ldloc i
ldc.i4.3
blt _more
pop // de-queue item -- required
pop // de-queue item -- required
pop // de-queue item -- required
ret
OP также использует терминологию "ограничение обратной ветвления", но неясно, была ли фраза найдена в спецификации или, возможно, в оригинальном вкладе. Возможно, это повлекло за собой появление в спецификации фразы "... более ранняя инструкция ветвления". В любом случае возникает вопрос о том, можно ли избежать ошибки, переставив код таким образом, чтобы не было мест, которые (технически) соответствуют "более раннему" (техническому) ограничению ECMA-335, III.1.7.5.
Схожая идея заключается в том, что "безусловная ветвь" в спецификации означает только br
семейные инструкции. Обойти br
вместо этого мы можем вставить ret
инструкция в теле метода, как показано ниже. Как вы, наверное, догадались, это безрезультатно. Хотя спецификация явно не говорит об этом, она явно предполагает ret
быть включенным в качестве "безусловной ветви". Это здравый смысл, и, соответственно, следующий пример все еще не работает:
// !!! FAILS - BAD EXAMPLE - NO
ldc.i4.6 // enqueue item -- NO!
ldc.i4.5 // enqueue item -- NO!
ldc.i4.4 // enqueue item -- NO!
ldc.i4.0
stloc i
_next:
ldloc i
ldc.i4.3
blt _more
ret
_more:
stloc cur // de-queue item -- NO! -- still follows an "unconditional branch"
ldloc cur
box int32
call void Debug::WriteLine(object)
ldloc i
ldc.i4.1
add
stloc i
br _next
В общем, я не думаю, что вы заставите эту технику работать, потому что она фундаментально требует (а) фактов, которые жестко запрограммированы в IL (т. Е. Количество элементов данных в очереди) должно полностью соответствовать (б) фактам, которые требуют интерпретации во время выполнения (т. е. число итераций цикла).
Я думаю, что более общее резюме проблемы, в отличие от описания ECMA, состоит в том, что все примеры сбоев влекут за собой то, что число элементов в стеке выполнения, испытываемое одной (или более) из инструкций метода, не является фиксированным, вместо этого получение разных значений в разное время при выполнении метода, и это основная ситуация, которая всегда будет строго запрещена, независимо от того, как вы ее достигнете. Мне это кажется более общим нерушимым ограничением.
Например, при обучении _more
в демонстрационном методе - и все в рамках одного вызова - сначала будет 2, затем 1, затем 0 "лишних" элементов в стеке выполнения (обратите внимание, что я вычитал по одному для каждой итерации из того, что вы могли ожидать, и это потому, что я использовал слово " избыток " как раз перед попыткой подчеркнуть тот факт, что в каждой отдельной итерации цикла один элемент должным образом ожидается - и требуется - в месте _more
а именно для предполагаемой операции выгрузки stloc cur
).
Я бы предположил, что для того, чтобы метод IL был действительным, ни одна из его инструкций не могла испытать изменения в преобладающей глубине стека выполнения. Другими словами, должно быть одно и только одно значение глубины стека, которое может быть статически определено и присвоено каждой из инструкций метода. Интуитивно понятно, что в противном случае ситуация, скорее всего, сделает задачу JIT либо крайне неразрешимой, либо, возможно, даже доказуемо невозможной.