Соглашения вызова
TL;DR: Для x86 в винде используется Microsoft x64 calling convention, в линуксе System V ABI.
В x86 - 8 32-битных регистров.
В x64 - 16 64-битных регистров. 64-битные версии Е* регистров называются R*. Дополнительные регистры получили названия r8...r15
.
- ESP - stack pointer, вершина стека. Указывает на текущую вершину стэка. Операции
push
иpop
читают из ESP адрес, по которому обращаться к стэку, а в конце раобты меняют значение ESP. После пролога функции все локальные переменные и аргументы оказываются выше ESP. - EBP - frame pointer, вершина фрейма. Все аргументы функции и адрес возврата находятся выше его, а локальные переменные - ниже.
Синтаксис MOV
mov %eax, %edx
Есть два синтаксиса:
- Intel: перемещает EDX в EAX. Используется во всяких MASM, TASM, NASM, FASM.
- AT&T: перемещает EAX в EDX. Используется во всех Unix-системах.
Так как работаем в основном в линуксе, то в большинстве случаев будет встречаться вариант, когда mov %eax, %edx
означает присвовить регистру edx значение регистра edx.
Как определить используемое соглашение
Делаем nm
на интересующей библиотеке и смотрим на имена символов.
Если начинаются с _
и не содержат @
, то это __cdecl
.
Если начинаются с _
и содержат @
, то это __stdcall
.
Если начинается с @
и содержит еще одну @
, то это __fastcall
.
Пролог
Обычно функция начинается с автосгенерированного пролога:
push ebp ; сохраняем вершину фрейма, чтобы после выхода из функции вызывающая функция смогла обращаться к своим локальным переменным
mov esp, ebp ; перемещаем вершину фрейма вниз, записывая в нее текущее значение вершины стека
sub esp, 20 ; локальные переменные функции находятся на стеке и ниже вершины фрейма, поэтому уменьшаем вершину стека на 20 байт, чтобы выделить 20 байт под локальные переменные
После этого EBP будет указывать на начало текущего фрейма, а ESP - на его конец.
Затем в коде обращение к локальным переменным может выглядеть так:
mov eax, [ebp-4] ; сохраняем eax в первую локальную переменную
mov ebx, [ebp-8] ; читаем вторую локальную переменную в регистр ebx
Эпилог
А выход из функции (эпилог) выглядит так:
mov ebp, esp; ; грохаем все локальные переменные со стека
pop ebp; ; восстанавливаем предыдущее значение ebp, чтобы оно указывало на вершину фрейма вызывающей функции
ret ; возвращаемся в вызывающую функцию
Для перечисленных ниже соглашений (кроме [cdecl]
) перед возвратом значений из функции подпрограмма обязана восстановить значения сегментных регистров, регистров esp
и ebp
. Значения остальных регистров могут не восстанавливаться.
Если размер возвращаемого значения функции не больше размера регистра eax
, возвращаемое значение сохраняется в регистре eax
. Иначе, возвращаемое значение сохраняется на вершине стека, а указатель на вершину стека сохраняется в регистре eax
.
Соглашения
Пусть у нас есть такая функция:
int sumExample(int a, int b)
{
return a + b;
}
Которая вызывается вот так:
int c = sumExample(2, 3);
System V ABI
https://wiki.osdev.org/System_V_ABI
Функции вызываются командами call
и callq
, которые пушат на стек адрес следующей инструкции и вызывают джамп. Возврат осуществляется командой ret
, которая попает со стека адрес возврата и джампает на него. Перед вызовом call
стэк выравнивается по 16 байтам.
Аргументы, переданные через стек, могут быть модифированы вызываемой функцией.
x86
Аргументы передаются через стек.
Функции сохраняют значения регистров ebx, esi, edi, ebp, esp
, а регистры eax, ecx, edx
могут быть затерты. Значение возвращается через регистр eax
, а если он больше 32 бит, то верхние 32 бита идут в edx
.
x64
Аргументы передаются в регистрах rdi, rsi, rdx, rcx, r8, r9
, остальные - на стеке, справа налево.
Функции сохраняют значения регистров rbx, rsp, rbp, r12, r13, r14, r15
, а регистры rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11
могут быть затерты. Значение возвращается через регистр rax
, а если он больше 64 бит, то верхние 64 бита идут в rdx
.
Пример:
sum.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <sumExample>:
0: 55 push %rbp ; бэкапим вершину стека родителя
1: 48 89 e5 mov %rsp,%rbp ; заводим себе фрейм
4: 89 7d fc mov %edi,-0x4(%rbp) ; кладем в локальную переменную первый аргумент
7: 89 75 f8 mov %esi,-0x8(%rbp) ; кладем в локальную переменную второй аргумент
a: 8b 55 fc mov -0x4(%rbp),%edx ; из локальной переменной перемещаем первый аргумент в регистр edx
d: 8b 45 f8 mov -0x8(%rbp),%eax ; из локальной переменной перемещаем второй аргумент в регистр eax
10: 01 d0 add %edx,%eax ; складываем регистры, результат окажется в eax
12: 5d pop %rbp ; ресторим вершину стека родителя
13: c3 retq ; возвращаемся
0000000000000014 <main>:
14: 55 push %rbp ; бэкапим вершину стека родителя
15: 48 89 e5 mov %rsp,%rbp ; заводим себе фрейм
18: 48 83 ec 10 sub $0x10,%rsp ; заводим место на стеке для 2 переменных (хз зачем, вроде оно тут не используется)
1c: be 03 00 00 00 mov $0x3,%esi ; пишем 3 в регистр первого аргумента
21: bf 02 00 00 00 mov $0x2,%edi ; пишем 2 в регистр второго аргумента
26: e8 00 00 00 00 callq 2b <main+0x17> ; вызываем sumExample
2b: 89 45 fc mov %eax,-0x4(%rbp) ; кладем результат в локальную переменную
2e: 90 nop
2f: c9 leaveq
30: c3 retq
cdecl
cdecl
— соглашение о вызовах, используемое компиляторами для языка C.
Аргументы функций передаются через стек, справа налево. Аргументы, размер которых меньше 4-х байт, расширяются до 4-х байт. Очистку стека производит вызывающая программа. Это основной способ вызова функций с переменным числом аргументов (например, [printf()](https://ru.wikipedia.org/wiki/Printf)
). Названия функций имеют префикс '_'. Способы получения возвращаемого значения функции приведены в таблице.
Перед вызовом функции вставляется код, называемый прологом и выполняющий следующие действия:
- сохранение значений регистров, используемых внутри функции;
- запись в стек аргументов функции
После вызова функции вставляется код, называемый эпилогом и выполняющий следующие действия:
- восстановление значений регистров, сохранённых кодом пролога;
- очистка стека (от локальных переменных и аргументов).
Пример:
; sumExample(2, 3)
; записываем в стек аргументы функции справа налево
push 3
push 2
; вызываем функцию
call _sumExample
; очищаем стек от аргументов
add esp,8
; копишуем возвращаемое значение (eax) в локальную переменную c
mov dword ptr [c],eax
Вызываемая функция выглядит так:
; пролог
push ebp
mov ebp,esp ; после этого над EBP находятся аргументы 2 и 3, а под ним будут локальные переменные
sub esp,0C0h ; выделяем на стеке место для 12 байт под локальные переменные
push ebx
push esi
push edi
lea edi,[ebp-0C0h] ; lea = load effective address, получаем адрес в памяти для вершины стека
mov ecx,30h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
; return a + b;
mov eax,dword ptr [a]
add eax,dword ptr [b]
; эпилог
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
stdcall или winapi
stdcall
или winapi
— соглашение о вызовах, применяемое в Windows для вызова функций WinAPI.
Аргументы функций передаются через стек, справа налево. Очистку стека производит вызываемая подпрограмма. Названия функций имеют префикс '_' и постфикс вида '@+необходимое количество байт на стэке'.
Пример:
; // push arguments to the stack, from right to left
push 3
push 2
; // call the function
call _sumExample@8
; // copy the return value from EAX to a local variable (int c)
mov dword ptr [c],eax
Код функции:
; // function prolog goes here (the same code as in the __cdecl example)
; // return a + b;
mov eax,dword ptr [a]
add eax,dword ptr [b]
; // function epilog goes here (the same code as in the __cdecl example)
; // cleanup the stack and return
ret 8
Так как очистку стэка производит вызываемая программа, то размер бинарников получается меньше, чем у cdecl. Однако функциям с переменным числом аргументов приходится использовать cdecl, потому что только вызывающий код знает количество аргументов.
fastcall
fastcall
— общее название соглашений, передающих параметры через регистры (обычно это самый быстрый способ, отсюда название). Если для сохранения всех параметров и промежуточных результатов регистров не достаточно, используется стек.
Соглашение о вызовах fastcall
не стандартизировано, поэтому используется только для вызова процедур и функций, не экспортируемых из исполняемого модуля и не импортируемых извне.
В компиляторах фирмы Borland для соглашения __fastcall, называемого также register[5], параметры передаются слева направо в регистрах eax, edx, ecx, а если параметров больше трёх — в стеке, также слева направо. Исходное значение указателя на вершину стека (значение регистра esp) возвращает вызываемая подпрограмма.
В 32-разрядной версии компилятора фирмы Microsoft, а также в компиляторе GCC, соглашение __fastcall, также называемое __msfastcall, определяет передачу первых двух параметров слева направо в регистрах ecx и edx, а остальные параметры передаются справа налево в стеке. Очистку стека производит вызываемая подпрограмма.
Названия функций начинаются с @ и заканчиваются на @ + необходимое количество байт на стэке.
Пример:
; // put the arguments in the registers EDX and ECX
mov edx,3
mov ecx,2
; // call the function
call @fastcallSum@8
; // copy the return value from EAX to a local variable (int c)
mov dword ptr [c],eax
Код функции:
; // function prolog
push ebp
mov ebp,esp
sub esp,0D8h
push ebx
push esi
push edi
push ecx
lea edi,[ebp-0D8h]
mov ecx,36h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-14h],edx
mov dword ptr [ebp-8],ecx
; // return a + b;
mov eax,dword ptr [a]
add eax,dword ptr [b]
;// function epilog
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
thiscall
thiscall — соглашение о вызовах, используемое компиляторами для языка C++ при вызове методов классов в объектно-ориентированном программировании.
Аргументы функции передаются через стек, справа налево. Очистку стека производит вызывающая программа. Соглашение thiscall отличается от cdecl соглашения только тем, что указатель на объект, для которого вызывается метод (указатель this), записывается в регистр ecx[8]. Если же используется функция с переменным количеством аргументов, то this кладется на стэк последним.
push 3
push 2
lea ecx,[sumObj]
call ?sum@CSum@@QAEHHH@Z ; CSum::sum
mov dword ptr [s4],eax
Код функции:
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
push ecx
lea edi,[ebp-0CCh]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-8],ecx
mov eax,dword ptr [a]
add eax,dword ptr [b]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8
To cut a long story short, we'll outline the main differences between the calling conventions:
__cdecl
is the default calling convention for C and C++ programs. The advantage of this calling convetion is that it allows functions with a variable number of arguments to be used. The disadvantage is that it creates larger executables.__stdcall
is used to call Win32 API functions. It does not allow functions to have a variable number of arguments.__fastcall
attempts to put arguments in registers, rather than on the stack, thus making function calls faster.Thiscall
calling convention is the default calling convention used by C++ member functions that do not use variable arguments.
X64
В X64 все Е* регистры называются R*. https://msdn.microsoft.com/ru-ru/library/9z1stfyw.aspx
В X64 используется только __fastcall, причем регистры используются для передачи первых 4 аргументов. Аргументы передаются в регистрах RCX, RDX, R8 и R9.