stdcall und cdecl

89

Es gibt (unter anderem) zwei Arten von Aufrufkonventionen - stdcall und cdecl . Ich habe einige Fragen dazu:

  1. 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?
  2. Wenn eine als stdcall deklarierte Funktion eine Funktion aufruft (die eine Aufrufkonvention als cdecl hat) oder umgekehrt, wäre dies unangemessen?
  3. Können wir im Allgemeinen sagen, welcher Aufruf schneller sein wird - cdecl oder stdcall?
Rakesh Agarwal
quelle
9
Es gibt viele Arten von Aufrufkonventionen, von denen dies nur zwei sind. en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck
1
Bitte markieren Sie eine korrekte Antwort
Ceztko

Antworten:

77

Raymond Chen gibt einen schönen Überblick über was __stdcallund __cdecltut .

(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.

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

Es ist möglich, die aufrufende Konvention wie folgt nicht übereinzustimmen :

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

So viele Codebeispiele verstehen das falsch, es ist nicht einmal lustig. Es soll so sein:

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

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 __cdecldie Standardeinstellung für C- und C ++ - Programme gemäß dem Visual C ++ - Compiler ist und die WinAPI-Funktionen die __stdcallKonvention verwenden .

(3) Es sollte keinen wirklichen Leistungsunterschied zwischen den beiden geben.

In silico
quelle
+1 für ein gutes Beispiel und Raymond Chens Beitrag über das Aufrufen der Kongressgeschichte. Für alle Interessierten sind die anderen Teile ebenfalls eine gute Lektüre.
OregonGhost
+1 für Raymond Chen. Btw (OT): Warum kann ich die anderen Teile nicht über das Blog-Suchfeld finden? Google findet sie, aber keine MSDN-Blogs?
Nordic Mainframe
44

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 qsortvon 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:

/* 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
quelle
Hinweis: __fastcall ist schneller als __cdecl und STDCALL ist die Standardaufrufkonvention für Windows 64-Bit
DNS
ohh; Es muss also die Rücksprungadresse angezeigt, die Blockgröße des Arguments hinzugefügt und dann zu der zuvor gepoppten Rücksprungadresse gesprungen werden. (Sobald Sie ret aufrufen, können Sie den Stapel nicht mehr bereinigen, aber sobald Sie den Stapel bereinigen, können Sie ret nicht mehr aufrufen (seitdem Sie müssten sich wieder begraben, um auf dem Stapel zu bleiben, was Sie zu dem Problem zurückbringt, dass Sie den Stapel nicht gereinigt haben.
Dmitry
Alternativ kehren Sie zu reg1 zurück, setzen den Stapelzeiger auf den Basiszeiger und springen dann zu reg1
Dmitry
Alternativ können Sie den Stapelzeigerwert von oben nach unten verschieben, bereinigen und dann ret
Dmitry
15

Ich habe einen Beitrag bemerkt, der besagt, dass es egal ist, ob Sie einen __stdcallvon einem __cdecloder umgekehrt anrufen . Es tut.

Der Grund: Wenn __cdecldie Argumente, die an die aufgerufenen Funktionen übergeben werden, von der aufrufenden Funktion __stdcallaus dem Stapel entfernt werden, werden die Argumente von der aufgerufenen Funktion aus dem Stapel entfernt. Wenn Sie eine __cdeclFunktion mit a aufrufen __stdcall, wird der Stapel überhaupt nicht bereinigt. Wenn also __cdecleine gestapelte Referenz für Argumente oder eine Rücksprungadresse verwendet wird, werden die alten Daten am aktuellen Stapelzeiger verwendet. Wenn Sie eine __stdcallFunktion von a aus aufrufen __cdecl, __stdcallbereinigt die Funktion die Argumente auf dem Stapel, und die __cdeclFunktion 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 __cdeclFunktion wird ein Unterstrich vorangestellt. Eine __stdcallFunktion wird mit einem Unterstrich versehen und mit einem at-Zeichen „@“ und der Anzahl der zu entfernenden Bytes versehen. ZB ist __cdeclf (x) verknüpft als _f, __stdcall f(int x)ist verknüpft als _f@4wo sizeof(int)ist 4 Bytes)

Wenn Sie es schaffen, den Linker zu überwinden, genießen Sie das Debugging-Chaos.

Lee Hamilton
quelle
3

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 retAnweisung ( ret 12wü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:

/* 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
quelle
2

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.

Hündchen
quelle
1

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.

scharfer Zahn
quelle
1

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 ++.

Woher weiß ein Anrufer, ob er den Stapel freigeben soll?

Der Aufrufer kennt die Aufrufkonvention der Funktion und behandelt den Aufruf entsprechend.

Weiß der Aufrufer an der Aufrufstelle, ob es sich bei der aufgerufenen Funktion um eine cdecl- oder eine stdcall-Funktion handelt?

Ja.

Wie funktioniert es ?

Es ist Teil der Funktionsdeklaration.

Woher weiß der Aufrufer, ob er den Stapel freigeben soll oder nicht?

Der Anrufer kennt die Anrufkonventionen und kann entsprechend handeln.

Oder liegt es in der Verantwortung der Linker?

Nein, die aufrufende Konvention ist Teil der Deklaration einer Funktion, sodass der Compiler alles weiß, was er wissen muss.

Wenn eine als stdcall deklarierte Funktion eine Funktion aufruft (die eine Aufrufkonvention als cdecl hat) oder umgekehrt, wäre dies unangemessen?

Warum sollte es?

Können wir im Allgemeinen sagen, welcher Aufruf schneller sein wird - cdecl oder stdcall?

Ich weiß es nicht. Probier es aus.

sellibitze
quelle
0

a) Wenn der Aufrufer eine cdecl-Funktion aufruft, woher weiß ein Aufrufer, ob er den Stapel freigeben soll?

Der cdeclModifikator ist Teil des Funktionsprototyps (oder des Funktionszeigertyps usw.), sodass der Aufrufer die Informationen von dort erhält und entsprechend handelt.

b) Wenn eine als stdcall deklarierte Funktion eine Funktion aufruft (die eine Aufrufkonvention als cdecl hat) oder umgekehrt, wäre dies unangemessen?

Nein, alles in Ordnung.

c) Können wir im Allgemeinen sagen, welcher Anruf schneller sein wird - cdecl oder stdcall?

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 stdcalles schneller ist und kleineren Code generiert, da es das Poppen der Argumente mit dem Poppen der Einheimischen kombiniert, aber mit OTOH cdeclkö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.

jpalecek
quelle
-1

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.

Doron
quelle