Programmation - Conventions de passage de paramètres sous x86

Décembre 2016


Les conventions de passage de paramètres sous x86







Lorsqu'un programme est compilé pour une architecture x86, le passage de paramètres pour une fonction peut se faire de différentes manières: par la pile de droite à gauche, de gauche à droite, par les registres, ou encore par un savant mélange des deux. Le problème se pose aussi de savoir qui va nettoyer la pile utilisée par une fonction qui vient de se terminer: la fonction appelante ou la fonction appelée?

Vu ces possibilités, il a bien fallu déterminer des normes qui ont été choisies en fonction de contraintes d'optimisation, de compatibilité, ou encore à cause de l'usage de fonctions un peu exotiques.

Ayant croisé dernièrement le chemin d'un tutoriel plutôt bien fait sur ce sujet, je commence à me demander s'il est utile de faire un tuto sur le même thème alors que l'essentiel a déjà été dit. Ceci dit, je pense qu'il serait utile d'explorer ce qu'il se passe du côté de langages comme le C avec des compilateurs largement utilisés comme gcc et Microsoft Visual C++.

Bon, mais est-il encore utile de rééxpliquer toutes ces normes qui ont déjà été détaillées dans le lien cité plus haut? Borf, pourquoi pas tant qu'à faire... Je ne ferai que des explications sommaires et je montrerai ce que donne l'implémentation en assembleur d'une même fonction simple pour chaque norme et si vous voulez approfondir et avoir plus de détail, rendez vous sur le lien du dessus.

Notez que la syntaxe que j'utiliserai en assembleur sera celle utilisée avec l'assembleur nasm.

Introduction


On va partir d'une fonction simple, appelée depuis une fonction principale main:
int soustraction (int a, int b, int c)
{
    return a - b - c;
}

int main ( int argc, char **argv)
{
    int nb1, nb2, nb3, resultat;
    
    //....quelques bout de code, on donne des valeurs aux trois variables nb.
    
    resultat = soustraction (nb1, nb2, nb3);
    
    //...reste du code
}

Un compilateur a besoin de savoir quelle convention il faut utiliser pour le passage des paramètres.
Le plus pratique c'est de le définir dans l'en-tête de la fonction.
Celà permet au compilateur de savoir comment compiler cette fonction
et les autres fonctions qui l'appèlent. Pour gcc, Microsoft Visual C++, et Delphi, je noterai, pour chaque norme, comment la signaler dans l'en-tête d'une fonction.

Cdecl


Nettoyage de la pile par: Fonction appelante.

Passage de paramètres: De droite à gauche par la pile.

En-tête de la fonction "soustraction" avec gcc:
int __attribute__((cdecl)) soustraction (int a, int b, int c)

En-tête de la fonction "soustraction" avec Microsoft Visual C++:
int __cdecl soustraction (int a, int b, int c)

Cdecl est la convention normalement utilisée sous Linux. Ceci est dû aux fonctions possédant un nombre de paramètres variable comme printf. En effet, pour nettoyer la pile, la fonction appelée a besoin de connaître exactement la quantité de paramètres à vider lorsqu'elle fini son execution. Voir à ce sujet l'opérateur ret: l'opérande qui lui est associé doit être une constante. (A vérifier)

Exemple d'implémentation en assembleur avec notre fonction:
soustraction:
    push ebp
    mov  ebp, esp
    
    mov  eax, [ebp + 8] ; a
    sub  eax, [ebp + 12] ; a = a - b
    sub  eax, [ebp + 16] ; a = a -c
    
    leave
    ret
    
main:
    ....
   ;Passage des paramètres de droite à gauche
   ;(Premier paramètre au sommet de la pile)
    push dword [ebp - 12] ; param c
    push dword [ebp - 8]  ; param b
    push dword [ebp - 4]  ; param a
    
    call soustraction
    ;La fonction appelante nettoie elle-même la pile
    add  esp, 12
    .....

Stdcall


Nettoyage de la pile par: Fonction appelée.

Passage de paramètres: De droite à gauche.

En-tête de la fonction "soustraction" avec gcc:
int __attribute__((stdcall)) soustraction (int a, int b, int c)

En-tête de la fonction "soustraction" avec Microsoft Visual C++:
int __stdcall soustraction (int a, int b, int c)

Stdcall est la convention de passage de paramètres utilisée par l'api
de Windows. Il semble qu'elle permette d'économiser la quantité de code produit car la fonction appelante n'a plus besoin de nettoyer la pile après l'appel d'une fonction soumise à cette convention.

