Что такое Calling Conventions?
Это стандартизированные методы реализации и вызова функций.
Соглашение о вызовах опредяют как функция вызывается, как функция управляет стеком и стековым кадром, как аргументы передаются в функцию, как функция возвращает значения.
Я разберу несколько наиболее часто используемых
stdcall (Standart Calling Convention)
STDCALL это стандартное соглашение для Win32 API. В данном соглашение, аргументы передаются справа налево и очистка стека ложится на вызываемую функцию. Для передачи аргументов используется стек, т.е. перед вызовом нужно положить аргументы на стек. Возвращаемое значение записывается в регистр eax.
Пример вызова абстрактной функции, которая принимает 3 4-байтовых аргумента.
PUSH 0x5
PUSH 0x4
PUSH 0x8
CALL _func@12
В вызываемой функции:
...
mov eax, 0x75 ; Записываем 0x75 в eax, т.е. в регистр, который отвечает за возвращаемое значение
...
ret 12 ; Очищаем стек на 12 байт (3 аргумента по 4 байта)
Давайте рассмотрим вызов функции MessageBox из Win32 API, используя NASM.
; Hello World with winapi messagebox using stdcall calling convention
extern _MessageBoxA@16
extern _ExitProcess@4
global _main
section .data
message db "Hello, Habr!", 0
title db "Habr!", 0
section .text
_main:
push dword 0x00
push dword title
push dword message
push dword 0
call _MessageBoxA@16
push 0
call _ExitProcess@4
Приведу пример вызова на языке программирования C.
MessageBox(0, message, title, MB_OK);
Разберем то, как мы передаем аргументы:
Первые 4 строки - передача аргументов, согласно STDCALL, то есть справа налево.
Рассмотрим, какие аргументы требуются для вызова функции MessageBox:
int MessageBox(
[in, optional] HWND hWnd,
[in, optional] LPCTSTR lpText,
[in, optional] LPCTSTR lpCaption,
[in] UINT uType
);
MessageBox принимает 4 аргумента:
HWND - Дескриптор родительского окна. Мы его оставляем 0, так как у нас его нет.
lpText - Строка, которая будет выведена в окне. В нашем случае это "Hello, Habr!"
lpCaption - Заголовок окна. В нашем случае это "Habr!"
uType - Тип MessageBox'a. Мы указываем 0x00, то есть MESSAGEBOX_OK. В окошке будет только одна кнопка - ОК.
С другими типами MessageBox вы можете ознакомиться в официальной документации
Мы видим в каком порядке принимаются параметры при вызове функции, но на стек мы кладем в обратном порядке, то есть первый аргумент, который мы кладем на стек - 0x00 - uType, второй ttl - lpCaption. Это и есть одна из особенностей данного соглашения о вызовах, вторая особенность - eax используется как регистр для возвращаемого значения.
Еще стоит упомянуть имена функций. Они начинаются с символа нижнего подчеркивания и заканчиваются на такую конструкцию "@[Количество байт, которые нужно выделить для аргументов.]"
cdecl (C calling convention)
Стандартное соглашение о вызовах для программ на C/C++.
В данном соглашение аргументы передаются справа налево и кладутся на стек, как и в [[#STDCALL Standart Calling Convention|stdcall]], возвращаемое значение кладется в регистр EAX. Но вот стек уже очищается функцией, которая вызывает. Имена функций начинаются с символа нижнего подчеркивания, без указания количества байт для аргументов к конце.
Пример:
push 18
push 19
call _add
add esp, 8
fastcall (Fast calling convention)
Главным отличием от двух соглашениях выше является то, что аргументы кладутся в регистры, если это возможно, что позволяет увеличить скорость вызова функции, потому что обратиться к регистру быстрее, чем к стеку.
Стоит указать, что в x86 только 2 регистра могут быть задействованы для передачи аргументов, остальные будут храниться в стеке.
Аргументы также передаются справа налево. Стек чистится вызываемой функцией.
Пример:
...
mov ecx, 0x3
mov edx, 0x1
call @sum@8
...
К названием функций добавляется @ в начале и следующая конструкция в конце "@[Количество байт, которые нужно выделить для аргументов.]"
thiscall
Это соглашение о вызовах используется для вызова нестатических функций-членов C++.
Так как используется только для нестатических функций-членов, то у нас есть указатель this, который передается в ECX, стек очищается вызываемой функцией, аргументы передаются справа налево на стек, возвращаемое значение помещается в регистр EAX.
Пример:
mov ecx, SomeObj
push b
push a
call _MyMethod
vectorcall
Это соглашение о вызовах, которое было выпущено для увеличения эффективности и скорости обработки, позволяя передавать векторные типы данных в регистры. (RCX/XMM0, RDX/XMM1, R8/XMM2, R9/XMM3 + XMM0-XMM5/YMM0-YMM5).
Стек очищается функцией, которая вызывает, аргументы передаются справа налево.
Итого
Соглашение о вызове - описание правил вызова функций, в которое входит способы передачи аргументов, способы вызова, способы возврата значения, именования функций и очистки памяти после завершения работы функции.
В компиляторе от Microsoft (cl) их можно использовать вот так:
stdcall (/Gz)
fastcall (/Gr)
cdecl (/Gd)
vectorcall (/Gv)
Комментарии (11)
nin-jin
08.07.2022 11:01+3gohrytt
08.07.2022 17:23+1Но ссылкку приложить - не, да зачем?
nin-jin
09.07.2022 13:11Как видите, Хабр её потерял: https://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions
gleb_l
08.07.2022 20:24+1Можно заметить, что при всей несимметричности stdcall (белье в стиралку загружает один эктор, а выплевывает после стирки она сама столько, сколько разумеет), ret <n> - самая эффективная команда выбрасывания отработанного стекового кадра, которая при этом ещё и не портит флаги.
DmitryKoterov
09.07.2022 09:59Ага, только переменное число аргументов так не передать. Потому и не кусаютъ.
gleb_l
09.07.2022 11:41Удивительно, что нет какого-нибудь ret cx - это скорее наследие изначально контроллерного предназначения x86, как и in/out <immed> в i8080
QtRoS
09.07.2022 11:02+1Возможно кому-то будет полезно, как легко не путать stdcall и cdecl: cdecl начинается с "C", названия языка программирования, в котором есть printf, известная функция с переменным числом аргументов, которое известно только снаружи функции. Соответственно и очищается стек снаружи.
nin-jin
09.07.2022 12:06И как же printf выполняет свою задачу не зная даже сколько аргументов ей передали?
gleb_l
09.07.2022 12:46В мире существует всего два способа для этого: передать кол-во заранее в условленном месте, и «копать, пока лопата не стукнется о сундук» - для printf используются оба - первый для передачи количества аргументов, второй - для вывода количества символов в каждой выводимой строке )
DownFvll
Интересная статья, однако стоило бы указать, к какой именно архитектуре относятся данные соглашения.
Например, везде в коде однозначно используется x86, однако в vectorcall затрагиваются регистры x64, хотя, безусловно, данное соглашение существует и для x86, и для x64 (https://docs.microsoft.com/ru-ru/cpp/cpp/vectorcall?view=msvc-170):
Хотелось бы увидеть чуть более подробное описание vectorcall.
Также стоило бы упомянуть, что вы используете Win32 ABI, т.к. в System V ABI есть отличия для наименований, количестве передаваемых в регистрах аргументов для fastcall и т.д.