Ассемблер

Ассемблер x86 и x64: как адресуются переменные в стеке

В ассемблере x86 и x64 переменные в стеке адресуются через регистр-опору и смещение. В классическом x86 часто используют ebp: локальные переменные лежат по адресам вроде [ebp-4], а аргументы функции — по адресам вроде [ebp+8]. В x64 часто используют rbp или rsp: локальные переменные могут быть по [rbp-4], [rbp-16] или по смещениям от rsp

Главная идея простая: стек — это область памяти, а регистр ebp, rbp, esp или rsp помогает найти нужную ячейку

Классическая схема x86

В 32-битном x86 функция часто начинается так:

push ebp
mov ebp, esp
sub esp, 16

Что происходит:

  • push ebp сохраняет старую опору стека;
  • mov ebp, esp делает ebp базой текущей функции;
  • sub esp, 16 выделяет 16 байт под локальные данные.

После этого локальная переменная может лежать здесь:

mov dword [ebp-4], 10

Это означает: записать число 10 в память по адресу ebp - 4

Почему локальные переменные с минусом

На x86 и x64 стек обычно растет вниз, то есть в сторону меньших адресов. Когда функция выделяет место под локальные переменные, она уменьшает esp или rsp. Поэтому локальные значения часто находятся ниже базового указателя:

[ebp-4]
[ebp-8]
[rbp-16]

Минус в адресе не означает отрицательное значение переменной. Это смещение адреса

Где лежат аргументы в x86

В простом 32-битном вызове аргументы часто передаются через стек. После стандартного пролога:

[ebp+8]   ; первый аргумент
[ebp+12]  ; второй аргумент
[ebp+16]  ; третий аргумент

Почему не [ebp+4]? Потому что рядом лежит адрес возврата. Обычно:

[ebp]     старый ebp
[ebp+4]   адрес возврата
[ebp+8]   первый аргумент

Эта схема зависит от соглашения вызова, но как учебная база она очень полезна

Что меняется в x64

В x64 первые аргументы часто передаются не через стек, а через регистры. В Linux x86-64 по System V ABI первые целочисленные аргументы идут через:

rdi, rsi, rdx, rcx, r8, r9

В Windows x64 первые аргументы идут через:

rcx, rdx, r8, r9

Стек все равно используется: для локальных переменных, сохранения регистров, временных данных и аргументов, которые не помещаются в регистры

RBP и RSP

rbp может быть стабильной опорой стекового кадра:

push rbp
mov rbp, rsp
sub rsp, 32
mov dword [rbp-4], 10

Но современные компиляторы иногда не используют rbp как отдельную опору и адресуют переменные через rsp:

mov dword [rsp+12], 10

Такой код труднее читать новичку, потому что rsp может меняться при вызовах и выделении места. Поэтому для обучения удобнее начинать с варианта с rbp

Intel и AT&T запись

Intel/NASM:

mov eax, dword [rbp-12]

AT&T:

movl -12(%rbp), %eax

Смысл одинаковый: взять 4 байта из памяти по адресу rbp - 12 и положить в eax

Пример локальной переменной

Пусть локальная переменная x лежит по [rbp-4]:

mov dword [rbp-4], 25
mov eax, dword [rbp-4]
add eax, 5
mov dword [rbp-4], eax

Здесь:

  1. в x записали 25;
  2. прочитали x в eax;
  3. увеличили значение;
  4. записали результат обратно в стек.

Частые ошибки

Считать [rbp-4] значением, а не адресом

[rbp-4] означает память по адресу rbp - 4. Без скобок rbp-4 — это просто расчет адреса

Путать локальные переменные и аргументы

В x86 локальные обычно ниже ebp, аргументы часто выше ebp. В x64 аргументы сначала могут быть в регистрах

Забывать про размер

byte [rbp-1], dword [rbp-4], qword [rbp-8] — это разные обращения

Переносить пример между x86 и x64 без изменений

В x86 регистры eax, ebp, esp. В x64 — rax, rbp, rsp, а соглашения вызова отличаются

Что почитать дальше по ассемблеру

Если нужен общий маршрут по теме, откройте рубрику ассемблеру. Для соседних задач пригодятся эти разборы:

Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные

Настройки cookie

0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x