Стандартный вызов и cdecl

Существует (среди прочего) два типа соглашений о вызовах - stdcall и cdecl. У меня есть несколько вопросов по ним:

  1. Когда вызывается функция cdecl, как вызывающая сторона узнает, должна ли она освободить стек? На месте вызова знает ли вызывающая сторона, является ли вызываемая функция функцией cdecl или stdcall? Как это работает? Как звонящий узнает, должен ли он освободить стек или нет? Или это ответственность линкеров?
  2. Если функция, которая объявлена ​​как stdcall, вызывает функцию (которая имеет соглашение о вызовах как cdecl), или наоборот, будет ли это неуместным?
  3. В общем, можно ли сказать, что вызов будет быстрее - cdecl или stdcall?

9 ответов

Раймонд Чен дает хороший обзор того, что __stdcall а также __cdecl делает

(1) Вызывающая сторона "знает", как очистить стек после вызова функции, потому что компилятор знает соглашение о вызовах этой функции и генерирует необходимый код.

void __stdcall StdcallFunc() {}

void __cdecl CdeclFunc()
{
    // The compiler knows that StdcallFunc() uses the __stdcall
    // convention at this point, so it generates the proper binary
    // for stack cleanup.
    StdcallFunc();
}

Можно не соответствовать соглашению о вызовах, например так:

LRESULT MyWndProc(HWND hwnd, UINT msg,
    WPARAM wParam, LPARAM lParam);
// ...
// Compiler usually complains but there's this cast here...
windowClass.lpfnWndProc = reinterpret_cast<WNDPROC>(&MyWndProc);

Так много примеров кода делают это неправильно, это даже не смешно. Это должно быть так:

// CALLBACK is #define'd as __stdcall
LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg
    WPARAM wParam, LPARAM lParam);
// ...
windowClass.lpfnWndProc = &MyWndProc;

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

(2) Оба способа должны работать. На самом деле, это происходит довольно часто, по крайней мере, в коде, который взаимодействует с Windows API, потому что __cdecl по умолчанию для программ на C и C++ в соответствии с компилятором Visual C++, а функции WinAPI используют __stdcall конвенция

(3) Между ними не должно быть реальной разницы в производительности.

В CDECL аргументы помещаются в стек в обратном порядке, вызывающая сторона очищает стек, и результат возвращается через реестр процессора (позже я назову его "регистр A"). В STDCALL есть одно отличие: вызывающий не очищает стек, а вызывающий - очищает.

Вы спрашиваете, какой из них быстрее. Ни один. Никто. Вы должны использовать соглашение о вызовах как можно дольше. Изменяйте соглашение только в том случае, если выхода нет, когда используются внешние библиотеки, требующие использования определенного соглашения.

Кроме того, существуют другие соглашения, которые компилятор может выбрать по умолчанию, т.е. компилятор Visual C++ использует FASTCALL, который теоретически быстрее из-за более широкого использования регистров процессора.

Обычно вы должны дать правильную подпись соглашения о вызове функциям обратного вызова, переданным в какую-либо внешнюю библиотеку, т.е. qsort из библиотеки C должен быть CDECL (если компилятор по умолчанию использует другое соглашение, тогда мы должны пометить обратный вызов как CDECL), или различные обратные вызовы WinAPI должны быть STDCALL (весь WinAPI является STDCALL).

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

А ниже приведен пример, показывающий, как это делает компилятор:

/* 1. calling function in C++ */
i = Function(x, y, z);

/* 2. function body in C++ */
int Function(int a, int b, int c) { return a + b + c; }

Cdecl:

/* 1. calling CDECL 'Function' in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x'
call (jump to function body, after function is finished it will jump back here, the address where to jump back is in registers)
move contents of register A to 'i' variable
pop all from the stack that we have pushed (copy of x, y and z)

/* 2. CDECL 'Function' body in pseudo-assembler */
/* Now copies of 'a', 'b' and 'c' variables are pushed onto the stack */
copy 'a' (from stack) to register A
copy 'b' (from stack) to register B
add A and B, store result in A
copy 'c' (from stack) to register B
add A and B, store result in A
jump back to caller code (a, b and c still on the stack, the result is in register A)

STDCALL:

/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x'
call
move contents of register A to 'i' variable

/* 2. STDCALL 'Function' body in pseaudo-assembler */
pop 'a' from stack to register A
pop 'b' from stack to register B
add A and B, store result in A
pop 'c' from stack to register B
add A and B, store result in A
jump back to caller code (a, b and c are no more on the stack, result in register A)

Я заметил сообщение о том, что не имеет значения, если вы позвоните __stdcall из __cdecl или наоборот. Оно делает.

Причина: с __cdecl аргументы, которые передаются вызываемым функциям, удаляются из стека вызывающей функцией, в __stdcallаргументы удаляются из стека вызываемой функцией. Если вы позвоните __cdecl функция с __stdcallстек вообще не очищается, поэтому, когда __cdecl использует основанную на стеке ссылку для аргументов или обратный адрес будет использовать старые данные в текущем указателе стека. Если вы позвоните __stdcall функция от __cdecl, __stdcall Функция очищает аргументы в стеке, а затем __cdecl функция делает это снова, возможно удаляя вызывающие функции, возвращающие информацию.

