Кадр функции и соглашение System V
Урок про правила, по которым функции договариваются о передаче аргументов и сохранении регистров.
Соглашение вызова (calling convention) — общие правила, как передавать аргументы, возвращать результат и кто сохраняет регистры.
Зачем нужны соглашения
Чтобы ваша функция могла вызвать чужую (например, из libc), обе должны договориться: где лежат аргументы, куда класть результат, какие регистры можно портить. В Linux x86-64 действует соглашение System V AMD64 ABI — его соблюдают все.
Аргументы по регистрам
Первые шесть целочисленных аргументов передаются в регистрах в строгом порядке, а результат возвращается в rax:
| Аргумент | Регистр |
| 1-й | rdi |
| 2-й | rsi |
| 3-й | rdx |
| 4-й | rcx |
| 5-й | r8 |
| 6-й | r9 |
| результат | rax |
Седьмой и далее аргументы передаются через стек. Поэтому для функции sum(a, b) вызывающий кладёт a в rdi, b в rsi и делает call.
Пролог и эпилог
В начале функции принято настроить «кадр стека» (стандартный пролог), а в конце вернуть всё назад (эпилог):
summa:
push rbp ; пролог: сохранить старый rbp
mov rbp, rsp ; rbp указывает на кадр этой функции
; ... тело: аргументы в rdi, rsi ...
mov rax, rdi
add rax, rsi ; rax = a + b
mov rsp, rbp ; эпилог: восстановить стек
pop rbp ; вернуть старый rbp
retКадр (rbp) даёт стабильную точку отсчёта для локальных переменных и аргументов, даже когда rsp двигается. По цепочке сохранённых rbp отладчик и восстанавливает стек вызовов.
Как работает под капотом
Смоделируем передачу аргументов и возврат результата по System V на Python:
def call_summa(rdi, rsi): # 1-й арг в rdi, 2-й в rsi
rax = rdi + rsi # тело: rax = a + b
return rax # результат в rax
result = call_summa(7, 35) # summa(7, 35)
print("rdi=7, rsi=35 -> rax =", result)Вывод:
rdi=7, rsi=35 -> rax = 42
Именно по этим правилам ваш ассемблерный код может вызывать функции на C и наоборот: важно лишь разложить аргументы по нужным регистрам и забрать ответ из rax.
Частые ошибки
- Перепутать порядок регистров аргументов. Порядок строгий: rdi, rsi, rdx, rcx, r8, r9.
- Затереть регистры, которые обязан сохранить вызываемый.
rbx,rbp,r12–r15— callee-saved: их нужно сохранить и восстановить. - Забыть про выравнивание стека на 16 байт перед
call. Нарушение валит вызовы в libc (например, SSE-инструкции).
Итог
- System V ABI задаёт правила вызова в Linux x86-64.
- Аргументы 1–6 идут в rdi, rsi, rdx, rcx, r8, r9; результат — в
rax. - Пролог (
push rbp; mov rbp, rsp) и эпилог настраивают и сворачивают кадр. - Часть регистров обязан сохранять вызываемый (callee-saved).