ВПЕРЁД ⇒
⇐ НАЗАД
Вызовы функций
Функция без аргументов и возвратного значения
Код
С++
void func() {
return;
}
int main() {
func();
}ASM (x86-64 clang)
func():
push rbp
mov rbp, rsp
pop rbp
ret
main:
push rbp
mov rbp, rsp
call func()
xor eax, eax
pop rbp
retONLINE COMPILER
Разбор инструкций
func() - возвращаемый тип void
func():
push rbp
mov rbp, rsp
pop rbp
retЭто практически пустая функция из первого примера, но без xor eax, eax. Причина проста: тип возврата void - функция ничего не возвращает, поэтому трогать eax/rax не нужно. Вызывающая сторона не будет читать этот регистр.
main - инструкция call func()
Эта инструкция делает две вещи атомарно:
- Кладёт на стек адрес следующей инструкции (
xor eax, eax) - это будет адрес возврата - Передаёт управление на метку
func()
До call:
Верхние адреса памяти
---------------------------------------------------------------
< ret addr в _start >
rsp → (rbp main) → < rbp _start >
---------------------------------------------------------------
Нижние адреса памяти
###############################################################
После call (уже внутри func):
Верхние адреса памяти
---------------------------------------------------------------
< ret addr в _start >
(rbp main) → < rbp _start >
rsp → ( ) → < addr of xor eax > ← push был сделан
инструкцией call
---------------------------------------------------------------
Нижние адреса памяти
Затем func() начинает подготавливать свой собственный стек-фрейм:
Верхние адреса памяти
--------------------------------------------
< ret addr в _start >
(rbp main) → < rbp _start >
< addr of xor eax >
rsp → (rbp func) → < rbp main >
--------------------------------------------
Нижние адреса памяти
ret внутри func()
Снимает с вершины стека адрес возврата (addr of xor eax) и прыгает туда. Стек возвращается в состояние как до call.
Полная картина выполнения
_start → call main
| → push rbp (сохранить rbp _start)
пролог main | → mov rbp, rsp (новый фрейм main)
→ call func() (push addr xor; jmp func)
→ push rbp (сохранить rbp main)
→ mov rbp, rsp (новый фрейм func)
→ pop rbp (восстановить rbp main)
→ ret (прыгнуть на xor eax)
эпилог main | → xor eax, eax (return 0)
| → pop rbp (восстановить rbp _start)
| → ret (вернуться в _start)
_start → exit()
Почему xor eax, eax стоит после call, а не перед?
call не трогает eax, но func() теоретически могла бы его изменить (если бы возвращала значение). Компилятор ставит подготовку возвращаемого значения после вызова, чтобы не перезаписывать eax зря - на случай если бы func() возвращала что-то в eax. В данном случае это не принципиально, но компилятор следует единой схеме.
Функция без аргументов и c возвратным значением
Код
С++
int func() {
return 1234;
}
int main() {
int var{func()};
}ASM (x86-64 clang)
func():
push rbp
mov rbp, rsp
mov eax, 1234
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
call func()
mov dword ptr [rbp - 4], eax
xor eax, eax
add rsp, 16
pop rbp
retONLINE COMPILER
Разбор инструкций
func() - возврат int
func():
push rbp
mov rbp, rsp
mov eax, 1234
pop rbp
retmov eax, 1234
Вместо записи на стек значение кладётся прямо в регистр eax. По соглашению System V AMD64 ABI, целочисленный результат функции всегда возвращается через rax (для 32-битных значений - через eax). Никакой промежуточной переменной на стеке нет, func() просто заряжает регистр и возвращает управление вызвавшей функции.
main - новые инструкции
sub rsp, 16
Впервые появляется явное выделение места на стеке. Компилятор заранее резервирует 16 байт, сдвигая rsp вниз.
Формально здесь нужен только 1 слот в 4 байта (под var), но компилятор выделяет 16 - это требование выравнивания стека. По ABI, перед любым call стек должен быть выровнен по 16 байт. После push rbp и mov rbp, rsp выравнивание уже соблюдено, и sub rsp, 16 сохраняет его перед вызовом func().
Верхние адреса памяти
-------------------------------------------------------------------
< ret addr в _start > ← 8 байт
(rbp main ) → < rbp _start > ← 8 байт
(rbp_main - 4 ) → < ??? > ← 4 байта под var
(rbp_main - 8 ) → < ??? > ← 4 байта
(rbp_main - 12) → < ??? > ← 4 байта
rsp → (rbp_main - 16) → < ??? > ← 4 байта
-------------------------------------------------------------------
Нижние адреса памяти
call func()
Работает так же, как в предыдущем примере - кладёт адрес возврата на стек и прыгает в func(). Внутри func() вычисляется результат и помещается в eax.
Стек после входа в func()
Верхние адреса памяти
-------------------------------------------------------------------
< ret addr в _start > ← 8 байт
(rbp main ) → < rbp _start > ← 8 байт
(rbp_main - 4 ) → < ??? > ← 4 байта под var
(rbp_main - 8 ) → < ??? > ← 4 байта
(rbp_main - 12) → < ??? > ← 4 байта
(rbp_main - 16) → < ??? > ← 4 байта
(rbp_main - 24) → < ret addr в main > ← 8 байт
rsp → (rbp func ) → < rbp main > ← 8 байт
-------------------------------------------------------------------
Нижние адреса памяти
mov dword ptr [rbp - 4], eax
Ключевое новшество этого примера - значение путешествует через регистр: func() кладёт его в eax, main забирает из eax в память. Стек используется для хранения локальных переменных функций, регистр - для передачи возвратного значения из вызываемой функции в вызывающую.
После возврата из func() значение 1234 находится в eax. Инструкция mov dword ptr [rbp - 4], eax инициализирует var на стеке путём чтения значения из регистра и записи этого значения в ячейку памяти, расположенную в стек-фрейме выполняющейся функции. Это прямое воплощение int var{func()}.
Стек после выхода из func()
Верхние адреса памяти
------------------------------------------------------------------------
< ret addr в _start > ← 8 байт
(rbp main ) → < rbp _start > ← 8 байт
(rbp_main - 4 ) → < 0x000004D2 > ← 4 байта (var = 1234)
(rbp_main - 8 ) → < ??? > ← 4 байта
(rbp_main - 12) → < ??? > ← 4 байта
rsp → (rbp_main - 16) → < ??? > ← 4 байта
------------------------------------------------------------------------
Нижние адреса памяти
xor eax, eax
Инструкция xor eax, eax выполняет подготовку возврата (return 0) из main. Обратите внимание: та же самая инструкция затирает eax, в котором только что лежал результат func(). Это нормально - значение уже сохранено в [rbp - 4].
add rsp, 16
Симметрично инструкции sub rsp, 16 из пролога функции main, инструкция add rsp, 16 в эпилоге функции main освобождает зарезервированные 16 байт, возвращая rsp к rbp.
Полная картина выполнения
_start → call main
| → push rbp (сохранить rbp _start)
| → mov rbp, rsp (новый фрейм main)
пролог main | → sub rsp, 16 (резерв под var + выравнивание)
→ call func() (push addr mov; jmp func)
→ push rbp (сохранить rbp main)
→ mov rbp, rsp (новый фрейм func)
→ mov eax, 1234 (подготовить возврат)
→ pop rbp (восстановить rbp main)
→ ret (прыгнуть на mov [rbp-4], eax)
→ mov [rbp-4], eax (сохранить 1234 в var)
эпилог main | → xor eax, eax (return 0)
| → add rsp, 16 (освободить стек)
| → pop rbp (восстановить rbp _start)
| → ret (вернуться в _start)
_start → exit()
Обратите внимание на то, как много “лишних” инструкций приходится выполнять только для того, чтобы просто вернуть 1234 из func в main. Добавление опции компиляции -O2 (оптимизации 2-го уровня) позволяет значительно сократить объём сгенерированных ассемблерных инструкций:
int func() {
return 1234;
}
int main() {
int var{func()};
}func():
mov eax, 1234
ret
main:
xor eax, eax
retВ данном случае, поскольку результат вызова функции func() нигде не используется в вызывающей функции main(), компилятор не добавляет (в ассемблерную программу) избыточную инструкцию вызова call func(). Создание стек-фрейма также не выполняется.
Функция с аргументом и без возвратного значения
Код
С++
void func(int arg) {
return;
}
int main() {
int var{1234};
func(var);
}ASM (x86-64 clang)
func(int):
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], edi
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov dword ptr [rbp - 4], 1234
mov edi, dword ptr [rbp - 4]
call func(int)
xor eax, eax
add rsp, 16
pop rbp
retONLINE COMPILER
Разбор инструкций
func(int) - приём аргумента через регистр
func(int):
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], edi
pop rbp
retmov dword ptr [rbp - 4], edi
Это сохранение аргумента на стек. По соглашению System V AMD64 ABI, первые 6 целочисленных аргументов передаются через регистры в таком порядке:
rdi(64 бита) /edi(32 бита) - Destination Index Registerrsi(64 бита) /esi(32 бита) - Source Index Registerrdx(64 бита) /edx(32 бита) - Data Registerrcx(64 бита) /ecx(32 бита) - Counter Registerr8(64 бита) /r8d(32 бита) - дополнительный регистр x64-архитектуры R8r9(64 бита) /r9d(32 бита) - дополнительный регистр x64-архитектуры R9
У func() один аргумент типа int (4 байта), поэтому он приходит в edi. Компилятор копирует его на стек по адресу [rbp - 4] - это локальная копия параметра arg, как если бы это была обычная локальная переменная.
Стек после входа в func()
Верхние адреса памяти
--------------------------------------------------------------------------
< ret addr в _start >
...
< ret addr в main >
rsp → (rbp func ) → < rbp main >
(rbp_func - 4) → < 0x000004D2 > ← arg (скопирован из edi)
--------------------------------------------------------------------------
Нижние адреса памяти
Поскольку func() ничего не делает с arg, инструкция mov dword ptr [rbp - 4], edi - просто формальность. При -O2 она исчезнет.
void func(int arg) {
return;
}
int main() {
int var{1234};
func(var);
}func():
ret
main:
xor eax, eax
retmain - подготовка и передача аргумента
sub rsp, 16
Выделение 16 байт на стеке. Формально нужно только 4 байта под var, но выравнивание требует кратности 16 перед call.
mov dword ptr [rbp - 4], 1234
Инициализация локальной переменной var значением 1234.
Верхние адреса памяти (внутри main после sub)
---------------------------------------------------------------
< ret addr в _start >
(rbp main ) → < rbp _start >
(rbp_main - 4 ) → < 0x000004D2 > ← var (1234)
(rbp_main - 8 ) → < ??? >
(rbp_main - 12) → < ??? >
rsp → (rbp_main - 16) → < ??? >
---------------------------------------------------------------
Нижние адреса памяти
mov edi, dword ptr [rbp - 4]
Ключевая инструкция передачи аргумента. Читает значение var из стека и помещает его в регистр edi. Это подготовка к вызову - по соглашению ABI, первый аргумент должен лежать именно там.
Обратите внимание на направление: edi, [rbp - 4] - это передача из памяти в регистр (направление передачи справа налево по синтаксису Intel).
call func(int)
Инструкция кладёт адрес возврата (xor eax, eax) на стек и прыгает в func(). В момент входа в func() регистр edi содержит значение 1234.
Верхние адреса памяти (сразу после call, до пролога func)
---------------------------------------------------------------
< ret addr в _start >
(rbp main ) → < rbp _start >
(rbp_main - 4 ) → < 0x000004D2 > ← var (1234)
(rbp_main - 8 ) → < ??? >
(rbp_main - 12) → < ??? >
(rbp_main - 16) → < ??? >
rsp → ( ) → < addr of xor eax >
---------------------------------------------------------------
Нижние адреса памяти
edi = 0x000004D2 ← аргумент готов к использованию
Затем func() выполняет свой пролог (push rbp; mov rbp, rsp) и копирует edi на свой стек.
Верхние адреса памяти (внутри func после пролога)
---------------------------------------------------------------
< ret addr в _start >
(rbp main ) → < rbp _start >
(rbp_main - 4 ) → < 0x000004D2 > ← var (1234)
(rbp_main - 8 ) → < ??? >
(rbp_main - 12) → < ??? >
(rbp_main - 16) → < ??? >
(rbp_main - 24) → < addr of xor eax >
rsp → (rbp func ) → < rbp main >
(rbp_func - 4 ) → < 0x000004D2 > ← arg
---------------------------------------------------------------
Нижние адреса памяти
Полная картина выполнения
_start → call main
| → push rbp (сохранить rbp _start)
| → mov rbp, rsp (новый фрейм main)
пролог main | → sub rsp, 16 (резерв под var)
→ mov [rbp-4], 1234 (инициализировать var)
→ mov edi, [rbp-4] (загрузить var в edi)
→ call func(int) (push addr xor; jmp func)
| → push rbp (сохранить rbp main)
пролог func | → mov rbp, rsp (новый фрейм func)
→ mov [rbp-4], edi (сохранить arg на стек)
эпилог func | → pop rbp (восстановить rbp main)
| → ret (прыгнуть на xor eax)
эпилог main | → xor eax, eax (return 0)
| → add rsp, 16 (освободить стек)
| → pop rbp (восстановить rbp _start)
| → ret (вернуться в _start)
_start → exit()
Маршрут данных
1234 → [main_rbp-4] (var в main)
[main_rbp-4] → edi (подготовка к вызову)
edi → [func_rbp-4] (arg в func)
Значение трижды копируется:
- Константа → память (инициализация
var) - Память → регистр (подготовка аргумента для
func) - Регистр → память (сохранение подготовленного аргумента
во входном параметре arg/в локальной переменной argфункцииfunc)
Сравнение с предыдущим примером
int var{func()} | func(var) | |
|---|---|---|
| Направление передачи | func → main | main → func |
| Регистр | eax (возврат) | edi (первый аргумент) |
Инструкция в main | mov [rbp-4], eax | mov edi, [rbp-4] |
| Операция | чтение регистра eax и запись в [rbp-4] | чтение регистра [rbp-4] и запись в edi |
ВПЕРЁД ⇒
⇐ НАЗАД
Источники
- Здесь будет список источников
Категория
Теги
- Cpp Cpp
- Asm Asm
- Assembly Assembly
- Intel-asm-syntax Intel-asm-syntax
- x86-64 x86-64
- Стек Стек
- Stack Stack
- Стек-вызовов Стек-вызовов
- Call-stack Call-stack
- Стек-фрейм Стек-фрейм
- Stack-frame Stack-frame
- Указатель-фрейма Указатель-фрейма
- Frame-pointer Frame-pointer
- Указатель-стека Указатель-стека
- Stack-pointer Stack-pointer
- clang clang
- clang-fomit-frame-pointer clang-fomit-frame-pointer
- clang-O2 clang-O2
- Эпилог Эпилог
- Пролог Пролог