Соглашение Microsoft для C пытается обойти это путем искажения имен. __cdecl Функция имеет префикс с подчеркиванием. __stdcall префиксы функций с подчеркиванием и суффиксом со знаком "@" и количеством байтов, которые будут удалены. Например __cdecl f(x) связан как _x, __stdcall f(int x) связан как _f@4 где sizeof(int) 4 байта)

Если вам удалось обойти компоновщик, наслаждайтесь беспорядком отладки.

Я хочу улучшить ответ @adf88. Я чувствую, что псевдокод для STDCALL не отражает того, как это происходит в реальности. "a", "b" и "c" не извлекаются из стека в теле функции. Вместо этого они появляются ret инструкция (ret 12 в этом случае), который одним махом переходит обратно к вызывающей стороне и в то же время извлекает "a", "b" и "c" из стека.

Вот моя версия, исправленная в соответствии с моим пониманием:

STDCALL:

/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then copy of 'y', then copy of 'x'
call
move contents of register A to 'i' variable

/* 2. STDCALL 'Function' body in pseaudo-assembler */ copy 'a' (from stack) to register A copy 'b' (from stack) to register B add A and B, store result in A copy 'c' (from stack) to register B add A and B, store result in A jump back to caller code and at the same time pop 'a', 'b' and 'c' off the stack (a, b and c are removed from the stack in this step, result in register A)

Это указано в типе функции. Когда у вас есть указатель на функцию, он считается cdecl, если явно не stdcall. Это означает, что если вы получите указатель stdcall и указатель cdecl, вы не сможете их поменять. Два типа функций могут вызывать друг друга без проблем, это просто получение одного типа, когда вы ожидаете другого. Что касается скорости, они оба выполняют одни и те же роли, просто в очень немного другом месте, это действительно не имеет значения.

Вызывающая сторона и вызываемая сторона должны использовать одно и то же соглашение в момент вызова - это единственный способ, которым он может надежно работать. И вызывающая сторона, и вызываемая сторона следуют заранее определенному протоколу - например, кому нужно очистить стек. Если соглашения не соответствуют вашей программе, вы столкнетесь с неопределенным поведением - скорее всего, просто сбоит.

Это требуется только для сайта вызова - сам код вызова может быть функцией с любым соглашением о вызовах.

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

Эти вещи зависят от компилятора и платформы. Ни C, ни стандарт C++ ничего не говорят о соглашениях о вызовах, за исключением extern "C" в C++.

как звонящий узнает, должен ли он освободить стек?

Вызывающая сторона знает соглашение о вызове функции и соответственно обрабатывает вызов.

На месте вызова знает ли вызывающая сторона, является ли вызываемая функция функцией cdecl или stdcall?

Да.

Как это работает?

Это часть объявления функции.

Как звонящий узнает, должен ли он освободить стек или нет?

Вызывающий абонент знает соглашения о вызовах и может действовать соответственно.

Или это ответственность линкеров?

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

Если функция, которая объявлена ​​как stdcall, вызывает функцию (которая имеет соглашение о вызовах как cdecl), или наоборот, будет ли это неуместным?

Нет. Зачем это?

В общем, можно ли сказать, что вызов будет быстрее - cdecl или stdcall?

Я не знаю. Попробуй это.

а) Когда вызывающая функция вызывает функцию cdecl, как вызывающая сторона узнает, должна ли она освободить стек?

cdecl Модификатор является частью прототипа функции (или типа указателя на функцию и т. д.), поэтому вызывающая сторона получает информацию оттуда и действует соответственно.

б) Если функция, которая объявлена ​​как stdcall, вызывает функцию (которая имеет соглашение о вызовах как cdecl) или наоборот, будет ли это неуместным?

Нет, все хорошо.

в) Можно ли вообще сказать, что вызов будет быстрее - cdecl или stdcall?

В общем, я бы воздержался от любых подобных заявлений. Различие имеет значение, например. когда вы хотите использовать функции va_arg. В теории это может быть stdcall быстрее и генерирует меньший код, потому что он позволяет комбинировать выталкивание аргументов с выталкиванием локальных, но OTOH с cdeclВы можете сделать то же самое, если вы умны.

Соглашения о вызовах, которые стремятся быть быстрее, обычно делают некоторую передачу регистра.

Соглашения о вызовах не имеют ничего общего с языками программирования C/C++ и скорее представляют собой особенности того, как компилятор реализует данный язык. Если вы постоянно используете один и тот же компилятор, вам не нужно беспокоиться о соглашениях о вызовах.

Однако иногда мы хотим, чтобы двоичный код, скомпилированный разными компиляторами, корректно взаимодействовал. Когда мы делаем это, нам нужно определить то, что называется Application Binary Interface (ABI). ABI определяет, как компилятор преобразует исходный код C/C++ в машинный код. Это будет включать в себя соглашения о вызовах, распределение имен и размещение v-таблицы. cdelc и stdcall - это два разных соглашения о вызовах, которые обычно используются на платформах x86.

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

Другие вопросы по тегам