====== Соглашения - 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 |
|__fastcall |R->L, 1 ECX, 2 EDX, потом стек|Регистр+стек|Callee|EAX|@func@N |Оптимизация |
|thiscall |this via ECX, потом стек|ECX/стек |Caller |EAX |манглинг |C++ методы |
---
=== __cdecl ===
* Аргументы: справа налево через стек.
* Стек чистит вызывающий (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
=== Локальные переменные и аргументы ===
* Аргументы функции: [ebp+8], [ebp+12] и т.д. ([[ebp+4]] — return address).
* Локальные переменные: [ebp-4], [ebp-8], ..., для каждой следующей — минус по 4 байта.
=== Пример с комментариями ===
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 (выход функции) ===
add rsp, 28h
ret
=== Локальные переменные ===
* Выделяются через sub rsp, XX.
* Доступ — [rsp+смещение] (если не используется frame pointer).
=== Дизассемблированный 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
* Если есть 5-й и далее аргумент: размещается в памяти, брать через [rsp+40h], [rsp+48h] и т.д.
=== Пример вызова (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!
---