Es gibt (unter anderem) zwei Arten von Aufrufkonventionen - stdcall und cdecl . Ich habe einige Fragen dazu:
- Woher weiß ein Aufrufer beim Aufrufen einer cdecl-Funktion, ob er den Stapel freigeben soll? Weiß der Aufrufer an der Aufrufstelle, ob es sich bei der aufgerufenen Funktion um eine cdecl- oder eine stdcall-Funktion handelt? Wie funktioniert es ? Woher weiß der Aufrufer, ob er den Stapel freigeben soll oder nicht? Oder liegt es in der Verantwortung der Linker?
- Wenn eine als stdcall deklarierte Funktion eine Funktion aufruft (die eine Aufrufkonvention als cdecl hat) oder umgekehrt, wäre dies unangemessen?
- Können wir im Allgemeinen sagen, welcher Aufruf schneller sein wird - cdecl oder stdcall?
Antworten:
Raymond Chen gibt einen schönen Überblick über was
__stdcall
und__cdecl
tut .(1) Der Aufrufer "weiß", dass er den Stapel nach dem Aufrufen einer Funktion bereinigen muss, da der Compiler die Aufrufkonvention dieser Funktion kennt und den erforderlichen Code generiert.
Es ist möglich, die aufrufende Konvention wie folgt nicht übereinzustimmen :
So viele Codebeispiele verstehen das falsch, es ist nicht einmal lustig. Es soll so sein:
Unter der Annahme, dass der Programmierer Compilerfehler nicht ignoriert, generiert der Compiler den Code, der zum ordnungsgemäßen Bereinigen des Stacks erforderlich ist, da er die Aufrufkonventionen der beteiligten Funktionen kennt.
(2) Beide Wege sollten funktionieren. Tatsächlich geschieht dies ziemlich häufig, zumindest in Code, der mit der Windows-API interagiert, da dies
__cdecl
die Standardeinstellung für C- und C ++ - Programme gemäß dem Visual C ++ - Compiler ist und die WinAPI-Funktionen die__stdcall
Konvention verwenden .(3) Es sollte keinen wirklichen Leistungsunterschied zwischen den beiden geben.
quelle
Wenn CDECL-Argumente in umgekehrter Reihenfolge auf den Stapel geschoben werden, löscht der Aufrufer den Stapel und das Ergebnis wird über die Prozessorregistrierung zurückgegeben (später werde ich es "Register A" nennen). In STDCALL gibt es einen Unterschied: Der Aufrufer löscht den Stapel nicht, die Calle nicht.
Sie fragen, welches schneller ist. Niemand. Sie sollten die native Anrufkonvention so lange wie möglich verwenden. Ändern Sie die Konvention nur, wenn es keinen Ausweg gibt, wenn Sie externe Bibliotheken verwenden, für die bestimmte Konventionen verwendet werden müssen.
Außerdem gibt es andere Konventionen, die der Compiler als Standard auswählen kann, dh der Visual C ++ - Compiler verwendet FASTCALL, was theoretisch schneller ist, da Prozessorregister umfangreicher verwendet werden.
Normalerweise müssen Sie Rückruffunktionen, die an eine externe Bibliothek übergeben werden, eine ordnungsgemäße Signatur der Aufrufkonvention geben, dh der Rückruf
qsort
von der C-Bibliothek muss CDECL sein (wenn der Compiler standardmäßig eine andere Konvention verwendet, müssen wir den Rückruf als CDECL markieren) oder verschiedene WinAPI-Rückrufe müssen STDCALL (ganze WinAPI ist STDCALL).Ein anderer üblicher Fall kann sein, wenn Sie Zeiger auf einige externe Funktionen speichern, dh um einen Zeiger auf die WinAPI-Funktion zu erstellen, muss deren Typdefinition mit STDCALL markiert sein.
Das folgende Beispiel zeigt, wie der Compiler dies tut:
CDECL:
STDCALL:
quelle
Ich habe einen Beitrag bemerkt, der besagt, dass es egal ist, ob Sie einen
__stdcall
von einem__cdecl
oder umgekehrt anrufen . Es tut.Der Grund: Wenn
__cdecl
die Argumente, die an die aufgerufenen Funktionen übergeben werden, von der aufrufenden Funktion__stdcall
aus dem Stapel entfernt werden, werden die Argumente von der aufgerufenen Funktion aus dem Stapel entfernt. Wenn Sie eine__cdecl
Funktion mit a aufrufen__stdcall
, wird der Stapel überhaupt nicht bereinigt. Wenn also__cdecl
eine gestapelte Referenz für Argumente oder eine Rücksprungadresse verwendet wird, werden die alten Daten am aktuellen Stapelzeiger verwendet. Wenn Sie eine__stdcall
Funktion von a aus aufrufen__cdecl
,__stdcall
bereinigt die Funktion die Argumente auf dem Stapel, und die__cdecl
Funktion führt sie dann erneut aus, wobei möglicherweise die Rückgabeinformationen der aufrufenden Funktionen entfernt werden.Die Microsoft-Konvention für C versucht, dies zu umgehen, indem die Namen entstellt werden. Einer
__cdecl
Funktion wird ein Unterstrich vorangestellt. Eine__stdcall
Funktion wird mit einem Unterstrich versehen und mit einem at-Zeichen „@“ und der Anzahl der zu entfernenden Bytes versehen. ZB ist__cdecl
f (x) verknüpft als_f
,__stdcall f(int x)
ist verknüpft als_f@4
wosizeof(int)
ist 4 Bytes)Wenn Sie es schaffen, den Linker zu überwinden, genießen Sie das Debugging-Chaos.
quelle
Ich möchte die Antwort von @ adf88 verbessern. Ich bin der Meinung, dass der Pseudocode für den STDCALL nicht die Art und Weise widerspiegelt, wie er in der Realität abläuft. 'a', 'b' und 'c' werden nicht vom Stapel im Funktionskörper entfernt. Stattdessen werden sie durch die
ret
Anweisung (ret 12
würde in diesem Fall verwendet) angezeigt, die auf einen Schlag zum Aufrufer zurückspringt und gleichzeitig 'a', 'b' und 'c' vom Stapel entfernt.Hier ist meine Version nach meinem Verständnis korrigiert:
STDCALL:
quelle
Es ist im Funktionstyp angegeben. Wenn Sie einen Funktionszeiger haben, wird davon ausgegangen, dass er cdecl ist, wenn nicht explizit stdcall. Dies bedeutet, dass Sie einen stdcall-Zeiger und einen cdecl-Zeiger nicht austauschen können, wenn Sie ihn erhalten. Die beiden Funktionstypen können sich ohne Probleme gegenseitig aufrufen. Es wird nur ein Typ angezeigt, wenn Sie den anderen erwarten. Was die Geschwindigkeit betrifft, spielen beide die gleichen Rollen, nur an einem etwas anderen Ort, es ist wirklich irrelevant.
quelle
Der Anrufer und der Angerufene müssen zum Zeitpunkt des Aufrufs dieselbe Konvention verwenden - nur so kann dies zuverlässig funktionieren. Sowohl der Anrufer als auch der Angerufene folgen einem vordefinierten Protokoll - zum Beispiel, wer den Stapel bereinigen muss. Wenn Konventionen nicht übereinstimmen, stößt Ihr Programm auf undefiniertes Verhalten - wahrscheinlich stürzt es nur spektakulär ab.
Dies ist nur pro Aufrufstelle erforderlich - der aufrufende Code selbst kann eine Funktion mit jeder aufrufenden Konvention sein.
Sie sollten keinen wirklichen Leistungsunterschied zwischen diesen Konventionen bemerken. Wenn dies zu einem Problem wird, müssen Sie normalerweise weniger Anrufe tätigen. Ändern Sie beispielsweise den Algorithmus.
quelle
Diese Dinge sind Compiler- und Plattform-spezifisch. Weder der C- noch der C ++ - Standard sagen etwas über das Aufrufen von Konventionen aus, außer
extern "C"
in C ++.Der Aufrufer kennt die Aufrufkonvention der Funktion und behandelt den Aufruf entsprechend.
Ja.
Es ist Teil der Funktionsdeklaration.
Der Anrufer kennt die Anrufkonventionen und kann entsprechend handeln.
Nein, die aufrufende Konvention ist Teil der Deklaration einer Funktion, sodass der Compiler alles weiß, was er wissen muss.
Warum sollte es?
Ich weiß es nicht. Probier es aus.
quelle
Der
cdecl
Modifikator ist Teil des Funktionsprototyps (oder des Funktionszeigertyps usw.), sodass der Aufrufer die Informationen von dort erhält und entsprechend handelt.Nein, alles in Ordnung.
Im Allgemeinen würde ich solche Aussagen unterlassen. Die Unterscheidung ist zB wichtig. wenn Sie va_arg-Funktionen verwenden möchten. Theoretisch könnte es sein, dass
stdcall
es schneller ist und kleineren Code generiert, da es das Poppen der Argumente mit dem Poppen der Einheimischen kombiniert, aber mit OTOHcdecl
können Sie das Gleiche auch tun, wenn Sie klug sind.Die aufrufenden Konventionen, die darauf abzielen, schneller zu sein, führen normalerweise eine Registerübergabe durch.
quelle
Aufrufkonventionen haben nichts mit den Programmiersprachen C / C ++ zu tun und sind eher spezifisch dafür, wie ein Compiler die angegebene Sprache implementiert. Wenn Sie konsistent denselben Compiler verwenden, müssen Sie sich keine Gedanken über das Aufrufen von Konventionen machen.
Manchmal möchten wir jedoch, dass Binärcode, der von verschiedenen Compilern kompiliert wurde, korrekt zusammenarbeitet. Wenn wir dies tun, müssen wir etwas definieren, das als Application Binary Interface (ABI) bezeichnet wird. Das ABI definiert, wie der Compiler die C / C ++ - Quelle in Maschinencode konvertiert. Dies umfasst Aufrufkonventionen, Namensverknüpfung und V-Tabellen-Layout. cdelc und stdcall sind zwei verschiedene Aufrufkonventionen, die üblicherweise auf x86-Plattformen verwendet werden.
Durch Platzieren der Informationen zur aufrufenden Konvention im Quellheader weiß der Compiler, welcher Code generiert werden muss, um mit der angegebenen ausführbaren Datei korrekt zu interagieren.
quelle