stdcall et cdecl

89

Il existe (entre autres) deux types de conventions d'appel - stdcall et cdecl . J'ai quelques questions à leur sujet:

  1. Lorsqu'une fonction cdecl est appelée, comment un appelant sait-il s'il doit libérer la pile? Sur le site d'appel, l'appelant sait-il si la fonction appelée est une fonction cdecl ou stdcall? Comment ça marche ? Comment l'appelant sait-il s'il doit libérer la pile ou non? Ou est-ce la responsabilité des linkers?
  2. Si une fonction déclarée comme stdcall appelle une fonction (qui a une convention d'appel comme cdecl), ou l'inverse, serait-ce inapproprié?
  3. En général, pouvons-nous dire que quel appel sera plus rapide - cdecl ou stdcall?
Rakesh Agarwal
la source
9
Il existe de nombreux types de conventions d'appel, dont il ne s'agit que de deux. en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck
1
S'il vous plaît, marquez une bonne réponse
ceztko

Réponses:

77

Raymond Chen donne un bon aperçu de ce __stdcallque __cdeclfait et fait .

(1) L'appelant «sait» nettoyer la pile après avoir appelé une fonction car le compilateur connaît la convention d'appel de cette fonction et génère le code nécessaire.

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

Il est possible de ne pas correspondre à la convention d'appel , comme ceci:

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

Tant d'échantillons de code se trompent, ce n'est même pas drôle. C'est censé être comme ça:

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

Cependant, en supposant que le programmeur n'ignore pas les erreurs du compilateur, le compilateur générera le code nécessaire pour nettoyer correctement la pile car il connaîtra les conventions d'appel des fonctions impliquées.

(2) Les deux méthodes devraient fonctionner. En fait, cela se produit assez fréquemment au moins dans le code qui interagit avec l'API Windows, car __cdeclc'est la valeur par défaut pour les programmes C et C ++ selon le compilateur Visual C ++ et les fonctions WinAPI utilisent la __stdcallconvention .

(3) Il ne devrait pas y avoir de réelle différence de performance entre les deux.

In silico
la source
+1 pour un bon exemple et l'article de Raymond Chen sur les appels à l'historique des congrès. Pour toute personne intéressée, les autres parties sont également une bonne lecture.
OregonGhost
+1 pour Raymond Chen. Btw (OT): Pourquoi est-ce que je ne trouve pas les autres parties en utilisant le champ de recherche de blog? Google les trouve, mais pas les blogs MSDN?
Nordic Mainframe
44

Dans CDECL, les arguments sont poussés sur la pile dans l'ordre inverse, l'appelant efface la pile et le résultat est renvoyé via le registre du processeur (plus tard, je l'appellerai "registre A"). Dans STDCALL, il y a une différence, l'appelant ne vide pas la pile, l'appelant le fait.

Vous demandez lequel est le plus rapide. Personne. Vous devez utiliser la convention d'appel native aussi longtemps que vous le pouvez. Modifiez la convention uniquement s'il n'y a pas de solution, lors de l'utilisation de bibliothèques externes qui nécessitent l'utilisation d'une certaine convention.

En outre, il existe d'autres conventions que le compilateur peut choisir par défaut, c'est-à-dire que le compilateur Visual C ++ utilise FASTCALL qui est théoriquement plus rapide en raison d'une utilisation plus étendue des registres du processeur.

Habituellement, vous devez donner une signature de convention d'appel appropriée aux fonctions de rappel passées à une bibliothèque externe, c'est-à-dire que le rappel qsortde la bibliothèque C doit être CDECL (si le compilateur utilise par défaut une autre convention, nous devons marquer le rappel comme CDECL) ou divers rappels WinAPI doivent être STDCALL (tout WinAPI est STDCALL).

Un autre cas habituel peut être celui où vous stockez des pointeurs vers certaines fonctions externes, c'est-à-dire que pour créer un pointeur vers la fonction WinAPI, sa définition de type doit être marquée avec STDCALL.

Et ci-dessous est un exemple montrant comment le compilateur le fait:

/* 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
la source
Remarque: __fastcall est plus rapide que __cdecl et STDCALL est la convention d'appel par défaut pour Windows 64 bits
dns
ohh; donc il doit afficher l'adresse de retour, ajouter la taille du bloc d'argument, puis passer à l'adresse de retour sautée plus tôt? (une fois que vous appelez ret, vous ne pouvez plus nettoyer la pile, mais une fois que vous nettoyez la pile, vous ne pouvez plus appeler ret (puisque vous devrez vous enterrer pour revenir sur la pile, ce qui vous ramènera au problème que vous n'avez pas nettoyé la pile).
Dmitry
alternativement, pop revenir à reg1, définir le pointeur de pile sur le pointeur de base, puis sauter à reg1
Dmitry
Sinon, déplacez la valeur du pointeur de pile du haut de la pile vers le bas, nettoyez, puis appelez ret
Dmitry
15

J'ai remarqué une publication qui dit que ce n'est pas grave si vous appelez __stdcallun __cdeclou vice versa. Cela fait.

La raison: avec __cdeclles arguments qui sont passés aux fonctions appelées sont supprimés de la pile par la fonction appelante, dans __stdcall, les arguments sont supprimés de la pile par la fonction appelée. Si vous appelez une __cdeclfonction avec a __stdcall, la pile n'est pas nettoyée du tout, donc éventuellement, lorsque le __cdeclutilise une référence empilée pour les arguments ou l'adresse de retour, les anciennes données seront utilisées au pointeur de pile actuel. Si vous appelez une __stdcallfonction à partir de a __cdecl, la __stdcallfonction nettoie les arguments sur la pile, puis la __cdeclfonction le fait à nouveau, en supprimant éventuellement les informations de retour des fonctions appelantes.

La convention Microsoft pour C essaie de contourner cela en modifiant les noms. Une __cdeclfonction est précédée d'un trait de soulignement. Une __stdcallfonction préfixe avec un trait de soulignement et suffixe avec un signe arobase «@» et le nombre d'octets à supprimer. Par exemple, __cdeclf (x) est lié comme _f, __stdcall f(int x)est lié comme _f@4sizeof(int)est 4 octets)

Si vous parvenez à dépasser l'éditeur de liens, profitez du désordre de débogage.

Lee Hamilton
la source
3

Je veux améliorer la réponse de @ adf88. Je pense que le pseudocode du STDCALL ne reflète pas la façon dont cela se passe dans la réalité. 'a', 'b' et 'c' ne sont pas extraits de la pile dans le corps de la fonction. Au lieu de cela, ils sont sautés par l' retinstruction ( ret 12qui serait utilisée dans ce cas) qui d'un seul coup retourne à l'appelant et en même temps fait apparaître «a», «b» et «c» de la pile.

Voici ma version corrigée selon ma compréhension:

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
la source
2

Il est spécifié dans le type de fonction. Lorsque vous avez un pointeur de fonction, il est supposé être cdecl sinon explicitement stdcall. Cela signifie que si vous obtenez un pointeur stdcall et un pointeur cdecl, vous ne pouvez pas les échanger. Les deux types de fonctions peuvent s'appeler sans problème, il s'agit simplement d'obtenir un type lorsque vous attendez l'autre. Quant à la vitesse, ils jouent tous les deux les mêmes rôles, juste dans un endroit très légèrement différent, c'est vraiment hors de propos.

Chiot
la source
1

L'appelant et l'appelé doivent utiliser la même convention au moment de l'invocation - c'est la seule façon dont cela pourrait fonctionner de manière fiable. L'appelant et l'appelé suivent un protocole prédéfini - par exemple, qui doit nettoyer la pile. Si les conventions ne correspondent pas, votre programme se heurte à un comportement indéfini - il se bloque probablement de manière spectaculaire.

Ceci n'est requis que par site d'invocation - le code d'appel lui-même peut être une fonction avec n'importe quelle convention d'appel.

Vous ne devriez pas remarquer de réelle différence de performances entre ces conventions. Si cela devient un problème, vous devez généralement effectuer moins d'appels - par exemple, modifiez l'algorithme.

dents acérées
la source
1

Ces éléments sont spécifiques au compilateur et à la plate-forme. Ni le standard C ni le standard C ++ ne disent quoi que ce soit sur les conventions d'appel, sauf extern "C"en C ++.

comment un appelant sait-il s'il doit libérer la pile?

L'appelant connaît la convention d'appel de la fonction et gère l'appel en conséquence.

Sur le site d'appel, l'appelant sait-il si la fonction appelée est une fonction cdecl ou stdcall?

Oui.

Comment ça marche ?

Cela fait partie de la déclaration de fonction.

Comment l'appelant sait-il s'il doit libérer la pile ou non?

L'appelant connaît les conventions d'appel et peut agir en conséquence.

Ou est-ce la responsabilité des linkers?

Non, la convention d'appel fait partie de la déclaration d'une fonction afin que le compilateur sache tout ce qu'il doit savoir.

Si une fonction déclarée comme stdcall appelle une fonction (qui a une convention d'appel comme cdecl), ou l'inverse, serait-ce inapproprié?

Non. Pourquoi devrait-il?

En général, pouvons-nous dire que quel appel sera plus rapide - cdecl ou stdcall?

Je ne sais pas. Essaye-le.

sellibitze
la source
0

a) Lorsqu'une fonction cdecl est appelée par l'appelant, comment un appelant sait-il s'il doit libérer la pile?

Le cdeclmodificateur fait partie du prototype de fonction (ou du type de pointeur de fonction, etc.) de sorte que l'appelant obtient les informations à partir de là et agit en conséquence.

b) Si une fonction qui est déclarée comme stdcall appelle une fonction (qui a une convention d'appel comme cdecl), ou l'inverse, serait-ce inapproprié?

Non c'est bon.

c) En général, pouvons-nous dire que quel appel sera plus rapide - cdecl ou stdcall?

En général, je m'abstiendrai de telles déclarations. La distinction compte par exemple. lorsque vous souhaitez utiliser les fonctions va_arg. En théorie, cela pourrait être stdcallplus rapide et générer du code plus petit car cela permet de combiner le popping les arguments avec le popping les locaux, mais avec OTOH cdecl, vous pouvez faire la même chose, aussi, si vous êtes intelligent.

Les conventions d'appel qui visent à être plus rapides font généralement un passage de registre.

jpalecek
la source
-1

Les conventions d'appel n'ont rien à voir avec les langages de programmation C / C ++ et sont plutôt spécifiques sur la façon dont un compilateur implémente le langage donné. Si vous utilisez systématiquement le même compilateur, vous n'aurez jamais à vous soucier des conventions d'appel.

Cependant, nous voulons parfois que le code binaire compilé par différents compilateurs interagisse correctement. Lorsque nous le faisons, nous devons définir quelque chose appelé l'interface binaire d'application (ABI). L'ABI définit la manière dont le compilateur convertit la source C / C ++ en code machine. Cela inclura les conventions d'appel, la modification des noms et la disposition de la v-table. cdelc et stdcall sont deux conventions d'appel différentes couramment utilisées sur les plates-formes x86.

En plaçant les informations sur la convention d'appel dans l'en-tête source, le compilateur saura quel code doit être généré pour interagir correctement avec l'exécutable donné.

Doron
la source