Что такое эквивалент C / C ++ для этого ассемблерного кода? - PullRequest
2 голосов
/ 10 апреля 2019

Я пытаюсь понять этот ассемблерный код, кто-нибудь может мне помочь с написанием его на языке C / C ++?

это код:

 loc_1C1D40:             ; unsigned int
 push    5
 call    ??_U@YAPAXI@Z   ; operator new[](uint)
 mov     [ebp+esi*4+var_14], eax
 add     esp, 4
 inc     esi
 mov     byte ptr [eax+4], 0
 cmp     esi, 4
 jl      short loc_1C1D40

Как я понимаю, первые две строки просто вызывают "оператор new", который возвращает адрес в eax. после этого «mov [ebp + esi * 4 + var_14], eax» означает, что адрес сохраняется в некотором массиве, вероятно. Причина увеличения esi довольно очевидна. но почему мы добавляем 4 к esp?

1 Ответ

2 голосов
/ 12 апреля 2019

Начните с построчного анализа, чтобы выяснить, что делает код .

push    5

Эта инструкция помещает постоянное значение "5" в стек. Зачем? Ну, потому что ...

call    ??_U@YAPAXI@Z   ; operator new[](uint)

Эта инструкция вызывает operator new[], которая принимает один uint параметр. Этот параметр, очевидно, передается в стек в любом соглашении о вызовах, используемом этим кодом. Итак, ясно, что до сих пор мы вызывали operator new[] для выделения массива размером 5 байт.

В C ++ это будет записано как:

BYTE* eax = new BYTE[5];

Вызов operator new[] возвращает его значение (указатель на начало выделенного блока памяти) в регистре EAX. Это общее правило для всех соглашений о вызовах x86 - функции всегда возвращают свой результат в регистр EAX.

mov     [ebp+esi*4+var_14], eax

Приведенный выше код сохраняет (mov es) результирующий указатель (тот, который возвращается в EAX) в ячейку памяти, адресуемую EBP + (ESI * 4) + var_14. Другими словами, он масштабирует значение в регистре ESI на 4 (предположительно, размер uint), добавляет смещение из регистра EBP, а затем добавляет смещение константы var_14.

Это примерно эквивалентно следующему псевдо-C ++ коду:

void* address = (EBP + (ESI * 4) + var_14);
*address = eax;
add     esp, 4

Это очищает стек, фактически отменяя начальную push 5 инструкцию.

push поместил 32-битное (4 байта) значение в стек, которое уменьшило указатель стека, который поддерживается в регистре ESP (обратите внимание, что стек увеличивается на x86 ). Эта add инструкция увеличивает указатель стека (опять же, регистр ESP) на 4 байта.

Балансировка стека таким способом является оптимизацией. Вы могли бы эквивалентно написать pop eax, но это привело бы к дополнительному побочному эффекту, заключающемуся в зацикливании значения в регистре EAX.

Не существует прямого эквивалента этой инструкции на языке C ++, поскольку она просто выполняет бухгалтерскую работу, которая обычно скрыта от вас языком высокого уровня.

inc     esi

Это увеличивает значение регистра ESI на 1. Это эквивалентно:

esi += 1;
mov     byte ptr [eax+4], 0

Сохраняет постоянное значение 0 в блоке памяти размера BYTE в EAX + 4. Это соответствует следующему псевдо-C ++:

BYTE* ptr = (eax + 4);
*ptr = 0;
cmp     esi, 4

Сравнивает значение регистра ESI с постоянным значением 4. Инструкция CMP фактически устанавливает флаги, как если бы вычитание было сделано.

Поэтому следующая инструкция:

jl      short loc_1C1D40

условно переходит, если значение регистра ESI равно меньше 4.

Сравнение и переход - это отличительная черта циклической конструкции на языке более высокого уровня, такой как цикл for или while.


Собрав все воедино, вы получите что-то вроде:

void Foo(char** var_14)
{
    for (int esi = 0; esi < 4; ++esi)
    {
        var_14[esi] = new char[5];
        var_14[esi][4] = 0;
    }
}

Это не совсем верно, конечно. Воссоздание исходного кода C или C ++ из скомпилированной сборки очень похоже на воссоздание исходной коровы из котлеты из говяжьего фарша.

Но это довольно хорошо. Фактически, , если вы скомпилируете вышеуказанную функцию в MSVC, оптимизируя скорость и ориентируясь на 32-разрядную версию x86, вы получите следующую сгенерированную сборку :

void Foo(char**) PROC
        push    esi
        push    edi
        mov     edi, DWORD PTR _var_14$[esp+4]
        xor     esi, esi
$LL4@Foo:
        push    5
        call    void * operator new[](unsigned int)  ; operator new[]
        mov     DWORD PTR [edi+esi*4], eax
        add     esp, 4
        inc     esi
        mov     BYTE PTR [eax+4], 0
        cmp     esi, 4
        jl      SHORT $LL4@Foo
        pop     edi
        pop     esi
        ret     0
void Foo(char**) ENDP

Это в значительной степени точно так же, как и то, что у вас было в вопросе, при условии, что вы игнорируете пролог и эпилог (которые вы так и не показали в вопросе).

Основное отличие состоит в том, что компилятор применяет довольно очевидную оптимизацию подъема цикла к инструкции MOV. Вместо исходного кода:

mov   [ebp + esi * 4 + var_14], eax

вместо этого он предварительно вычисляет esp + var_14 в прологе, кэшируя результат в свободном регистре EDI:

mov   edi, DWORD PTR _var_14$[esp + 4]

, позволяющий просто загружать инструкцию внутри цикла:

mov   DWORD PTR [edi + esi * 4], eax

Понятия не имею, почему ваш код этого не делает или почему он использует EBP для удержания смещения.

...