Exemple d'implémentation en assembleur avec notre fonction:
soustraction:
    push ebp
    mov  ebp, esp
    
    mov  eax, [ebp + 8] ;a
    sub  eax, [ebp + 12] ; a = a - b
    sub  eax, [ebp + 16] ; a = a -c
    
    leave
    ;La fonction appelée nettoie la pile (ici 12 octets car 3 entiers * 4 octets)
    ret  12
    
main:
    ....
    push dword [ebp - 12] ; param c
    push dword [ebp - 8]  ; param b
    push dword [ebp - 4]  ; param a
    
    call soustraction
    ;La fonction appelante n'a plus besoin de nettoyer la pile

Fastcall


Nettoyage de la pile par: Fonction appelée

Passage de paramètres: Le premier dans le registre ecx, le second dans le registre edx, les suivants, s'il y en a, sont passés dans la pile de droite à gauche comme sous Stdcall ou Cdecl.

En-tête de la fonction "soustraction" avec gcc:
int __attribute__((fastcall)) soustraction (int a, int b, int c)

En tête de la fonction "soustraction" avec Microsoft Visual C++:
int __fastcall soustraction (int a, int b, int c)


A noter que la convention "register" utilisée sous Delphi semble être un Fastcall mais implémenté d'une manière différente.

Exemple d'implémentation en assembleur avec notre fonction:
soustraction:
    push ebp
    mov  ebp, esp
    
    mov  eax, ecx  ; a (permier paramètre dans ecx)
    sub  eax, edx  ; a = a -b (second paramètre dans edx)
    sub  eax, [ebp + 8] ; a = a -c (les autres paramètres sont passés dans la pile)
    
    leave
    ;Pile nettoyée par la fonction appelée
    ret  4
    
main:
    ....
    mov  ecx, [ebp - 4]   ; param a
    mov  edx, [ebp - 8]  ; param b
    push dword [ebp - 12] ; param c
    
    call soustraction

Register


Nettoyage de la pile par: Fonction appelée.

Passage de paramètres: Premier paramètre dans le registre eax, le second dans le registre ecx, le troisième dans le registre edx, et les autres dans la pile (vérifier dans quel sens).

En-tête de la fonction "soustraction" avec gcc:
???

En-tête de la fonction "soustraction" avec Microsoft Visual C++:
???


Register semble être une implémentation différente de Fastcall sous Borland Delphi.

Exemple d'implémentation en assembleur avec notre fonction:
soustraction:
    push ebp
    mov  ebp, esp
    
    sub  eax, ecx
    sub  eax, edx
    
    leave
   ;Pile nettoyée par la fonction appelée. Mais en dessous de trois paramètres, il n'y a rien à nettoyer.
    ret
    
main:
    ....
    mov  eax, [ebp - 4]   ; a
    mov  ecx, [ebp - 8]  ; b
    mov  edx, [ebp - 12] ; c
    
    call soustraction

Pascal


Nettoyage de la pile par: Fonction appelée.

Passage de paramètres: De gauche à droite.

En-tête de la fonction "soustraction" avec gcc:
On dirait qu'il n'existe pas d'attribut sous gcc pour ça. Voir une magouille en dessous pour résoudre ça.

En-tête de la fonction "soustraction" avec Microsoft Visual C++:
//Sous d'anciennes versions (n'est plus supporté):
int __pascal soustraction (int a, int b, int c)


La convention utilisée avec le langage Pascal...
On dirait qu'il n'est pas/plus supporté sous gcc et Visual C++.
Ceci dit, on peut ruser. La convention pascal c'est comme un stdcall à l'envers. Donc pour utiliser notre fonction soustraction située dans une librairie compilée avec la convention pascal, il suffit de déclarer l'en-tête de cette fonction en stdcall et d'inverser les paramètres:
Exemple avec gcc:
int __attribute__((stdcall)) soustraction (int c, int b, int a)
Mais c'est à tester.

Exemple d'implémentation en assembleur avec notre fonction:
soustraction:
    push ebp
    mov  ebp, esp
    
    mov  eax, [ebp + 16]; a
    sub  eax, [ebp + 12]; a=a-b
    sub  eax, [ebp + 8]; a=a-c
    
    leave
   ; Nettoyé par la fonction appelée
    ret  12
    
main:
   ; Passage des paramètres de gauche à droite
    push dword [ebp - 4]; a
    push dword [ebp - 8] ; b
    push dword [ebp - 12]; c (dernier paramètre au sommet de la pile)
    
    call soustraction

Sources de ce document et liens


A voir également :

Ce document intitulé «  Programmation - Conventions de passage de paramètres sous x86  » issu de CommentCaMarche (www.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.