web-dev-qa-db-fra.com

stdcall et cdecl

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

  1. Quand une fonction cdecl est appelée, comment un appelant peut-il savoir s'il doit libérer la pile? Sur le site de l'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 en tant que stdcall appelle une fonction (qui a une convention d'appel en tant que cdecl), ou inversement, cela serait-il inapproprié?
  3. En général, pouvons-nous dire quel appel sera le plus rapide - cdecl ou stdcall?
80
Rakesh Agarwal

Raymond Chen donne un bon aperçu de ce que __stdcall et __cdecl 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 que ce n'est même pas drôle. C'est supposé ê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, il 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 voies devraient fonctionner. En fait, cela se produit assez souvent, du moins dans le code qui interagit avec l'API Windows, car __cdecl est la valeur par défaut pour les programmes C et C++ selon le compilateur Visual C++ et les fonctions WinAPI utilisent le __stdcall convention .

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

72
In silico

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

Vous demandez lequel est le plus rapide. Personne. Vous devez utiliser la convention d'appel natif aussi longtemps que vous le pouvez. Modifiez la convention uniquement s'il n'y a pas d'issue, lorsque vous utilisez des bibliothèques externes nécessitant l'utilisation de certaines conventions.

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 de l’utilisation plus étendue des registres du processeur.

Habituellement, vous devez attribuer une signature appropriée à la convention d’appel pour les fonctions de rappel transmises à une bibliothèque externe, c’est-à-dire que le rappel à qsort à partir de la bibliothèque C doit être CDECL (si le compilateur utilise par défaut une autre convention, nous devons marquer le rappel en tant que CDECL). ou divers rappels WinAPI doivent être STDCALL (toute WinAPI est STDCALL).

Un autre cas habituel peut être lorsque vous stockez des pointeurs sur certaines fonctions externes, par exemple, pour créer un pointeur sur une 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)
41
adf88

J'ai remarqué une publication qui dit que peu importe si vous appelez un __stdcall À partir d'un __cdecl Ou vice versa. Cela fait.

La raison: avec __cdecl, Les arguments passés aux fonctions appelées sont supprimés de la pile par la fonction appelante. Dans __stdcall, Les arguments sont retirés de la pile par la fonction appelée. Si vous appelez une fonction __cdecl Avec un __stdcall, La pile n'est pas nettoyée du tout. Par conséquent, lorsque __cdecl Utilise une référence empilée pour les arguments ou l'adresse de retour utiliser les anciennes données sur le pointeur de pile actuel. Si vous appelez une fonction __stdcall À partir d'un __cdecl, La fonction __stdcall Nettoie les arguments de la pile, puis la fonction __cdecl Le fait à nouveau, supprimer éventuellement les fonctions appelantes renvoyer des informations.

La convention de Microsoft pour C tente de contourner ce problème en modifiant les noms. Une fonction __cdecl Est préfixée par un trait de soulignement. Un préfixe de fonction __stdcall Avec un tiret bas et un suffixe avec un signe @ et le nombre d'octets à supprimer. Par exemple, __cdecl f(x) est lié en tant que _f, __stdcall f(int x) est lié en tant que _f@4sizeof(int) est 4 octets)

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

14
Lee Hamilton

Je veux améliorer la réponse de @ adf88. Je pense que le pseudocode pour le STDCALL ne reflète pas la manière dont cela se passe dans la réalité. 'a', 'b' et 'c' ne sont pas sortis de la pile dans le corps de la fonction. Au lieu de cela, ils sont sautés par l'instruction ret (ret 12 _ serait utilisé dans ce cas) qui en un seul saut revient en arrière à l'appelant et en même temps fait sortir "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)
3
golem

C'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èmes, c’est obtenir un type quand on attend l’autre. En ce qui concerne la vitesse, ils jouent tous les deux le même rôle, mais à un endroit légèrement différent, cela n’a vraiment aucune importance.

2
Puppy

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 tous deux un protocole prédéfini - par exemple, qui doit nettoyer la pile. Si les conventions ne concordent pas, votre programme rencontre un comportement indéfini - il est probable que le plantage soit 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 performance entre ces conventions. Si cela devient un problème, vous devez généralement effectuer moins d'appels - par exemple, modifiez l'algorithme.

1
sharptooth

Ces choses sont spécifiques au compilateur et à la plate-forme. Ni le standard C, ni le standard C++ ne disent quoi que ce soit à propos des conventions d'appel, à l'exception de 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 de l'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 en tant que stdcall appelle une fonction (qui a une convention d'appel en tant que cdecl), ou inversement, cela serait-il inapproprié?

Pourquoi devrait-il?

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

Je ne sais pas. Essaye-le.

1
sellibitze

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

Le modificateur cdecl fait partie du prototype de fonction (ou du type de pointeur de fonction, etc.) afin que l'appelant obtienne les informations à partir de là et agisse 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, cela serait-il inapproprié?

Non ça va.

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

En général, je m'abstiendrais de telles déclarations. La distinction compte par exemple. lorsque vous souhaitez utiliser les fonctions va_arg. En théorie, il se pourrait que stdcall soit plus rapide et génère un code plus petit, car il permet de combiner les arguments contextuels avec les paramètres locaux, mais avec OTOH avec cdecl, vous pouvez également faire la même chose. , si vous êtes malin.

Les conventions d’appel qui visent à être plus rapides font généralement passer des registres.

0
jpalecek