[PROLOG] 0x002, Les conventions d'appels | (French version)
November 11, 2022 prolog
Comme leur nom l’indique, les conventions d’appels définissent des règles communes en matière de passage d’arguments et de valeurs retour vers et par une fonction. Bah oui me direz vous… mais voilà, la difficulté c’est qu’il existe plusieurs de ces conventions. Par exemple, elles sont différentes entre un LINUX 64-bit et un WINDOWS 64-bit, et encore différentes entre un Linux 32-bits et un Linux 64-bits ….
Comme il en existe beaucoup, nous allons nous limiter aux conventions d’appels sur processeurs Intel X86 (32-bits et 64 bits). D’une manière générale, je laisse de côté sur mon blog tout ce qui va concerner les processeurs ARM, et donc n’écrit pas pour les considérations niveau des MAC M1 construits sur un ARM64.
Avant de parler des OS (LINUX et WINDOWS 64-bits), étudions 3 des principales conventions d’appel que l’on rencontre dans les architectures 32-bits Intel :
cdecl
stdcall
fastcall
CDECL
Cette convention d’appel est largement utilisée par les compilateurs C sur la plateforme X86 32-bits
.
En convention cdecl
la responsabilité de nettoyage de la stack incombe à la procédure appelante (je vous renvoie au billet précédent pour cerner les notions essentielles de Stack, appelant, appelé). Pour rappel “nettoyer la pile” signifie que l’appelant doit repositionner le registre ESP (pointeur de pile) sur l’adresse qu’il avait avant l’appel (avant son call)
Les principales (liste non-exaustive) propriétés de cette convention d’appel sont:
- les arguments sont passés aux fonctions par la stack
- les valeurs retour des fonctions sont renvoyées par le registre EAX pour les entiers
- les valeurs retour des fonctions sont renvoyées par le registre x87 ST0 pour les réels
- les registres EAX, ECX et EDX doivent être sauvegardés par l’appelant
- tous les autres registres doivent être sauvegardés (au besoin) par l’appelé
push a3
push a2
push a1
call additionne
add esp,12 ; et hop je te remets le pointeur de pile où il était avant le call
STDCALL
Cette convention est utilisée pour les appels de fonctions de l’API Win32.
Les principales (liste non exhaustive) propriétés de cette convention d’appel sont:
- Cette fois-ci, c’est à la fonction appelée qu’incombe la responsabilité de nettoyer la stack
- les arguments sont passés de la droite vers la gauche
- les valeurs retour des fonctions sont renvoyées par le registre EAX pour les entiers et les adresses
- les valeurs retour des fonctions sont renvoyées par les registres DX, BX, AX pour les réels
- les registres EAX, ECX et EDX doivent être sauvegardés par l’appelant
- tous les autres registres doivent être sauvegardés (au besoin) par l’appelé
push a3
push a2
push a1
call additionne
additionne:
; ....
ret 12 ; et hop je te remets le pointeur de pile où il était avant le call
Microsoft FASTCALL
Les principales (liste non exhaustive) propriétés de cette convention d’appel sont:
- utilise les registres ECX et EDX pour passer les 2 premiers arguments (de la gauche vers la droite) à la fonction appelée. Puis utilise la stack pour passer les arguments suivants (mais cette fois-ci de le la droite vers la gauche)
- c’est à la fonction appelée qu’incombe la responsabilité de nettoyer la stack
Attention, sur un OS 64 bits, les compilateurs ignoreront (sans lever d’erreur) la directive __fastcall
. En effet, ces compilateurs doivent produire du code respectant l’ABI de l’OS. Et pour les conventions 64-bits, c’est juste en dessous …
Basculons maintenant dans le monde merveilleux des OS 64-bits, monde qui se décompose principalement en 2 conventions d’appel:
- Linux 64-bit (System V AMD64 ABI)
- Windows 64-bit (fastcall)
Windows 64-bit
Cette convention est utilisée dans les DLLs Windows x86-64 (à la place de stdcall
en win32). Elle est assez proche d’une convention. fastcall
Les 4 premiers arguments sont passés dans les registres RCX, RDX, R8 et R9, les arguments suivants sont passés sur la pile.
L’appelant doit également préparer un espace sur la pile pour 32 octets, soit 4 mots de 64 bits, l’appelé pourra y sauvegarder les 4 premiers arguments. On appelle cet espace mémoire alloué
le shadow space
.L’appelé est responsable de rétablir la valeur du pointeur de pile à la valeur qu’il avait avant l’appel de la fonction
Les registres RAX, RCX, RDX, R8, R9, R10, R11 sont considérés comme volatiles, c.-à-d. que leur sauvegarde est de la responsabilité de l’appelant.
Les registres RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15 sont considérés comme non volatiles
Linux 64-bit (et MacOS ;-)
Cette convention est définie dans l’ABI SYSTEM V AMD 64 et c’est celle de beaucoup d’UNIX, LINUX et MacOs. Elle est, au final, assez similaire à la convention Windows 64-bit, en voici les grands principes :
Les 6 premiers arguments sont passés dans les registres RDI, RSI, RCX, RDX, R8 et R9, les arguments suivants sont passés sur la pile.
Pas de création de
shadow space
.L’appelé est responsable de rétablir la valeur du pointeur de pile à la valeur qu’il avait avant l’appel de la fonction
Les registres RAX, RCX, RDX, R8, R9, R10, R11 sont considérés comme volatiles, c.-à-d. que leur sauvegarde est de la responsabilité de l’appelant.
Les registres RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15 sont considérés comme non volatiles
Et bien voilà pour l’essentiel à connaître sur les conventions d’appel et les ABI.
Résumons-nous, si vous avez suivi la suite des 3 billets PROLOG (Asm, Mémoire et celui-ci) alors vous êtes en mesure de mettre toutes ces briques ensemble. Et pour cela, il n’y a pas mieux qu’une petite exploitation d’un premier Buffer Stack Overflow - simple - (BOF pour les intimes ;-). Et bien c’est l’exercice que je vous propose dans le billet suivant pour clôturer cette série PROLOG et ensuite passer aux choses sérieuses …
–