stdcall i cdecl

92

Istnieją (między innymi) dwa typy konwencji wywoływania - stdcall i cdecl . Mam na nie kilka pytań:

  1. Kiedy wywoływana jest funkcja cdecl, skąd dzwoniący wie, czy powinien zwolnić stos? Czy w miejscu wywołania dzwoniący wie, czy wywoływana funkcja jest funkcją cdecl czy stdcall? Jak to działa ? Skąd dzwoniący wie, czy powinien zwolnić stos, czy nie? Czy jest to odpowiedzialność łączników?
  2. Jeśli funkcja zadeklarowana jako stdcall wywołuje funkcję (która ma konwencję wywoływania jak cdecl) lub odwrotnie, czy byłoby to niewłaściwe?
  3. Ogólnie, czy możemy powiedzieć, że to wywołanie będzie szybsze - cdecl czy stdcall?
Rakesh Agarwal
źródło
9
Istnieje wiele typów konwencji wywoływania, z których są to tylko dwie. en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck
1
Proszę zaznaczyć poprawną odpowiedź
ceztko

Odpowiedzi:

79

Raymond Chen daje dobry przegląd tego, co __stdcalli __cdeclrobi .

(1) Obiekt wywołujący „wie”, aby wyczyścić stos po wywołaniu funkcji, ponieważ kompilator zna konwencję wywoływania tej funkcji i generuje niezbędny kod.

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();
}

Możliwe jest niedopasowanie konwencji wywoływania , na przykład:

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

Tak wiele próbek kodu jest błędnych, że nawet nie jest to śmieszne. Powinno wyglądać tak:

// CALLBACK is #define'd as __stdcall
LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg
    WPARAM wParam, LPARAM lParam);
// ...
windowClass.lpfnWndProc = &MyWndProc;

Jednak zakładając, że programista nie ignoruje błędów kompilatora, kompilator wygeneruje kod potrzebny do prawidłowego oczyszczenia stosu, ponieważ będzie znał konwencje wywoływania zaangażowanych funkcji.

(2) Oba sposoby powinny działać. W rzeczywistości zdarza się to dość często, przynajmniej w kodzie, który współdziała z interfejsem API systemu Windows, ponieważ __cdecljest to ustawienie domyślne dla programów C i C ++ według kompilatora Visual C ++, a funkcje WinAPI używają __stdcallkonwencji .

(3) Nie powinno być między nimi żadnej rzeczywistej różnicy w wydajności.

In silico
źródło
+1 za dobry przykład i post Raymonda Chena o nazywaniu historii konwencji. Dla wszystkich zainteresowanych pozostałe fragmenty również są dobrą lekturą.
OregonGhost
+1 dla Raymonda Chena. Btw (OT): Dlaczego nie mogę znaleźć innych części za pomocą pola wyszukiwania w blogu? Google je znajduje, ale nie blogi MSDN?
Nordic Mainframe
44

W CDECL argumenty są umieszczane na stosie w odwrotnej kolejności, wywołujący czyści stos, a wynik jest zwracany przez rejestr procesora (później będę nazywać go „rejestrem A”). W STDCALL jest jedna różnica, dzwoniący nie wyczyścił stosu, a calle to zrobił.

Pytasz, który jest szybszy. Nikt. Powinieneś używać rodzimej konwencji wywoływania tak długo, jak możesz. Konwencję należy zmieniać tylko wtedy, gdy nie ma wyjścia, podczas korzystania z bibliotek zewnętrznych, które wymagają użycia określonej konwencji.

Poza tym istnieją inne konwencje, które kompilator może wybrać jako domyślną, np. Kompilator Visual C ++ wykorzystuje FASTCALL, który teoretycznie jest szybszy z powodu szerszego wykorzystania rejestrów procesora.

Zwykle musisz nadać prawidłową sygnaturę konwencji wywoływania funkcjom zwrotnym przekazanym do jakiejś zewnętrznej biblioteki tj. qsortWywołanie zwrotne do z biblioteki C musi być CDECL (jeśli kompilator domyślnie używa innej konwencji to musimy oznaczyć wywołanie zwrotne jako CDECL) lub różne wywołania zwrotne WinAPI muszą być STDCALL (całe WinAPI to STDCALL).

