stdcall та cdecl


92

Існують (серед інших) два типи конвенцій викликів - stdcall та cdecl . У мене є кілька запитань щодо них:

  1. Коли викликається функція cdecl, як абонент знає, чи повинен він звільнити стек? Чи знає виклик на сайті виклику, чи викликана функція є функцією cdecl або stdcall? Як це працює ? Звідки абонент знає, повинен він звільнити стек чи ні? Або це відповідальність лінкерів?
  2. Якщо функція, яка оголошена як stdcall, викликає функцію (яка має умову виклику як cdecl), або навпаки, чи буде це недоречним?
  3. Взагалі, чи можна сказати, який виклик буде швидшим - cdecl чи stdcall?

9
Існує багато типів домовленостей про дзвінки, з них лише два. en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck

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) Між ними не повинно бути реальної різниці в продуктивності.


+1 за хороший приклад і допис Раймонда Чена про історію викликів конгресів. Для всіх, хто зацікавлений, інші частини цього також добре читаються.
OregonGhost

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

44

У CDECL аргументи висуваються в стек у порядку зворотного виклику, абонент очищає стек, а результат повертається через реєстр процесора (пізніше я називатиму це "реєстром А"). У 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)

Примітка: __fastcall швидше, ніж __cdecl, а STDCALL є стандартною умовою викликів для Windows 64-розрядної версії
dns

ой! отже, він повинен вивести адресу повернення, додати розмір блоку аргументів, а потім перейти до адреси повернення, що з'явилася раніше? (як тільки ви викликаєте ret, ви більше не можете очищати стек, але як тільки ви очистите стек, ви більше не можете викликати ret (оскільки вам потрібно було б поховатись назад, щоб повернутися до стека, повернувши вас до проблеми, що ви не очистили стек).
Дмитро

альтернативно, поп повернутися до reg1, встановити покажчик стека на базовий вказівник, а потім перейти до reg1
Дмитро

В якості альтернативи, перемістіть значення покажчика стека з верхньої частини стека вниз, очистіть, а потім зателефонуйте ret
Дмитро

15

Я помітив повідомлення, в якому сказано, що неважливо, зателефонувати ви абоненту __stdcallз__cdecl або або навпаки. Це робить.

Причина: __cdeclколи аргументи, що передаються викликаним функціям, видаляються із стеку за допомогою викликаючої функції, в __stdcall, аргументи видаляються зі стека викликаною функцією. Якщо ви викликаєте __cdeclфункцію за допомогою a __stdcall, стек взагалі не очищається, тому врешті-решт коли__cdecl для аргументів або адреси повернення використовується посилання на основі стека, старі дані будуть використовуватися в поточному покажчику стека. Якщо ви викликаєте __stdcallфункцію з a __cdecl, __stdcallфункція очищає аргументи в стеку, а потім __cdeclфункція робить це знову, можливо, видаляючи інформацію про виклик, що повертає інформацію.

Конвенція Майкрософт для C намагається обійти це шляхом перекручування імен. __cdeclФункція з префіксом підкреслення. Префікс __stdcallфункції підкреслюється, суфікс складається зі знака «@» та кількості байтів, які потрібно видалити. Напр__cdecl f (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?

Не знаю. Перевірте це.


0

а) Коли абонент викликає функцію cdecl, як абонент знає, чи повинен він звільнити стек?

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

б) Якщо функція, яка оголошена як stdcall, викликає функцію (яка має умову виклику як cdecl), або навпаки, це буде недоречно?

Ні, це добре.

в) Взагалі, чи можна сказати, який виклик буде швидшим - cdecl чи stdcall?

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

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


-1

Конвенції викликів не мають нічого спільного з мовами програмування C / C ++ і є досить конкретними щодо того, як компілятор реалізує дану мову. Якщо ви постійно використовуєте один і той же компілятор, вам ніколи не потрібно турбуватися про виклик конвенцій.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.