Соглашения - Calling Conventions на x86 и x64 с примерами и схемами
Термины и основы
Calling convention — соглашение о способе передачи аргументов функции, возврате значений и очистке стека.
Важен при низкоуровневой разработке, реверс-инжиниринге, работе с чужим кодом, ассемблерной вставке.
Различие соглашений: кто и как передаёт аргументы, кто очищает стек, используется ли frame pointer.
x86 и Win64 кардинально отличаются!
x86: Основные calling conventions
| Convention | Порядок аргументов | Как передаются | Кто чистит стек | Возврат значения | Имя функции | Где применяется |
| cdecl |Cправа налево |Стек |Caller |EAX |_func |C/varargs |
|stdcall | Cправа налево | Стек | Callee | EAX | _func@N | WinAPI |
| | | | | | |
* Аргументы: справа налево через стек.
Стек чистит вызывающий (caller).
Хорошо подходит для функций с переменным числом аргументов (printf и др).
Имя: обычно с нижним подчёркиванием на уровне объектного файла.
Возврат: EAX.
Пример (cdecl, x86):
int sum(int a, int b) { return a + b; }
push 2 ; b
push 1 ; a
call sum
add esp, 8 ; очищаем стек
sum:
push ebp
mov ebp, esp
sub esp, 4 ; локальная переменная
mov eax, [ebp+8] ; a
add eax, [ebp+12] ; b
mov esp, ebp
pop ebp
ret
—
__stdcall
Аргументы: справа налево через стек.
Стек чистит вызываемая функция (callee).
Переменные аргументы невозможны.
Имя: _func@N (символ + байты аргументов).
Возврат: EAX.
Пример (stdcall, x86):
push 2
push 1
call sum@8
; нет add esp, 8!
...
sum@8:
push ebp
mov ebp, esp
sub esp, 4
...логика...
mov esp, ebp
pop ebp
ret 8 ; callee очищает стек
—
__fastcall
1-й аргумент в ECX, 2-й в EDX, остальные через стек.
Стек чистит callee (ret N).
Имя: @func@N.
Возврат: EAX.
Пример (fastcall, x86):
push 3 ; 3-й аргумент
mov edx, 2 ; 2-й
mov ecx, 1 ; 1-й
call sum@12
... ; нет add esp
sum@12:
push ebp
mov ebp, esp
...
mov esp, ebp
pop ebp
ret 0Ch
—
thiscall
Для методов С++.
Указатель this — ECX, остальные аргументы через стек.
Стек чистит caller.
Имя видоизменено по схеме C++ манглинга.
Используется только для x86.
Пример (thiscall, x86):
push 2 ; b
push 1 ; a
mov ecx, obj ; this
call Foo::bar
add esp, 8
—
Детали реализации (prologue/epilogue) на x86
Prologue (вход функции)
push ebp ; сохранить старый frame pointer
mov ebp, esp ; установить новый frame pointer (ebp)
sub esp, X ; выделить X байт под локальные переменные
Epilogue (выход из функции)
mov esp, ebp
pop ebp
ret / ret N ; ret N для stdcall/fastcall
Локальные переменные и аргументы
Пример с комментариями
sum:
push ebp ; сохранить ebp
mov ebp, esp ; новый frame pointer
sub esp, 8 ; 2 локальных переменных (например)
mov eax, [ebp+8] ; первый аргумент a
add eax, [ebp+12] ; второй b
mov [ebp-4], eax ; локалка c
...
mov esp, ebp
pop ebp
ret / ret N
—
x64 (Win64 ABI): подробный разбор
Передача аргументов
Первые 4 аргумента — через регистры: RCX, RDX, R8, R9 (или XMM0–XMM3 для float/double).
5-й и далее — через стек ([rsp+N]).
Caller ОБЯЗАН выделять 32 байта «shadow space» перед вызовом (sub rsp, 28h).
Других соглашений под Win64 нет!
Prologue (вход функции)
sub rsp, 28h ; shadow space (32 байта) + выравнивание (обычно)
; если нужны локальные — sub rsp, больше
Обычно frame pointer (rbp) не используется. Если включени дебаг-опции — возможно будет push rbp; mov rbp, rsp.
Аргументы сразу доступны по rcx, rdx, r8, r9.
Shadow space: [rsp+0], [rsp+8], [rsp+16], [rsp+24]
Epilogue (выход функции)
Локальные переменные
Дизассемблированный x64-пример (MSVC / x64dbg)
sum:
sub rsp, 28h ; Shadow space
mov eax, ecx ; 1-й аргумент
add eax, edx ; 2-й
add eax, r8d ; 3-й
add eax, r9d ; 4-й
shl eax, 1 ; умножение на 2
add rsp, 28h
ret
Пример вызова (caller, x64)
mov ecx, 1 ; первый аргумент
mov edx, 2 ; второй
mov r8d, 3 ; третий
mov r9d, 4 ; четвертый
sub rsp, 28h ; shadow space
; здесь push или mov в [rsp+40h] пятый/шестой/и т.д.
call sum
add rsp, 28h
Важные примечания Win64 ABI
Перед каждым call stack должен быть выровнен на 16 байт!
Shadow (home) space обязателен, даже если всего 1 аргумент.
Все значения до 4-х аргументов необходимо сохранять там callee-стороне при необходимости (spill).
strconv, printf и другие vararg: после 4-го аргумента значения только через стек.
Возврат: через RAX (int/pointer) или XMM0 (float/double).
Отличие от Linux x64 (System V ABI)
Под Linux: первые шесть аргументов кладутся в RDI, RSI, RDX, RCX, R8, R9.
Нет shadow space.
Не путать с Win64!
—
Как узнать соглашение вызова по дизассемблированному коду (x64dbg)
cdecl: после call всегда add esp, X (caller очищает стек).
stdcall/fastcall: после call нет add esp, а внутри функции — ret X (callee очищает).
fastcall: часть аргументов переданы через ecx/edx, остальные стек.
thiscall: ecx = this.
Win64: всегда используются rcx/rdx/r8/r9 (caller), выделяется 28h (или >=28h) перед call (shadow space).
Внутри функции часто нет frame pointer, доступ к локальным напрямую через rsp.
—
Краткая шпаргалка по стеку (Win64, вызов функции с 6 аргументами)
| | Значение | Комментарий |
| ———- | ————– | ————————————————– |
| rsp+0 | Shadow RCX | 1-й аргумент (копия) |
| rsp+8 | Shadow RDX | 2-й |
| rsp+16 | Shadow R8 | 3-й |
| rsp+24 | Shadow R9 | 4-й |
| rsp+32 | 5-й | 5-й аргумент (через стек) |
| rsp+40 | 6-й | 6-й аргумент |
—
Мини-конспект для реверса Win64
Все функции вызываются с выравнием и shadow space.
До 4-х аргументов — через rcx/rdx/r8/r9 (или xmm0–xmm3).
5+ — через стек по адресам выше shadow space.
Внутри функции часто нет frame pointer, локальные и временные размещаются через rsp.
После call caller обязан вернуть rsp к прежнему значению (add rsp, XX).
—
Примеры для практики
Пример 1: Простая функция (Win64)
int foo(int a, int b) { return a+b; }
foo:
mov eax, ecx ; a
add eax, edx ; b
ret
mov ecx, 5
mov edx, 6
sub rsp, 28h ; выделил shadow space + выравнивание
call foo
add rsp, 28h
Пример 2: С vararg (Win64, printf)
Первые 4 аргумента — rcx, rdx, r8, r9 + копии кладутся в shadow space.
5-й и выше на стек
Внутри функции printf читает все аргументы из shadow space + стек.
—
Заключение
Win64 calling convention:
Первые 4 аргумента — через rcx, rdx, r8, r9
Shadow space (32 байта) обязателен перед каждым call
5+ аргументов размещаются на стеке выше shadow
Возврат значения — через rax
Frame pointer часто не используется
После call восстановить rsp!
—