Ассемблер
Ассемблер 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
Здесь:
- в
xзаписали25; - прочитали
xвeax; - увеличили значение;
- записали результат обратно в стек.
Частые ошибки
Считать [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, а соглашения вызова отличаются
Что почитать дальше по ассемблеру
Если нужен общий маршрут по теме, откройте рубрику ассемблеру. Для соседних задач пригодятся эти разборы:
- CMP, JMP, JE, JNZ: условия и циклы в ассемблере
- I8080, AVR и STM32 на ассемблере: когда идти в нишевые ветки
- MASM, NASM, FASM, SASM: что выбрать новичку
- Ассемблер для чайников: первый код без страха