stdcall e cdecl

Vi sono (tra gli altri) due tipi di convenzioni di chiamata: stdcall e cdecl . Ho alcune domande su di loro:

  1. Quando viene chiamata una funzione cdecl, come fa un chiamante a sapere se dovrebbe liberare lo stack? Nel sito di chiamata, il chiamante sa se la funzione chiamata è una funzione cdecl o stdcall? Come funziona ? Come fa il chiamante a sapere se dovrebbe liberare lo stack o no? O è la responsabilità dei linker?
  2. Se una funzione dichiarata come stdcall chiama una funzione (che ha una convenzione di chiamata come cdecl), o viceversa, sarebbe inappropriata?
  3. In generale, possiamo dire quale chiamata sarà più veloce – cdecl o stdcall?

Raymond Chen offre una buona panoramica di cosa fa __stdcall e __cdecl .

(1) Il chiamante “sa” per ripulire lo stack dopo aver chiamato una funzione perché il compilatore conosce la convenzione di chiamata di quella funzione e genera il codice necessario.

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

È ansible non corrispondere alla convenzione di chiamata , in questo modo:

 LRESULT MyWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); // ... // Compiler usually complains but there's this cast here... windowClass.lpfnWndProc = reinterpret_cast(&MyWndProc); 

Così tanti esempi di codice sbagliano non è nemmeno divertente. Dovrebbe essere così:

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

Tuttavia, supponendo che il programmatore non ignori gli errori del compilatore, il compilatore genererà il codice necessario per ripulire correttamente lo stack poiché conoscerà le convenzioni di chiamata delle funzioni coinvolte.

(2) Entrambi i modi dovrebbero funzionare. In effetti, ciò accade abbastanza frequentemente almeno nel codice che interagisce con l’API di Windows, poiché __cdecl è l’impostazione predefinita per i programmi C e C ++ in base al compilatore Visual C ++ e le funzioni WinAPI utilizzano la convenzione __stdcall .

(3) Non ci dovrebbe essere una reale differenza di prestazioni tra i due.

Negli argomenti CDECL vengono inseriti nello stack in ordine inverso, il chiamante cancella lo stack e il risultato viene restituito tramite il registro del processore (in seguito lo chiamerò “registro A”). In STDCALL c’è una differenza, il chiamante non riesce a svuotare lo stack, il calle fa.

Stai chiedendo quale è più veloce. Nessuno. Dovresti usare la convenzione di chiamata nativa finché puoi. Cambia convenzione solo se non c’è via d’uscita, quando si usano librerie esterne che richiedono l’utilizzo di determinate convenzioni.

Inoltre, ci sono altre convenzioni che il compilatore può scegliere come predefinito, ad esempio il compilatore Visual C ++ utilizza FASTCALL che è teoricamente più veloce a causa di un uso più esteso dei registri del processore.

Di solito è necessario fornire una firma della convenzione di chiamata appropriata alle funzioni di callback passate ad alcune librerie esterne, ad esempio il callback a qsort dalla libreria C deve essere CDECL (se il compilatore utilizza per default altre convenzioni, è necessario contrassegnare il callback come CDECL) o vari callback WinAPI devono essere STDCALL (l’intero WinAPI è STDCALL).

Un altro caso normale può essere quando si memorizzano i puntatori ad alcune funzioni esterne, ad esempio per creare un puntatore alla funzione WinAPI la sua definizione di tipo deve essere contrassegnata con STDCALL.

E sotto c’è un esempio che mostra come lo fa il compilatore:

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

Ho notato un post che dice che non importa se chiami una __stdcall da __cdecl o viceversa. Lo fa.

Il motivo: con __cdecl gli argomenti passati alle funzioni chiamate vengono rimossi dallo stack dalla funzione chiamante, in __stdcall , gli argomenti vengono rimossi dallo stack dalla funzione chiamata. Se si chiama una funzione __cdecl con una __stdcall , lo stack non viene ripulito del tutto, quindi alla fine quando __cdecl usa un riferimento basato su stack per argomenti o l’indirizzo di ritorno userà i vecchi dati nel puntatore dello stack corrente. Se si chiama una funzione __stdcall da un __cdecl , la funzione __stdcall pulisce gli argomenti nello stack, quindi la funzione __cdecl esegue nuovamente, eventualmente rimuovendo le informazioni di ritorno delle funzioni di chiamata.

La convenzione Microsoft per C tenta di aggirare questo maneggiando i nomi. Una funzione __cdecl è preceduta da un trattino basso. Una funzione __stdcall prefisso con un carattere di sottolineatura e un suffisso con un segno “@” e il numero di byte da rimuovere. Ad esempio __cdecl f (x) è collegato come _x , __stdcall f(int x) è collegato come [email protected] dove sizeof(int) è 4 byte)

Se riesci a superare il linker, goditi il ​​caos di debug.

È specificato nel tipo di funzione. Quando hai un puntatore a funzione, si presume che sia cdecl se non esplicitamente stdcall. Ciò significa che se si ottiene un puntatore stdcall e un puntatore cdecl, non è ansible scambiarli. I due tipi di funzione possono chiamarsi senza problemi, è solo ottenere un tipo quando ci si aspetta l’altro. Per quanto riguarda la velocità, entrambi svolgono gli stessi ruoli, solo in un posto leggermente diverso, è davvero irrilevante.

Voglio migliorare la risposta di @ adf88. Sento che lo pseudocodice per lo STDCALL non riflette il modo in cui accade nella realtà. ‘a’, ‘b’ e ‘c’ non vengono estratti dalla pila nel corpo della funzione. Invece vengono ret dall’istruzione ret (in questo caso si usa ret 12 ) che in un colpo salta al chiamante e allo stesso tempo fa scoppiare ‘a’, ‘b’ e ‘c’ dallo stack.

Ecco la mia versione corretta secondo la mia comprensione:

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 

/* 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. Corpo ‘Funzione’ di STDCALL in pseaudo-assemblatore * /
copia ‘a’ (dallo stack) per registrare A
copia ‘b’ (dallo stack) per registrare B
aggiungi A e B, memorizza il risultato in A
copia ‘c’ (dallo stack) per registrare B
aggiungi A e B, memorizza il risultato in A
torna al codice del chiamante e allo stesso tempo fai uscire “stack”, “b” e “c” (a, b e
c vengono rimossi dalla pila in questo passaggio, il risultato è nel registro A)

Il chiamante e il chiamato devono utilizzare la stessa convenzione al punto di invocazione: è l’unico modo in cui può funzionare in modo affidabile. Sia il chiamante che il chiamato seguono un protocollo predefinito, ad esempio chi deve ripulire lo stack. Se le convenzioni non corrispondono, il tuo programma va incontro a un comportamento indefinito, probabilmente si blocca in modo spettacolare.

Questo è richiesto solo per sito di chiamata: il codice chiamante può essere una funzione con qualsiasi convenzione di chiamata.

Non dovresti notare alcuna reale differenza di prestazioni tra quelle convenzioni. Se questo diventa un problema, di solito devi effettuare meno chiamate, ad esempio cambiare l’algoritmo.

Queste cose sono specifiche per compilatore e piattaforma. Né lo standard C né lo standard C ++ dicono nulla sulla chiamata delle convenzioni tranne per la extern "C" in C ++.

come fa un chiamante a sapere se dovrebbe liberare lo stack?

Il chiamante conosce la convenzione di chiamata della funzione e gestisce la chiamata di conseguenza.

Nel sito di chiamata, il chiamante sa se la funzione chiamata è una funzione cdecl o stdcall?

Sì.

Come funziona ?

Fa parte della dichiarazione di funzione.

Come fa il chiamante a sapere se dovrebbe liberare lo stack o no?

Il chiamante conosce le convenzioni di chiamata e può agire di conseguenza.

O è la responsabilità dei linker?

No, la convenzione di chiamata fa parte della dichiarazione di una funzione in modo che il compilatore sappia tutto ciò che deve sapere.

Se una funzione dichiarata come stdcall chiama una funzione (che ha una convenzione di chiamata come cdecl), o viceversa, sarebbe inappropriata?

No. Perché dovrebbe?

In generale, possiamo dire quale chiamata sarà più veloce – cdecl o stdcall?

Non lo so. Provalo.

a) Quando il chiamante chiama una funzione cdecl, come fa un chiamante a sapere se dovrebbe liberare lo stack?

Il modificatore cdecl fa parte del prototipo della funzione (o del puntatore della funzione, ecc.) In modo che il chiamante ottenga le informazioni da lì e agisca di conseguenza.

b) Se una funzione dichiarata come stdcall chiama una funzione (che ha una convenzione di chiamata come cdecl), o viceversa, sarebbe inappropriata?

No, va bene.

c) In generale, possiamo dire quale chiamata sarà più veloce – cdecl o stdcall?

In generale, mi asterrò da tali dichiarazioni. La distinzione è importante ad es. quando vuoi usare le funzioni va_arg. In teoria, potrebbe essere che stdcall sia più veloce e generi un codice più piccolo perché consente di combinare l’inserimento degli argomenti con l’apertura della gente del posto, ma OTOH con cdecl , puoi anche fare la stessa cosa, se sei intelligente.

Le convenzioni di chiamata che mirano ad essere più veloci di solito effettuano alcuni passaggi di registro.

Le convenzioni di chiamata non hanno nulla a che fare con i linguaggi di programmazione C / C ++ e sono piuttosto specifiche su come un compilatore implementa la lingua specificata. Se si utilizza costantemente lo stesso compilatore, non è necessario preoccuparsi di chiamare le convenzioni.

Tuttavia, a volte vogliamo che il codice binario compilato da diversi compilatori interagisca correttamente. Quando lo facciamo, dobbiamo definire qualcosa chiamato Application Binary Interface (ABI). L’ABI definisce come il compilatore converte il codice sorgente C / C ++ in codice macchina. Ciò includerà le convenzioni di chiamata, il nome mangling e il layout v-table. cdelc e stdcall sono due diverse convenzioni di chiamata comunemente usate sulle piattaforms x86.

Inserendo le informazioni sulla convenzione di chiamata nell’intestazione source, il compilatore saprà quale codice deve essere generato per interagire correttamente con l’eseguibile dato.