стандартный вызов и cdecl

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

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

person Rakesh Agarwal    schedule 04.08.2010    source источник
comment
Существует много типов соглашений о вызовах, из которых только два. en.wikipedia.org/wiki/X86_calling_conventions   -  person Mooing Duck    schedule 04.03.2014
comment
Пожалуйста, отметьте правильный ответ   -  person ceztko    schedule 22.08.2018


Ответы (9)


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

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

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

СТАНДАРТНЫЙ ВЫЗОВ:

/* 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)
person adf88    schedule 04.08.2010
comment
Примечание. __fastcall работает быстрее, чем __cdecl, а STDCALL является соглашением о вызовах по умолчанию для 64-разрядной версии Windows. - person dns; 10.12.2013
comment
Ох; поэтому он должен извлекать адрес возврата, добавлять размер блока аргументов, а затем переходить к адресу возврата, извлеченному ранее? (как только вы вызываете ret, вы больше не можете очищать стек, но как только вы очищаете стек, вы больше не можете вызывать ret (поскольку вам нужно будет похоронить себя обратно, чтобы вернуться в стек, возвращая вас к проблеме, что вы не очистили стек). - person Dmitry; 15.08.2017
comment
в качестве альтернативы, pop возвращается к reg1, устанавливает указатель стека на базовый указатель, затем переходит к reg1 - person Dmitry; 15.08.2017
comment
в качестве альтернативы переместите значение указателя стека с вершины стека вниз, очистите, а затем вызовите ret - person Dmitry; 15.08.2017
comment
Обратите внимание, что в Windows x64 все __fastcall, __stdcall и __cdecl являются псевдонимами для одного и того же соглашения о вызовах, которым является __fastcall. Как 32-разрядные, так и 64-разрядные версии имеют альтернативное соглашение о вызовах __vectorcall. - person Chuck Walbourn; 01.01.2021
comment
какой из них быстрее. Никто. Они действительно имеют одинаковую скорость? cdecl имеет одну дополнительную инструкцию по сравнению со stdcall. Я предполагаю, что у этого есть тактовая стоимость, которую stdcall не несет. Не говоря уже о том, что cdecl будет потреблять больше памяти команд, если используется в программе более одного раза, съедая строки кэша. - person Thomas Eding; 17.01.2021

Я заметил сообщение, в котором говорится, что не имеет значения, звоните ли вы __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 байтам)

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

person Lee Hamilton    schedule 17.09.2012
comment
Первый абзац (теперь) неверен, так как большинство компиляторов учитывают различия между соглашениями о вызовах, чтобы предотвратить возникновение таких проблем. - person TheMadHau5; 03.12.2020

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

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

СТАНДАРТНЫЙ ЗВОНОК:

/* 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)

person golem    schedule 25.10.2015

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

person Puppy    schedule 04.08.2010

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

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

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

person sharptooth    schedule 04.08.2010

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

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

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

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

да.

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

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

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

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

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

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

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

Нет. Почему?

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

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

person sellibitze    schedule 04.08.2010

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

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

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

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

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

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

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

person jpalecek    schedule 04.08.2010

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

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

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

person doron    schedule 04.08.2010