stdcall и cdecl

92

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

  1. Когда вызывается функция cdecl, как вызывающий абонент узнает, следует ли освобождать стек? Знает ли вызывающий на месте вызова, является ли вызываемая функция функцией cdecl или stdcall? Как это работает ? Как вызывающий абонент узнает, следует ли ему освобождать стек? Или это ответственность линкеров?
  2. Если функция, объявленная как stdcall, вызывает функцию (которая имеет соглашение о вызовах как cdecl) или наоборот, будет ли это неуместным?
  3. В общем, можно ли сказать, какой вызов будет быстрее - cdecl или stdcall?
Ракеш Агарвал
источник
9
Существует много типов соглашений о вызовах, из которых всего два. en.wikipedia.org/wiki/X86_calling_conventions
Мычание утки
1
Отметьте, пожалуйста, правильный ответ
ceztko

Ответы:

79

Раймонд Чен дает хороший обзор того, что __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) Между ними не должно быть реальной разницы в производительности.

In silico
источник
+1 за хороший пример и сообщение Раймонда Чена об истории соглашений о вызовах. Для всех, кто интересуется, другие части также могут быть полезны.
OregonGhost,
+1 для Раймонда Чена. Кстати (ОТ): Почему я не могу найти другие части с помощью окна поиска в блоге? Google находит их, но не блоги MSDN?
Nordic Mainframe
44

В 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)
adf88
источник
Примечание. __Fastcall быстрее, чем __cdecl, а STDCALL - соглашение о вызовах по умолчанию для 64-разрядной версии Windows
dns
ох; поэтому он должен выдать адрес возврата, добавить размер блока аргументов, затем перейти к адресу возврата, указанному ранее? (после вызова ret вы больше не можете очищать стек, но после очистки стека вы больше не можете вызывать ret (поскольку вам нужно будет снова закопаться, чтобы вернуться в стек, что вернет вас к проблеме, что вы не очистили стек)
Дмитрий
в качестве альтернативы, pop return to reg1, установка указателя стека на базовый указатель, затем переход к reg1
Дмитрий
в качестве альтернативы переместите значение указателя стека из верхней части стека в нижнюю, очистите, затем вызовите ret
Дмитрий
15

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

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

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

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

Ли Гамильтон
источник
3

Я хочу улучшить ответ @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)

голем
источник
2

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

Щенок
источник
1

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

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

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

острый зуб
источник
1

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

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

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

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

Да.

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

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

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

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

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

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

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

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

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

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

Sellibitze
источник
0

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

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

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

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

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

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

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

jpalecek
источник
-1

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

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

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

Дорон
источник