Существует (среди прочего) два типа соглашений о вызовах - stdcall и cdecl . У меня к ним несколько вопросов:
- Когда вызывается функция cdecl, как вызывающий абонент узнает, следует ли освобождать стек? Знает ли вызывающий на месте вызова, является ли вызываемая функция функцией cdecl или stdcall? Как это работает ? Как вызывающий абонент узнает, следует ли ему освобождать стек? Или это ответственность линкеров?
- Если функция, объявленная как stdcall, вызывает функцию (которая имеет соглашение о вызовах как cdecl) или наоборот, будет ли это неуместным?
- В общем, можно ли сказать, какой вызов будет быстрее - cdecl или stdcall?
Ответы:
Раймонд Чен дает хороший обзор того, что
__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
с a__cdecl
или наоборот. Оно делает.Причина:
__cdecl
аргументы, которые передаются вызываемым функциям, удаляются из стека вызывающей функцией, в__stdcall
аргументы удаляются из стека вызываемой функцией. Если вы вызываете__cdecl
функцию с помощью__stdcall
, стек вообще не очищается, поэтому, в конечном итоге, когда__cdecl
используется ссылка на стек для аргументов или адреса возврата, будут использоваться старые данные в текущем указателе стека. Если вы вызываете__stdcall
функцию из a__cdecl
,__stdcall
функция очищает аргументы в стеке, а затем__cdecl
функция делает это снова, возможно, удаляя информацию, возвращаемую вызывающими функциями.Соглашение Microsoft для C пытается обойти это, изменяя имена.
__cdecl
Функция с префиксом подчеркивания. А__stdcall
функция префиксы с подчеркиванием и суффиксы с символа «@» , и число байтов , которые будут удалены. Например,__cdecl
f (x) связан как_f
,__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
Модификатор является частью функции прототипа (или функция типа указателя и т.д.) , так что абонент получить информацию оттуда и действует соответствующим образом .Нет, все хорошо.
В общем, я бы воздержался от подобных заявлений. Различие имеет значение, например. когда вы хотите использовать функции va_arg. Теоретически это может быть
stdcall
быстрее и генерирует меньший код, потому что он позволяет комбинировать выталкивание аргументов с выталкиванием локальных переменных, но с помощью OTOHcdecl
вы тоже можете сделать то же самое, если вы умны.Соглашения о вызовах, которые стремятся быть быстрее, обычно используют некоторую передачу регистров.
источник
Соглашения о вызовах не имеют ничего общего с языками программирования C / C ++ и скорее определяют, как компилятор реализует данный язык. Если вы постоянно используете один и тот же компилятор, вам не нужно беспокоиться о соглашениях о вызовах.
Однако иногда нам нужно, чтобы двоичный код, скомпилированный разными компиляторами, правильно взаимодействовал. Когда мы это делаем, нам нужно определить нечто, называемое двоичным интерфейсом приложения (ABI). ABI определяет, как компилятор преобразует исходный код C / C ++ в машинный код. Это будет включать соглашения о вызовах, изменение имен и макет v-таблицы. cdelc и stdcall - это два разных соглашения о вызовах, обычно используемых на платформах x86.
Помещая информацию о соглашении о вызовах в исходный заголовок, компилятор будет знать, какой код необходимо сгенерировать для правильного взаимодействия с данным исполняемым файлом.
источник