How's that again?

Соглашения вызова

Вики

CodeProject

(https://stackoverflow.com/questions/1395591/what-is-exactly-the-base-pointer-and-stack-pointer-to-what-do-they-point)

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)). Названия функций имеют префикс '_'. Способы получения возвращаемого значения функции приведены в таблице.

Таблица 1

Перед вызовом функции вставляется код, называемый прологом и выполняющий следующие действия:

  • сохранение значений регистров, используемых внутри функции;
  • запись в стек аргументов функции

После вызова функции вставляется код, называемый эпилогом и выполняющий следующие действия:

  • восстановление значений регистров, сохранённых кодом пролога;
  • очистка стека (от локальных переменных и аргументов).

Пример:

    ; 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.