Istnieją (między innymi) dwa typy konwencji wywoływania - stdcall i cdecl . Mam na nie kilka pytań:
- 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?
- 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?
- Ogólnie, czy możemy powiedzieć, że to wywołanie będzie szybsze - cdecl czy stdcall?
Odpowiedzi:
Raymond Chen daje dobry przegląd tego, co
__stdcall
i__cdecl
robi .(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ż
__cdecl
jest to ustawienie domyślne dla programów C i C ++ według kompilatora Visual C ++, a funkcje WinAPI używają__stdcall
konwencji .(3) Nie powinno być między nimi żadnej rzeczywistej różnicy w wydajności.
źródło
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.
qsort
Wywoł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)
źródło
Zauważyłem post, który mówi, że nie ma znaczenia, czy dzwonisz
__stdcall
z a,__cdecl
czy odwrotnie. To robi.Powód:
__cdecl
gdy argumenty przekazywane do wywoływanych funkcji są usuwane ze stosu przez funkcję wywołującą, w__stdcall
programie argumenty są usuwane ze stosu przez wywoływaną funkcję. Jeśli wywołasz__cdecl
funkcję z a__stdcall
, stos nie zostanie w ogóle wyczyszczony, więc ostatecznie, gdy__cdecl
uż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__stdcall
funkcję z a__cdecl
,__stdcall
funkcja czyści argumenty na stosie, a następnie__cdecl
robi 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.
__cdecl
Funkcji prefiksem podkreślenia. A__stdcall
prefiksy funkcja znaku podkreślenia oraz z rozszerzeniem na znak „@” i liczby bajtów zostać usunięte. Np.__cdecl
F (x) jest połączona jako_f
,__stdcall f(int x)
jest połączona,_f@4
gdziesizeof(int)
jest 4 bajty)Jeśli uda ci się ominąć linker, ciesz się bałaganem debugowania.
źródło
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
ret
instrukcję (ret 12
w 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)
źródło
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.
źródło
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.
źródło
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 ++.Wzywający zna konwencję wywoływania funkcji i odpowiednio obsługuje wywołanie.
Tak.
Jest częścią deklaracji funkcji.
Dzwoniący zna konwencje wywoływania i może odpowiednio postępować.
Nie, konwencja wywoływania jest częścią deklaracji funkcji, więc kompilator wie wszystko, co musi wiedzieć.
Nie. Dlaczego miałoby to robić?
Nie wiem Sprawdź to.
źródło
cdecl
Modyfikator jest częścią prototypu funkcji (lub funkcja typu kursora itd.), Więc rozmówca uzyskać informacje stamtąd i działa odpowiednio.Nie, w porządku.
Generalnie powstrzymałbym się od takich stwierdzeń. To rozróżnienie ma znaczenie np. kiedy chcesz używać funkcji va_arg. Teoretycznie może być
stdcall
szybszy i generujący mniejszy kod, ponieważ pozwala łączyć otwieranie argumentów z otwieraniem lokalnych, ale OTOH zcdecl
, możesz zrobić to samo, jeśli jesteś sprytny.Konwencje wywołań, które mają być szybsze, zwykle wykonują pewne przekazywanie rejestru.
źródło
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.
źródło