Introduction à la convention d’appel 64bits, et rappels.

Paru le 08 mars 2007

Article rédigé par Matthieu Suiche
http://www.msuiche.net
Microsoft Student Partner

Sur cette page

I - 32-bits
II – 64bits

I - 32-bits

Auparavant, avec les processeurs 32bits, la convention utilisée était divisée en 3 catégories :
Et comme un beau dessin vaut mieux qu’un long discours nous y voilà !

1) STDCALL (Standard Call)

Généralement utilisée pour les API Win32, les fonctions de ce type qui ont leurs arguments sont « pushée » sur la pile, et la pile est nettoyée par l’appelé.

        VOID __stdcall CalledFunction(Argument0, Argument1, ..., Argument n-2, Argument n-1, Argument n );
      

Bb738063.64bits_1(fr-fr,MSDN.10).jpg

2) CDECL (Standard C Calling Convention)

En C/C++, le compilateur utilise par défaut cette convention ce qui explique qu’elle est assez courante. Ici les arguments sont aussi « pushé » sur la pile, mais à la différence des fonctions de type __stdcall, la pile est nettoyée par l’appelant.

        VOID __cdecl CalledFunction(Argument0, Argument1, ..., Argument n-2, Argument n-1, Argument n );
      

Bb738063.64bits_2(fr-fr,MSDN.10).jpg

3) FASTCALL (Fast calling convention)

L’avantage de la convention __fastcall est l’utilisation des registres pour passer les arguments à la fonction. Il est conseillé de ne pas utiliser le type __fastcall avec des fonctions de plus de 2 arguments.
Pourquoi ? Les deux premiers arguments passent par les registres ecx et edx et les arguments éventuellement supplémentaires sont poussés sur la pile. L’avantage est le gain de vitesse du à l’économisation de cycles d’instructions.

        VOID __cdecl CalledFunction(Argument0, Argument1);
      

Bb738063.64bits_3(fr-fr,MSDN.10).jpg

II – 64bits

En 64bits en raison de l’extension du jeu de registres et des registres eux-mêmes de 32bits à 64bits, la convention __fastcall est devenue la nouvelle convention d’appel par défaut. En effet, __cdecl n’est plus la convention appelée par défaut.
Le « nouveau » modèle de __fastcall passe les quatres premiers arguments dans les registres et les suivants dans la pile. A la différence de la convention x86 ou là uniquement 2 registres pouvaient passer.

1) Extension du jeu de registre.

Avec l’assembleur x86-64, de nouveau registres sont apparus définis de r8 à r15.
Pour ces nouveaux registres, il existe une notation bien précise pour leurs valeurs 32, 16 et 8bits.
Que voici donc :
rXb pour le registre 8bits, avec un b comme B…ernard ! euh non BYTE!
rXw pour le registre 16bits, avec un w comme…. WORD !
rXd pour le registre 32bits, avec un d comme DWORD !
rX pour le registre 64bits.

Ce qui nous donne
rX = 1 Quadword = 2 dwords = 4 words = 8 bytes
rXd = 1 Dword = 2 words = 4 bytes
rXw = 1 Word = 2bytes

En ce qui concerne les registres déjà existant, ils se voient remplacer le “e” par un r, par exemple l’extension d’eax en x86-64 est rax.

Voilà un petit tableau récapitulatif.

Bb738063.64bits_4(fr-fr,MSDN.10).jpg

Bien sûr, il en est de même pour les registres rcx, rdx, rbx, rsp, rbp, rsi, rdi, rip.

2) __fastcall

L’Application Binary Interface(ABI) x64 est une convention d’appel de type fastcall sur 4 registres, avec prise en charge de la pile si il y a plus de 4 arguments dans ce qu’on appelle le « shadow space ».
Les arguments sont passés dans l’ordre suivant à l’intérieur les registres:
rcx, rdx, r8 et r9,
Pour les arguments de type double/float, ils sont pris en charge par les registres par registres, dans l’ordre toujours, dans les registres XMM :
XMM0, XMM1, XMM2, XMM3

Les arguments supérieurs à 8 octects (64bits), comme les paramètres de type _m128, ne poussent que leurs adresses à la fonction appelée.

Voici quelques exemples pour mieux illustrer ce qui vient d’être dit avec des fonctions de 4 arguments:

Exemple 1: Cocktail Molotov d’arguments: int, long, _m64, short

        Void function_1(int a, long b, __m64 c, short d);
        // a dans rcx, b dans rdx, c dans r8, d dans r9

        Un appel classique en C tel que le suivant :
        function_1(a, b, c, d);

        Donnerait à peu près ceci une fois compilé:
        mov r9w, word ptr [d]
        mov r8, qword ptr [c]
        mov edx, dword ptr [b]
        mov ecx, dword ptr [a]
        Call function_1

      

Exemple 2: Argument de type float, double

        Void function_2(float a, double b, float c, double d);
        // a dans XMM0, b dans XMM1, c dans XMM2, d dans XMM3

        Un appel classique en C tel que le suivant :
        function_2(a, b, c, d);

        Donnerait à peu près ce ci une fois compilé:
        movss   xmm3, qword ptr [d]
        movlpd  xmm2, oword ptr [c]
        movlpd  xmm1, oword ptr [b]
        movss   xmm0, qword ptr [a]
        call Function_2

      

Exemple 3: Mixe de XMM et de registres 64bits standard.

        Void Function_3(int a, float b, int c, double d);
        // a dans rcx, b dans XMM1, c dans r8, d dans XMM3

        Un appel classique en C tel que le suivant :
        function_3(a, b, c, d);

        Donnerait à peu près ceci une fois compilé:
        movlpd  xmm3, oword ptr [d]
        mov     r8d, dword ptr [c]
        movss   xmm1, qword ptr [b]
        mov     ecx, dword ptr  [a]
        call    Function_3

      

Exemple 4: Cocktail exotique d’arguments :

        Void function_4(int a, _m128 b, struct c, float d);
        // a dans rcx, pointeur de b dans rdx, pointeur de c dans r8, d dans XMM3

        Un appel classique en C tel que le suivant :
        function_4(a, b, c, d);

        Donnerait à peu près ceci une fois compilé:
        movss   xmm1, qword ptr [d]
        lea r8, [c]
        lea rdx, [b]
        mov ecx, dword ptr [a]
        call function_4
      

Les registres rax, rcx, rdx, r8, r9, r10, et r11 sont considérés comme volatiles, ce qui signifie qu’ils sont considérés comme détruit lors d’un appel à une fonction. Rcx, rdx, r8, et r9 sont initialisés avant l’appel de la fonction avant d’appeler la fonction.
Chaque fonction réserve 32 octects (4*sizeof(ULONGLONG)) sur la partie « base » pile pour sauvegarder les arguments registres, même si la fonction dispose de moins de 4 arguments.

Voici à quoi ressemble la pile après qu’une fonction A est appelée une fonction B, et que cette dernière vient juste de sauvegarder les arguments que A vient de lui faire passer. Attention, ici la pile n’a pas encore été ajustée par un « sub esp, XXh »

Bb738063.64bits_5(fr-fr,MSDN.10).jpg

Les registres rbx, rbp, rdi, rsi, r12, r13, r14, et r15 sont considérés comme non-volatiles, ce qui signifie qu’ils doivent être sauvegardés durant le prologue de la fonction et restaurés par l’épilogue de la fonction appelée.
Les valeurs de retour sont stockées soit dans rax pour les fonctions dites « classique » soit dans XMM0 pour les fonctions de type double ou float.

Voilà, maintenant vous savez comment les conventions x86-64 et x86 fonctionnent.

Pour une documentation plus approfondie, voici quelques documents traitant du 64bits.