Как на самом деле передаются аргументы в System V AMD64
Соглашение о вызовах System V AMD64 используют Linux, FreeBSD и macOS. В общих чертах его знают все: «первые шесть аргументов в регистрах». Но когда смотришь на дизассемблер, всплывают детали, которых в этой фразе нет.
Целочисленные аргументы
Первые шесть целых и указателей идут в регистрах по порядку, остальные кладутся на стек справа налево:
| Аргумент | Регистр |
|---|---|
| 1 | rdi |
| 2 | rsi |
| 3 | rdx |
| 4 | rcx |
| 5 | r8 |
| 6 | r9 |
Числа с плавающей точкой независимо от этого занимают xmm0–xmm7. Поэтому у f(int a, double b, int c) аргумент c окажется в rsi, а не в rdx: счётчики у двух классов раздельные.
Загадочный al перед printf
Перед вызовом функции с переменным числом аргументов компилятор пишет в al количество задействованных векторных регистров:
lea rdi, [rip + .Lfmt]
mov esi, 42
xor eax, eax ; ни одного double — al = 0
call printf
Пролог printf смотрит на al и решает, нужно ли сохранять xmm0–xmm7 в область регистров для va_list. Забудьте обнулить eax в ассемблерной вставке — и получите редкий, плохо воспроизводимый мусор.
Выравнивание стека
В момент инструкции call указатель стека должен быть кратен 16. Сам call кладёт 8 байт адреса возврата, поэтому внутри функции rsp смещён на 8 — отсюда классический sub rsp, 8 или лишний push в прологе. Нарушение чаще всего проявляется падением на movaps где-нибудь глубоко в libc.
Red zone
Листовая функция может использовать 128 байт ниже rsp, не двигая его: ABI гарантирует, что обработчики сигналов эту область не тронут. Именно поэтому ядро собирают с -mno-red-zone — прерывания там приходят на тот же стек.