stdcall и cdecl

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

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

7 ответов

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

72
ответ дан 24 November 2019 в 09:16
поделиться

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

2
ответ дан 24 November 2019 в 09:16
поделиться

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

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

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

1
ответ дан 24 November 2019 в 09:16
поделиться

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

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

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

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

Да.

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

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

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

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

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

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

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

Нет. А почему?

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

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

1
ответ дан 24 November 2019 в 09:16
поделиться

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

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

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

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

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

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

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

0
ответ дан 24 November 2019 в 09:16
поделиться

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

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

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

-1
ответ дан 24 November 2019 в 09:16
поделиться

В 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)
41
ответ дан 24 November 2019 в 09:16
поделиться
Другие вопросы по тегам:

Похожие вопросы: