stdcall et cdecl

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

  1. Lorsqu’une fonction cdecl est appelée, comment un appelant peut-il savoir s’il doit libérer la stack? 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 stack 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, cela serait-il inapproprié?
  3. En général, pouvons-nous dire que l’appel sera plus rapide – cdecl ou stdcall?

    Raymond Chen donne un bon aperçu de ce que fait __stdcall et __cdecl .

    (1) L’appelant “sait” nettoyer la stack 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 comstackr 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); // ... // Comstackr usually complains but there's this cast here... windowClass.lpfnWndProc = reinterpret_cast(&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 stack car il connaîtra les conventions d’appel des fonctions impliquées.

    (2) Les deux manières devraient fonctionner. En fait, cela se produit assez fréquemment au 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 la convention __stdcall .

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

    Dans les arguments CDECL sont poussés dans la stack dans l’ordre inverse, l’appelant efface la stack 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 n’abandonne pas la stack, la calle fait.

    Vous demandez lequel est le plus rapide. Personne. Vous devez utiliser la convention d’appel native aussi longtemps que possible. Modifiez les conventions uniquement s’il n’y a pas de sortie possible 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, à savoir que le compilateur Visual C ++ utilise FASTCALL qui est théoriquement plus rapide en raison de l’utilisation plus étendue des registres de processeur.

    Habituellement, vous devez donner une signature de convention d’appel appropriée aux fonctions de rappel transmises à une bibliothèque externe, c.-à-d. Que le compilateur utilise par défaut une autre convention, nous devons marquer le rappel comme CDECL) ou différents rappels WinAPI doivent be STDCALL (WinAPI entier est STDCALL).

    Autre cas habituel, lorsque vous stockez des pointeurs sur certaines fonctions externes, par exemple pour créer un pointeur sur 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 comstackr 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 comstackr 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) 

    J’ai remarqué un message disant que ce n’est pas grave si vous appelez un __stdcall depuis un __cdecl ou __cdecl versa. Cela fait.

    La raison: avec __cdecl les arguments transmis aux fonctions appelées sont supprimés de la stack par la fonction appelante, dans __stdcall , les arguments sont supprimés de la stack par la fonction appelée. Si vous appelez une fonction __cdecl avec un __stdcall , la stack n’est pas nettoyée du tout, si bien que le __cdecl utilise une référence empilée pour que les arguments ou les adresses de retour utilisent les anciennes données au pointeur de stack actuel. Si vous appelez une fonction __stdcall depuis un __cdecl , la fonction __stdcall nettoie les arguments de la stack, puis la fonction __cdecl fait à nouveau, supprimant éventuellement les informations de retour des fonctions appelantes.

    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. Une fonction __stdcall préfixe avec un trait de soulignement et un suffixe avec un signe “@” et le nombre d’octets à supprimer. Par exemple, __cdecl f (x) est lié en tant que _x , __stdcall f(int x) est lié en tant que _f@4sizeof(int) est 4 octets)

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

    Il est spécifié dans le type de fonction. Lorsque vous avez un pointeur de fonction, il est supposé être cdecl s’il n’est pas explicitement appelé 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 suffit d’obtenir un type lorsque vous vous attendez à l’autre. En ce qui concerne la vitesse, ils remplissent tous les deux les mêmes rôles, juste dans un endroit très légèrement différent, c’est vraiment sans importance.

    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 sortis de la stack 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, dans un swoop, retourne à l’appelant et en même temps, “a”, “b” et “c” de la stack.

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

    STDCALL:

     /* 1. calling STDCALL in pseudo-assembler (similar to what the comstackr 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 comstackr 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. Corps STDCALL ‘Fonction’ dans pseaudo-assembleur * /
    copier ‘a’ (de la stack) pour enregistrer A
    copier ‘b’ (de la stack) pour enregistrer B
    append A et B, stocker le résultat dans A
    copier ‘c’ (de la stack) pour enregistrer B
    append A et B, stocker le résultat dans A
    revenir au code de l’appelant et en même temps pop ‘a’, ‘b’ et ‘c’ de la stack (a, b et
    c sont supprimés de la stack dans cette étape, aboutissent au registre A)

    L’appelant et l’appelé doivent utiliser la même convention au moment de l’invocation – c’est la seule façon de fonctionner de manière fiable. L’appelant et l’appelé suivent tous deux un protocole prédéfini – par exemple, qui doit nettoyer la stack. Si les conventions ne correspondent pas, votre programme s’exécute dans un comportement indéfini.

    Ceci n’est requirejs que par site d’appel – le code appelant 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 faire moins d’appels – par exemple, modifier l’algorithme.

    Ces choses sont spécifiques au compilateur et à la plate-forme. Ni le standard C, ni le standard C ++ ne disent quelque chose sur les conventions d’appel, à l’exception de extern "C" en C ++.

    Comment un appelant sait-il s’il doit libérer la stack?

    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 stack 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, cela serait-il inapproprié?

    Non, pourquoi devrait-il?

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

    Je ne sais pas. Essaye-le.

    a) Lorsqu’une fonction cdecl est appelée par l’appelant, comment un appelant peut-il savoir s’il doit libérer la stack?

    Le modificateur cdecl fait partie du prototype de fonction (ou du type de pointeur de fonction, etc.), de sorte que l’appelant récupère les informations et agit en conséquence.

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

    Non c’est bon.

    c) En général, peut-on dire que l’appel sera plus rapide – cdecl ou stdcall?

    En général, je m’abstiendrais de telles déclarations. La distinction est importante, par exemple. quand vous voulez utiliser les fonctions va_arg En théorie, stdcall pourrait être plus rapide et générer un code plus petit, car il permet de combiner les arguments avec les sections locales, mais cdecl avec cdecl , vous pouvez faire la même chose si vous êtes intelligent.

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

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

    Cependant, nous souhaitons parfois que le code binary compilé par différents compilateurs fonctionne correctement. Lorsque nous faisons cela, nous devons définir quelque chose appelé interface binary d’application (ABI). L’ABI définit comment le compilateur convertit la source C / C ++ en code machine. Cela inclut les conventions d’appel, la gestion des noms et la disposition des v-tables. 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é.