Что такое 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)

Главным отличием от двух соглашениях выше является то, что аргументы кладутся в регистры, если это возможно, что позволяет увеличить скорость вызова функции, потому что обратиться к регистру быстрее, чем к стеку.

Про память рекомендую посмотреть это видео от AlekOs

Стоит указать, что в 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)


  1. DownFvll
    08.07.2022 10:03
    +2

    Интересная статья, однако стоило бы указать, к какой именно архитектуре относятся данные соглашения.

    Например, везде в коде однозначно используется x86, однако в vectorcall затрагиваются регистры x64, хотя, безусловно, данное соглашение существует и для x86, и для x64 (https://docs.microsoft.com/ru-ru/cpp/cpp/vectorcall?view=msvc-170):

    (RCX/XMM0, RDX/XMM1, R8/XMM2, R9/XMM3 + XMM0-XMM5/YMM0-YMM5)

    Хотелось бы увидеть чуть более подробное описание vectorcall.

    Также стоило бы упомянуть, что вы используете Win32 ABI, т.к. в System V ABI есть отличия для наименований, количестве передаваемых в регистрах аргументов для fastcall и т.д.


  1. nin-jin
    08.07.2022 11:01
    +3

    1. gohrytt
      08.07.2022 17:23
      +1

      Но ссылкку приложить - не, да зачем?


      1. nin-jin
        09.07.2022 13:11

  1. gleb_l
    08.07.2022 20:24
    +1

    Можно заметить, что при всей несимметричности stdcall (белье в стиралку загружает один эктор, а выплевывает после стирки она сама столько, сколько разумеет), ret <n> - самая эффективная команда выбрасывания отработанного стекового кадра, которая при этом ещё и не портит флаги.


    1. DmitryKoterov
      09.07.2022 09:59

      Ага, только переменное число аргументов так не передать. Потому и не кусаютъ.


      1. gleb_l
        09.07.2022 11:41

        Удивительно, что нет какого-нибудь ret cx - это скорее наследие изначально контроллерного предназначения x86, как и in/out <immed> в i8080


  1. QtRoS
    09.07.2022 11:02
    +1

    Возможно кому-то будет полезно, как легко не путать stdcall и cdecl: cdecl начинается с "C", названия языка программирования, в котором есть printf, известная функция с переменным числом аргументов, которое известно только снаружи функции. Соответственно и очищается стек снаружи.


    1. nin-jin
      09.07.2022 12:06

      И как же printf выполняет свою задачу не зная даже сколько аргументов ей передали?


      1. gleb_l
        09.07.2022 12:46

        В мире существует всего два способа для этого: передать кол-во заранее в условленном месте, и «копать, пока лопата не стукнется о сундук» - для printf используются оба - первый для передачи количества аргументов, второй - для вывода количества символов в каждой выводимой строке )


        1. nin-jin
          09.07.2022 13:10

          То есть и стек она вполне может за собой подчищать.