Кадр функции и соглашение 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, r12r15 — 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).
Проверьте себя
1. В каком регистре передаётся первый целочисленный аргумент по System V?
Arax
Brdi
Crsi
Drbx
2. Где возвращается результат функции по System V?
AВ rdi
BВ rsp
CВ rax
DНа стеке всегда
3. Что делает пролог функции push rbp; mov rbp, rsp?
AЗавершает функцию
BСохраняет старый rbp и настраивает кадр стека текущей функции
CПередаёт аргументы
DОчищает rax