Попытка понять реализацию функции alloca() в сборке на x86

Я новичок в ассемблере и сейчас читаю книгу под названием Reverse Engineering for Beginners и добрался до той части, где говорится о распределении памяти в стеке. Я понимаю (я думаю) концепцию распределения стека, но в примере было что-то, чего я не понял, и я буду рад, если кто-нибудь здесь сможет помочь.

Книга дает эту функцию в качестве примера:

#ifdef __GNUC__
#include <alloca.h> // GCC
#else
#include <malloc.h> // MSVC
#endif
#include <stdio.h>
void f()
{
    char *buf=(char*)alloca (600);
#ifdef __GNUC__
    snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); // GCC
#else
    _snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); // MSVC
#endif
    puts (buf);
};

Я понимаю, что делает функция C. Он выделяет 600 байт памяти в стеке, а затем записывает в это пространство строку hi! используя функцию _snprintf . Затем функция печатает его.

Все хорошо на данный момент. После этого в книге приводится реализация сборки, созданная компилятором MSVC, и код выглядит так:

mov eax, 600 ; 00000258H
call __alloca_probe_16
mov esi, esp
push 3
push 2
push 1
push OFFSET $SG2672
push 600 ; 00000258H
push esi
call __snprintf
push esi
call _puts
add esp, 28 

Здесь я понимаю, что регистр EAX будет содержать аргумент для функции __alloca_probe_16. но теперь что-то не имеет смысла для меня. Насколько я понимаю, функция __alloca_probe_16 в основном просто вычитает количество байтов, которое находится в значении EAX, из ESP.

Так, например, если ESP указывает на 1000, теперь оно указывает на 400. Затем мы сохраняем 400 в ESI и начинаем помещать аргументы _snprintf в стек, а ESI указывает на место, куда функция должна начать запись данных. Итак, моя проблема заключается в следующем, если оба регистра ESP и ESI указывают на 400, и я выделил память из 1000-400 (600 байт), и я начинаю запихивать вещи в стек, не войдут ли они в позицию, начиная с 400 и уменьшается? Я имею в виду, почему он вычел 600 байт, если он их не использует? На мой взгляд, так выглядит стек после строки push esi.


|          400          |
|          600          |
|    adrr of string     |
|           1           |
|           2           |
|           3           | 400 , ESP , ESI
|                       | ...
|_______________________| 1000

Я знаю, что я, вероятно, ошибаюсь и что-то не так понял, потому что я не думаю, что книга неверна, я буду рад, если кто-то поможет мне понять, что происходит в этом ассемблерном коде.


person Ronen    schedule 16.08.2020    source источник
comment
Да, именно так выглядит стек. Как вы думаете, что не так с этой картинкой? Что вы находите в этом удивительного?   -  person Igor Tandetnik    schedule 16.08.2020
comment
мой вопрос: почему я выделил 600 байт, если я их не использую?   -  person Ronen    schedule 16.08.2020
comment
Вы передали указатель на эти байты в качестве первого аргумента snprintf. Это 400 вверху вашей картинки. snprintf запишет вывод в эти байты.   -  person Igor Tandetnik    schedule 16.08.2020
comment
Итак, snprintf записывает данные в [ESI + 600], [ESI+601] и т. д., пока не дойдет до байта \0? или это начинается с 400 и 401 402 и т.д.?   -  person Ronen    schedule 16.08.2020
comment
snprintf будет записывать данные в 400, 401 и так далее, где 400 — его первый аргумент, значение наверху стека при вызове. Но не дальше 400+600, где 600 — второй аргумент.   -  person Igor Tandetnik    schedule 16.08.2020


Ответы (1)


так, например, если ESP указывает на 1000, теперь он указывает на 400. Затем мы сохраняем 400 в ESI и начинаем помещать аргументы _snprintf в стек, а ESI указывает на место, где функция должна начать запись данных.

Верно.

Итак, моя проблема в том, что если оба регистра ESP и ESI указывают на 400, и я выделил память из 1000-400 (600 байт), и я начинаю заталкивать что-то в стек, не войдут ли они в позицию, начинающуюся с 400 и уменьшается?

Да, конечно будут.

Я имею в виду, мы вычли 600 байт, если я их не использую?

Вы не использовали это пространство пока, но сделали его доступным, чтобы snprintf имел место для записи создаваемой им строки, и вы передали ему адрес 400 (который был в ESI), чтобы сообщить это сделать так. Когда snprintf вернется, строка "hi! 1, 2, 3 \n" будет сохранена, начиная с адреса 400.

Конечно, вам не нужно 600 байт для такой короткой строки; это просто пример для иллюстрации. Вы можете сделать его меньше, если хотите.

person Nate Eldredge    schedule 16.08.2020