Открыть сервис

Соглашения о вызовах в языке 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.
  • Первые восемь аргументов с плавающей точкой передаются в регистрах XMM0XMM7.
  • Остальные аргументы передаются через стек справа налево.
  • Очистку стека выполняет вызывающий код.
  • Возвращаемое значение передаётся в регистре RAX (или RDX:RAX для 64-битных значений).
  • Для функций с переменным числом аргументов в регистре AL передаётся количество используемых регистров XMM.

Microsoft x64 calling convention

Используется в Windows для 64-битных приложений. Отличается от System V:

  • Первые четыре целочисленных аргумента передаются в регистрах RCX, RDX, R8, R9.
  • Первые четыре аргумента с плавающей точкой передаются в регистрах XMM0XMM3.
  • Вызывающий код всегда резервирует в стеке 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 →