Innym typowym przypadkiem może być przechowywanie wskaźników do niektórych funkcji zewnętrznych, tj. Aby utworzyć wskaźnik do funkcji WinAPI, jej definicja typu musi być oznaczona STDCALL.

A poniżej przykład pokazujący, jak robi to kompilator:

/* 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)
adf88
źródło
Uwaga: __fastcall jest szybszy niż __cdecl, a STDCALL jest domyślną konwencją wywoływania dla 64-bitowego systemu Windows
dns
ohh więc musi zdjąć adres zwrotny, dodać rozmiar bloku argumentów, a następnie przejść do adresu zwrotnego, który pojawił się wcześniej? musiałbyś pogrzebać się z powrotem na stosie, wracając do problemu, że nie wyczyściłeś stosu).
Dmitry
alternatywnie, powróć do reg1, ustaw wskaźnik stosu na wskaźnik bazowy, a następnie przejdź do reg1
Dmitry
alternatywnie, przenieś wartość wskaźnika stosu z góry na dół, wyczyść, a następnie zadzwoń do ret
Dmitry
15

Zauważyłem post, który mówi, że nie ma znaczenia, czy dzwonisz __stdcallz a, __cdeclczy odwrotnie. To robi.

Powód: __cdeclgdy argumenty przekazywane do wywoływanych funkcji są usuwane ze stosu przez funkcję wywołującą, w __stdcallprogramie argumenty są usuwane ze stosu przez wywoływaną funkcję. Jeśli wywołasz __cdeclfunkcję z a __stdcall, stos nie zostanie w ogóle wyczyszczony, więc ostatecznie, gdy __cdeclużyje odniesienia opartego na stosie dla argumentów lub adresu zwrotnego, użyje starych danych pod bieżącym wskaźnikiem stosu. Jeśli wywołasz __stdcallfunkcję z a __cdecl, __stdcallfunkcja czyści argumenty na stosie, a następnie __cdeclrobi to ponownie, prawdopodobnie usuwając zwracane przez funkcje wywołujące informacje.

Konwencja Microsoftu dotycząca języka C próbuje obejść ten problem, zmieniając nazwy. __cdeclFunkcji prefiksem podkreślenia. A __stdcallprefiksy funkcja znaku podkreślenia oraz z rozszerzeniem na znak „@” i liczby bajtów zostać usunięte. Np. __cdeclF (x) jest połączona jako _f, __stdcall f(int x)jest połączona, _f@4gdzie sizeof(int)jest 4 bajty)

Jeśli uda ci się ominąć linker, ciesz się bałaganem debugowania.

Lee Hamilton
źródło
3

Chcę poprawić odpowiedź @ adf88. Czuję, że pseudokod STDCALL nie odzwierciedla sposobu, w jaki to się dzieje w rzeczywistości. „a”, „b” i „c” nie są usuwane ze stosu w treści funkcji. Zamiast tego są wywoływane przez retinstrukcję ( ret 12w tym przypadku byłaby użyta), która za jednym zamachem przeskakuje z powrotem do wywołującego i jednocześnie zdejmuje ze stosu znaki „a”, „b” i „c”.

Oto moja wersja poprawiona zgodnie z moim zrozumieniem:

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)

golem
źródło
2

Jest określony w typie funkcji. Gdy masz wskaźnik funkcji, zakłada się, że jest to cdecl, jeśli nie jest to jawnie stdcall. Oznacza to, że jeśli otrzymasz wskaźnik stdcall i wskaźnik cdecl, nie możesz ich wymienić. Te dwa typy funkcji mogą wywoływać się nawzajem bez problemów, po prostu otrzymujesz jeden typ, gdy oczekujesz drugiego. Jeśli chodzi o szybkość, to obaj pełnią te same role, tylko w nieco innym miejscu, to naprawdę nie ma znaczenia.

Szczeniak
źródło
1

Dzwoniący i odbierający muszą stosować tę samą konwencję w punkcie wywołania - tylko w ten sposób może to działać niezawodnie. Zarówno wywołujący, jak i odbierający stosują się do predefiniowanego protokołu - na przykład, kto musi wyczyścić stos. Jeśli konwencje są niezgodne, program napotyka nieokreślone zachowanie - prawdopodobnie po prostu ulega spektakularnej awarii.

Jest to wymagane tylko na stronę wywołania - sam kod wywołujący może być funkcją z dowolną konwencją wywoływania.

Nie powinieneś zauważyć żadnej rzeczywistej różnicy w wydajności między tymi konwencjami. Jeśli stanie się to problemem, zwykle musisz wykonywać mniej połączeń - na przykład zmienić algorytm.

ostry
źródło
1

Te rzeczy są specyficzne dla kompilatora i platformy. Ani standard C, ani C ++ nie mówią nic o wywoływaniu konwencji z wyjątkiem extern "C"C ++.

skąd dzwoniący wie, czy powinien zwolnić stos?

Wzywający zna konwencję wywoływania funkcji i odpowiednio obsługuje wywołanie.

Czy w miejscu wywołania dzwoniący wie, czy wywoływana funkcja jest funkcją cdecl czy stdcall?

Tak.

Jak to działa ?

Jest częścią deklaracji funkcji.

Skąd dzwoniący wie, czy powinien zwolnić stos, czy nie?

Dzwoniący zna konwencje wywoływania i może odpowiednio postępować.

Czy jest to odpowiedzialność łączników?

Nie, konwencja wywoływania jest częścią deklaracji funkcji, więc kompilator wie wszystko, co musi wiedzieć.

Jeśli funkcja zadeklarowana jako stdcall wywołuje funkcję (która ma konwencję wywoływania jak cdecl) lub odwrotnie, czy byłoby to niewłaściwe?

Nie. Dlaczego miałoby to robić?

Ogólnie, czy możemy powiedzieć, że to wywołanie będzie szybsze - cdecl czy stdcall?

Nie wiem Sprawdź to.

sellibitze
źródło
0

a) Gdy wywołujący wywołuje funkcję cdecl, skąd dzwoniący wie, czy powinien zwolnić stos?

cdeclModyfikator jest częścią prototypu funkcji (lub funkcja typu kursora itd.), Więc rozmówca uzyskać informacje stamtąd i działa odpowiednio.

b) Jeśli funkcja zadeklarowana jako stdcall wywołuje funkcję (która ma konwencję wywoływania jak cdecl) lub odwrotnie, czy byłoby to niewłaściwe?

Nie, w porządku.

c) Ogólnie, czy możemy powiedzieć, które wywołanie będzie szybsze - cdecl czy stdcall?

Generalnie powstrzymałbym się od takich stwierdzeń. To rozróżnienie ma znaczenie np. kiedy chcesz używać funkcji va_arg. Teoretycznie może być stdcallszybszy i generujący mniejszy kod, ponieważ pozwala łączyć otwieranie argumentów z otwieraniem lokalnych, ale OTOH z cdecl, możesz zrobić to samo, jeśli jesteś sprytny.

Konwencje wywołań, które mają być szybsze, zwykle wykonują pewne przekazywanie rejestru.

jpalecek
źródło
-1

Konwencje wywoływania nie mają nic wspólnego z językami programowania C / C ++ i są raczej szczegółami na temat tego, jak kompilator implementuje dany język. Jeśli konsekwentnie używasz tego samego kompilatora, nigdy nie musisz się martwić o wywoływanie konwencji.

Jednak czasami chcemy, aby kod binarny skompilowany przez różne kompilatory działał poprawnie. Kiedy to robimy, musimy zdefiniować coś, co nazywa się Application Binary Interface (ABI). ABI definiuje, w jaki sposób kompilator konwertuje źródło C / C ++ na kod maszynowy. Obejmuje to konwencje wywoływania, zniekształcanie nazw i układ v-table. cdelc i stdcall to dwie różne konwencje wywoływania, powszechnie używane na platformach x86.

Umieszczając informację o konwencji wywoływania w nagłówku źródła, kompilator będzie wiedział, jaki kod musi zostać wygenerowany, aby poprawnie współdziałać z danym plikiem wykonywalnym.

doron
źródło