Где находится вершина стека на x86?

/ Просмотров: 982

Статья является переводом этой статьи

Я заметил, что многие программисты путаются насчёт направления, в котором растёт стек на x86, и что означает "вершина стека" и "основание стека". Кажется, что эта путаница обусловлена несоответствием направления стека в человеческих мыслях и на реальной архитектуре x86.

В этой статье я намерен распутать эту путаницу с помощью нескольких полезных диаграмм.

Аналогии насчёт стека

Вернёмся к основам. Иногда стек демонстрируют студентам с помощью стопки тарелок. Вы кладёте тарелку на вершину стека и убираете её сверху. Вершина стека - это место, где окажется ваша тарелка после того, как вы её положите сверху, а также откуда вы берёте тарелку, когда хотите её убрать.

Тарелки

Аппаратный стек

В компьютерах обычно стек - это особым образом обрабатываемая область памяти. В абстрактном смысле представим аналогию - вы кладёте данные в стек, помещая их на вершину стека, и извлекаете данные, забирая их с вершины стека. Обратите внимание, что эта концепция никак не решает вопрос, где вершина стека находится в памяти.

Стек в архитектуре x86

Настало время рассмотреть причину путаницы. В архитектуре Intel x86 стек расположен "вниз головой". Он начинается с какого-то адреса в памяти и растёт вниз до какого-то нижнего адреса. Вот как это выглядит:

Стек растёт вниз

Таким образом, говоря "вершина стека" на x86, мы имеем в виду низший адрес в памяти, занятой стеком. Это может выглядеть неестественно для некоторых людей. Однако до тех пор, пока мы помним диаграмму выше, всё должно быть в порядке.

Теперь давайте посмотрим в графическом представлении некоторые принципы построения программ для x86.

Добавление и удаление данных с помощью указателя стека

В архитектуре x86 имеется особый регистр для работы со стеком - ESP (Extended Stack Pointer). По определению этот регистр всегда указывает на вершину стека:

Указатель стека

На этой диаграмме адрес 0x908ABCC - вершина стека. Там находится некоторое машинное слово, а регистр ESP содержит адрес 0x9080ABCC, другими словами, указывает на него.

Чтобы поместить новые данные в стек, используется инструкция push. Эта инструкция сначала уменьшает значение ESP на 4, а затем помещает операнд по адресу, на который указывает регистр ESP. Таким образом,

push eax

это то же самое, что

sub esp, 4
mov [esp], eax

Принимая предыдущую диаграмму за точку отсчёта и полагая, что в регистре EAX содержится значение 0xDEADBEEF, после выполнения этих инструкций стек будет выглядеть следующим образом:

Стек после выполнения push

Аналогично, инструкция pop извлекает значение, лежащее на вершине стека, помещает его в операнд, увеличивая затем указатель стека. Другими словами,

pop eax

это то же самое, что и

mov eax, [esp]
add esp, 4

Снова принимая предыдущую диаграмму (после push) за точку отсчёта, выполнив инструкции, получим следующую картину:

Стек после pop

Значение 0xDEADBEEF запишется в регистр EAX. Обратите внимание, что 0xDEADBEEF остаётся лежать по адресу 0x9080ABC8, поскольку мы не выполнили ни одной инструкции, которая перезаписала бы его.

Кадры стека и соглашение о вызовах

Рассматривая код ассемблера, созданный из исходного кода на C, можно найти множество интересных шаблонов. Возможно, самый узнаваемый шаблон - это способ передачи параметров в функции с использованием стека и способ размещения локальных переменных в стеке. Я продемонстрирую это на примере простой программы:

int foobar(int a, int b, int c)
{
    int xx = a + 2;
    int yy = b + 3;
    int zz = c + 4;
    int sum = xx + yy + zz;

    return xx * yy * zz + sum;
}

int main()
{
    return foobar(77, 88, 99);
}

Как аргументы, переданные в функцию foobar, так и локальные переменные этой функции наряду с некоторыми другими данными разместятся в стеке, как только функция foobar будет вызвана. Этот набор данных в стеке называется кадром функции. Прямо перед выходом из функции кадр стека будет выглядеть следующим образом:

Кадр стека

Зелёные данные помещены в стек вызывающей функцией, а синие - самой функцией foobar. Скомпилируем функцию с помощью gcc со следующими параметрами:

gcc -masm=intel -S z.c -o z.s

Следующий фрагмент кода - ассемблер, сгенерированный для функции foobar. Я прокомментировал его для лучшего понимания.

_foobar:
    ; Значение EBP нужно сохранять между вызовами.
    ; Поскольку функция модифицирует его, 
    ; нужно сохранить его значение в стек.
    ;
    push    ebp
    ; 
    ; Теперь регистр EBP будет хранить
    ; текущий кадр стека функции
    mov     ebp, esp

    ; Make space on the stack for local variables
    ;
    sub     esp, 16

    ; Поместить значение переменной a в регистр EAX, увеличить на 2,
    ; сохранить полученное значение в переменную xx
    ;
    mov     eax, DWORD PTR [ebp+8]
    add     eax, 2
    mov     DWORD PTR [ebp-4], eax

    ; Поместить значение переменной b в регистр EAX, увеличить на 3,
    ; сохранить полученное значение в переменную yy.
    ;
    mov     eax, DWORD PTR [ebp+12]
    add     eax, 3
    mov     DWORD PTR [ebp-8], eax

    ; Поместить значение переменной c в EAX, увеличить на 4,
    ; сохранить полученное значение в переменную zz
    ;
    mov     eax, DWORD PTR [ebp+16]
    add     eax, 4
    mov     DWORD PTR [ebp-12], eax

    ; Сложить xx, yy, zz, результат поместить в переменную sum
    ;
    mov     eax, DWORD PTR [ebp-8]
    mov     edx, DWORD PTR [ebp-4]
    lea     eax, [edx+eax]
    add     eax, DWORD PTR [ebp-12]
    mov     DWORD PTR [ebp-16], eax

    ; Производим последние вычисления в регистре EAX,
    ; которое является возвращаемым значением
    ;
    mov     eax, DWORD PTR [ebp-4]
    imul    eax, DWORD PTR [ebp-8]
    imul    eax, DWORD PTR [ebp-12]
    add     eax, DWORD PTR [ebp-16]

    ; Здесь инструкция leave - эквивалент следующих инструкций
    ;
    ;   mov esp, ebp
    ;   pop ebp
    ;
    ; Она очищает локальные переменные и восстанавливает значение регистра EBP.
    ;
    leave
    ret

Поскольку регистр ESP изменяется при выполнении функции, регистр EBP (базовый указатель, также известный как указатель кадра на других архитектурах) используется как якорь, относительно которого код находит все локальные переменные и параметры. Аргументы находятся выше указателя EBP в стеке (следовательно, для доступа к ним используется положительное смещение), в то время как локальные переменные - ниже указателя EBP.

Оставьте комментарий!

Комментарий будет опубликован после проверки

Вы можете войти под своим логином или зарегистрироваться на сайте.

(обязательно)