Stdcall и cdecl

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

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

Ответ 1

Раймонд Чен дает хороший обзор того, что __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 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 и С++ в соответствии с Visual С++ компилятор и функции WinAPI используют соглашение __stdcall.

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

Ответ 2

В 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)

Ответ 3

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

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

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

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

Ответ 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)

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

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

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

Да.

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

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

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

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

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

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

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

Нет. Почему это должно быть?

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

Я не знаю. Проверьте его.

Ответ 8

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

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

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

Нет, это нормально.

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

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

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

Ответ 9

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

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

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