Соглашения о вызовах в языке C++¶
Соглашения о вызовах в языке C++ — это набор правил, определяющих, каким образом функция получает аргументы от вызывающего кода и возвращает результат. Эти правила регламентируют порядок передачи параметров (в регистрах процессора или через стек), порядок их размещения, способ очистки стека после вызова, а также правила декорирования (искажения) имён функций. Выбор соглашения о вызовах влияет на производительность, совместимость с внешним кодом (например, с API операционной системы) и возможность использования функций из библиотек, написанных на других языках программирования.
В языке C++ соглашение о вызовах не является частью стандарта, а определяется конкретным компилятором и целевой платформой (архитектурой процессора и операционной системой). Стандарт лишь гарантирует, что вызов функции внутри одной единицы трансляции происходит согласованно. Для межмодульного взаимодействия программист должен явно указывать соглашение с помощью ключевых слов или атрибутов компилятора.
¶Основные соглашения для x86 (32-бита)
На платформе x86 (32-битная архитектура) исторически сложилось несколько основных соглашений, различающихся, прежде всего, способом очистки стека.
¶cdecl
Соглашение cdecl (от англ. C declaration) является стандартным для языка C и C++ на платформе x86. Его ключевые особенности:
- Аргументы передаются через стек справа налево (последний аргумент помещается в стек первым).
- Очистку стека выполняет вызывающий код. Это позволяет реализовать функции с переменным числом аргументов (variadic functions), такие как
printf. - Имя функции декорируется префиксом подчёркивания (например,
_func). - Возвращаемое значение, если оно помещается в регистр, передаётся через регистр
EAX(для целых чисел и указателей) или через пару регистровEDX:EAX(для 64-битных значений).
¶stdcall
Соглашение stdcall используется в основном для вызова функций Win32 API. Отличия от cdecl:
- Аргументы также передаются справа налево через стек.
- Очистку стека выполняет вызываемая функция. Это сокращает размер генерируемого кода, но делает невозможным использование функций с переменным числом аргументов.
- Имя функции декорируется префиксом подчёркивания и суффиксом
@с количеством байт, занимаемых аргументами (например,_func@8).
¶fastcall
Соглашение fastcall предназначено для ускорения вызовов за счёт передачи первых двух (или трёх, в зависимости от компилятора) аргументов в регистрах ECX и EDX, а остальных — через стек. Очистка стека, как и в stdcall, выполняется вызываемой функцией. Это соглашение часто используется компиляторами для оптимизации внутренних вызовов, но не является стандартизированным.
¶thiscall
Соглашение thiscall используется для вызова нестатических функций-членов класса. В нём указатель на объект (this) передаётся в регистре ECX, а остальные аргументы — через стек. Очистка стека выполняется вызываемой функцией, если число аргументов фиксировано, или вызывающим кодом, если функция имеет переменное число аргументов.
¶Соглашения для x86-64 (64-бита)
На архитектуре x86-64 количество соглашений значительно сокращено. Основным стандартом является System V AMD64 ABI (используется в Linux, macOS, BSD) и Microsoft x64 calling convention (используется в Windows).
¶System V AMD64 ABI
Это соглашение применяется в большинстве Unix-подобных систем. Его особенности:
- Первые шесть целочисленных аргументов передаются в регистрах
RDI,RSI,RDX,RCX,R8,R9. - Первые восемь аргументов с плавающей точкой передаются в регистрах
XMM0–XMM7. - Остальные аргументы передаются через стек справа налево.
- Очистку стека выполняет вызывающий код.
- Возвращаемое значение передаётся в регистре
RAX(илиRDX:RAXдля 64-битных значений). - Для функций с переменным числом аргументов в регистре
ALпередаётся количество используемых регистровXMM.
¶Microsoft x64 calling convention
Используется в Windows для 64-битных приложений. Отличается от System V:
- Первые четыре целочисленных аргумента передаются в регистрах
RCX,RDX,R8,R9. - Первые четыре аргумента с плавающей точкой передаются в регистрах
XMM0–XMM3. - Вызывающий код всегда резервирует в стеке 32 байта (теневые слоты) для возможности сохранения регистровых аргументов.
- Очистку стека выполняет вызывающий код.
- Возвращаемое значение передаётся в регистре
RAX.
¶Указание соглашений в коде
В C++ соглашение о вызовах задаётся с помощью ключевых слов, специфичных для компилятора. В Microsoft Visual C++ используются модификаторы __cdecl, __stdcall, __fastcall, __thiscall, __vectorcall. В GCC и Clang для платформы x86 используются атрибуты __attribute__((cdecl)), __attribute__((stdcall)) и другие.
Пример объявления функции с соглашением stdcall в Visual C++: ``cpp __stdcall void Func(int a, int b); ``
В стандарте C++11 и новее появился атрибут [[gnu::stdcall]] для GCC/Clang, однако он не является переносимым и не входит в стандарт.
¶Практическое значение
Выбор соглашения о вызовах критически важен при разработке библиотек, которые должны использоваться из других языков программирования или из кода, скомпилированного другим компилятором. Несоответствие соглашений приводит к ошибкам времени выполнения, порче стека или неверной интерпретации аргументов. Также соглашения влияют на производительность: передача аргументов в регистрах (fastcall, System V) значительно быстрее, чем работа со стеком, особенно для небольших функций, вызываемых в циклах.
¶Интересные факты
- Соглашение System V AMD64 ABI является обязательным для всех программ, работающих в окружении Unix, и его нарушение может привести к падению программы.
- В 64-битных системах Windows не существует соглашения
thiscall— вызов методов класса использует обычное Microsoft x64 соглашение, где указательthisпередаётся как первый аргумент в регистреRCX. - Некоторые компиляторы поддерживают автоматический выбор оптимального соглашения для внутренних вызовов, игнорируя указанное программистом, что может приводить к неожиданному поведению при отладке.
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →


