stdcall und cdecl

Lesezeit: 7 Minuten

stdcall und cdecl
Rakesh Agarwal

Es gibt (unter anderem) zwei Arten von Aufrufkonventionen – Standardanruf und cdekl. Ich habe ein paar Fragen dazu:

  1. Woher weiß ein Aufrufer, wenn eine cdecl-Funktion aufgerufen wird, ob er den Stack freigeben soll? Weiß der Aufrufer auf der Aufrufseite, ob die aufgerufene Funktion eine cdecl- oder eine stdcall-Funktion ist? Wie funktioniert es ? Woher weiß der Anrufer, ob er den Stack freigeben soll oder nicht? Oder ist es die Verantwortung des Linkers?
  2. Wenn eine Funktion, die als stdcall deklariert ist, eine Funktion aufruft (die eine Aufrufkonvention als cdecl hat) oder umgekehrt, wäre dies unangemessen?
  3. Können wir im Allgemeinen sagen, welcher Anruf schneller sein wird – cdecl oder stdcall ?

  • Es gibt viele Arten von Aufrufkonventionen, von denen dies nur zwei sind. en.wikipedia.org/wiki/X86_calling_conventions

    – Muhende Ente

    4. März 14 um 0:53 Uhr


  • Bitte markieren Sie eine richtige Antwort

    – ceztko

    22. August 18 um 16:25 Uhr

1644111069 312 stdcall und cdecl
In Silico

Raymond Chen gibt einen schönen Überblick darüber, was __stdcall und __cdecl tut.

(1) Der Aufrufer “weiß”, dass er den Stack nach dem Aufruf 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, dass die Aufrufkonvention nicht übereinstimmtso was:

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 machen das falsch, dass es nicht einmal lustig ist. So soll es 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 jedoch den Code, der zum ordnungsgemäßen Bereinigen des Stapels erforderlich ist, da er die Aufrufkonventionen der beteiligten Funktionen kennt.

(2) Beide Wege sollten funktionieren. Tatsächlich passiert dies zumindest in Code, der mit der Windows-API interagiert, ziemlich häufig, weil __cdecl ist der Standardwert für C- und C++-Programme gemäß dem Visual C++-Compiler und Die WinAPI-Funktionen verwenden die __stdcall Konvention.

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

  • +1 für ein gutes Beispiel und Raymond Chens Beitrag über das Aufrufen der Kongressgeschichte. Für alle Interessierten sind auch die anderen Teile davon eine gute Lektüre.

    – OregonGhost

    4. August 10 um 10:04 Uhr


  • +1 für Raymond Chen. Übrigens (OT): Warum kann ich die anderen Teile nicht über das Blog-Suchfeld finden? Google findet sie, aber keine MSDN-Blogs?

    – Nordischer Großrechner

    4. August 10 um 11:06 Uhr

1644111070 677 stdcall und cdecl
adf88

In CDECL werden Argumente in umgekehrter Reihenfolge auf den Stapel geschoben, der Aufrufer löscht den Stapel und das Ergebnis wird über die Prozessorregistrierung zurückgegeben (später werde ich es “Register A” nennen). Bei STDCALL gibt es einen Unterschied, der Aufrufer löscht den Stapel nicht, der Angerufene tut es.

Sie fragen, welches schneller ist. Niemand. Sie sollten so lange wie möglich die native Aufrufkonvention verwenden. Ändern Sie die Konvention nur, wenn es keinen Ausweg gibt, wenn Sie externe Bibliotheken verwenden, die die Verwendung einer bestimmten Konvention erfordern.

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 umfassender verwendet werden.

Normalerweise müssen Sie Callback-Funktionen, die an eine externe Bibliothek übergeben werden, z. B. Callback to, eine ordnungsgemäße Aufrufkonventionssignatur geben qsort aus 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 sein (die gesamte WinAPI ist STDCALL).

Ein anderer üblicher Fall kann sein, wenn Sie Zeiger auf einige externe Funktionen speichern, dh um einen Zeiger auf eine WinAPI-Funktion zu erstellen, muss ihre Typdefinition mit STDCALL gekennzeichnet sein.

Und unten ist ein Beispiel, das zeigt, wie der Compiler das macht:

/* 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)

  • Hinweis: __fastcall ist schneller als __cdecl und STDCALL ist die Standard-Aufrufkonvention für Windows 64-Bit

    – DNS

    10. Dezember 13 um 7:58 Uhr


  • ohh; also muss es die Rücksprungadresse ausgeben, die Blockgröße des Arguments hinzufügen und dann zur zuvor ausgeworfenen Rücksprungadresse springen? (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 (da Sie müssten sich wieder vergraben, um auf dem Stack zu ret, was Sie zu dem Problem zurückbringt, dass Sie den Stack nicht bereinigt haben).

    – Dmitri

    14. August 17 um 23:09 Uhr

  • Alternativ können Sie zu reg1 zurückkehren, den Stapelzeiger auf den Basiszeiger setzen und dann zu reg1 springen

    – Dmitri

    14. August 17 um 23:11 Uhr

  • Verschieben Sie alternativ den Wert des Stapelzeigers von oben nach unten, bereinigen Sie und rufen Sie dann ret auf

    – Dmitri

    14. August 17 um 23:12 Uhr

  • Beachten Sie, dass unter Windows x64 __fastcall, __stdcallund __cdecl sind alle Aliase für die gleiche Aufrufkonvention, die ist __fastcall. Sowohl 32-Bit als auch 64-Bit haben eine alternative Aufrufkonvention von __vectorcall

    – Chuck Walbourn

    31. Dezember 2020 um 22:03 Uhr


1644111070 240 stdcall und cdecl
Lee Hamilton

Mir ist ein Posting aufgefallen, das besagt, dass es egal ist, ob man einen anruft __stdcall von einem __cdecl oder umgekehrt. Es tut.

Der Grund: mit __cdecl Die Argumente, die an die aufgerufenen Funktionen übergeben werden, werden von der aufrufenden Funktion aus dem Stack entfernt, in __stdcall, werden die Argumente von der aufgerufenen Funktion aus dem Stack entfernt. Wenn Sie a anrufen __cdecl Funktion mit a __stdcallder Stapel wird überhaupt nicht aufgeräumt, also wenn die __cdecl verwendet eine stapelbasierte Referenz für Argumente, oder die Rücksprungadresse verwendet die alten Daten am aktuellen Stapelzeiger. Wenn Sie a anrufen __stdcall Funktion von a __cdecldas __stdcall Funktion bereinigt die Argumente auf dem Stack und dann die __cdecl Funktion wiederholt dies und entfernt möglicherweise die Rückgabeinformationen der aufrufenden Funktionen.

Die Microsoft-Konvention für C versucht dies zu umgehen, indem sie die Namen verstümmelt. EIN __cdecl Der Funktion wird ein Unterstrich vorangestellt. EIN __stdcall Funktionspräfixe mit einem Unterstrich und einem angehängten At-Zeichen „@“ und der Anzahl der zu entfernenden Bytes. Z.B __cdecl f(x) ist verknüpft als _f, __stdcall f(int x) ist verlinkt als _f@4 wo sizeof(int) ist 4 Byte)

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

  • Der erste Absatz ist (jetzt) ​​falsch, da die meisten Compiler die Unterschiede zwischen den Aufrufkonventionen behandeln, um zu verhindern, dass solche Probleme auftreten.

    – TheMadHau5

    3. Dezember 2020 um 10:48 Uhr

  • Der Anrufer und der Angerufene müssen beide der Anrufkonvention des Angerufenen zustimmen, ja. Aber “einen __stdcall von einem __cdecl aufrufen” weist nur darauf hin, dass die Art und Weise, wie der eigene Elternteil des Aufrufers ihm Argumente übergeben hat, nicht relevant dafür ist, wie er den Stack verwaltet, wenn er andere Funktionen aufruft. __cdecl und __stdcall stimmen darin überein, welche Register call-preserved vs. call-clobbered sind, so dass keine Notwendigkeit besteht, Register extra zu speichern, die eine Funktion beispielsweise nicht bereits selbst verwendet.

    – Peter Cordes

    14. September 21 um 3:14 Uhr

  • Das Problem wäre (wie du sagst) wenn du eine anrufst __stdcall Funktion Als wäre es ein __cdecl Funktion. Nicht von ein __cdecl Funktion. z.B __stdcall foo(int x) kann geschrieben werden, um anzurufen printf. Ihre Antwort ist richtig, mit Ausnahme des ersten Absatzes, der besagt, dass Sie mit einer anderen Sache nicht einverstanden sind. >.<

    – Peter Cordes

    14. September 21 um 3:15 Uhr


Ich möchte die Antwort von @ adf88 verbessern. Ich habe das Gefühl, dass der Pseudocode für STDCALL nicht die Art und Weise widerspiegelt, wie es in der Realität passiert. ‘a’, ‘b’ und ‘c’ werden nicht aus dem Stapel im Funktionsrumpf entfernt. Stattdessen werden sie von der geknallt ret Anweisung (ret 12 würde in diesem Fall verwendet werden), der auf einen Schlag zum Aufrufer zurückspringt und gleichzeitig ‘a’, ‘b’ und ‘c’ aus dem Stapel holt.

Hier ist meine nach meinem Verständnis korrigierte Version:

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)

Es ist im Funktionstyp angegeben. Wenn Sie einen Funktionszeiger haben, wird davon ausgegangen, dass er cdecl ist, wenn nicht explizit stdcall. Das bedeutet, dass Sie einen stdcall-Zeiger und einen cdecl-Zeiger nicht austauschen können. Die beiden Funktionstypen können sich ohne Probleme gegenseitig aufrufen, es wird nur ein Typ abgerufen, wenn Sie den anderen erwarten. Was die Geschwindigkeit angeht, spielen beide die gleichen Rollen, nur an einem ganz geringfügig anderen Ort, das ist wirklich irrelevant.

1644111070 697 stdcall und cdecl
scharfer Zahn

Der Aufrufer und der Angerufene müssen am Aufrufpunkt dieselbe Konvention verwenden – nur so könnte es zuverlässig funktionieren. Sowohl der Aufrufer als auch der Angerufene folgen einem vordefinierten Protokoll – zum Beispiel, wer den Stack aufräumen muss. Wenn Konventionen nicht übereinstimmen, läuft Ihr Programm auf undefiniertes Verhalten – wahrscheinlich stürzt es einfach spektakulär ab.

Dies ist nur pro Aufrufstelle erforderlich – der Aufrufcode selbst kann eine Funktion mit jeder Aufrufkonvention sein.

Sie sollten keinen wirklichen Leistungsunterschied zwischen diesen Konventionen bemerken. Wenn das ein Problem wird, müssen Sie normalerweise weniger Anrufe tätigen – zum Beispiel den Algorithmus ändern.

1644111070 981 stdcall und cdecl
Sellibitze

Diese Dinge sind Compiler- und Plattform-spezifisch. Weder der C- noch der C++-Standard sagen etwas über Aufrufkonventionen aus, außer für extern "C" in C++.

Wie weiß ein Anrufer, ob er den Stack freigeben soll?

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

Weiß der Aufrufer auf der Aufrufseite, ob die aufgerufene Funktion eine cdecl- oder eine stdcall-Funktion ist?

Ja.

Wie funktioniert es ?

Sie ist Teil der Funktionsdeklaration.

Woher weiß der Anrufer, ob er den Stack freigeben soll oder nicht?

Der Anrufer kennt die Anrufkonventionen und kann entsprechend handeln.

Oder ist es die Verantwortung des Linkers?

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

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

Nein. Warum sollte es?

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

Ich weiß nicht. Probier es aus.

.

790650cookie-checkstdcall und cdecl

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy