.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 либо крайне неразрешимой, либо, возможно, даже доказуемо невозможной.

Другие вопросы по тегам