стандартный вызов и cdecl - PullRequest
79 голосов
/ 04 августа 2010

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

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

Ответы [ 9 ]

72 голосов
/ 04 августа 2010

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

41 голосов
/ 04 августа 2010

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

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

Кроме того, существуют другие соглашения, которые компилятор может выбрать в качестве одного по умолчанию, т.е. компилятор 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)
14 голосов
/ 17 сентября 2012

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

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

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

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

3 голосов
/ 25 октября 2015

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

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

STDCALL:

<code>/* 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</p>

<p>/* 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 <em>this</em> step, result in register A)
2 голосов
/ 04 августа 2010

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

1 голос
/ 04 августа 2010

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

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

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

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

Да.

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

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

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

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

Или это ответственность компоновщиков?

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

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

Нет. С чего бы это?

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

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

1 голос
/ 04 августа 2010

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

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

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

0 голосов
/ 04 августа 2010

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

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

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

0 голосов
/ 04 августа 2010

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

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

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

Нет, все в порядке.

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

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

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

...