Un appel de fonction est un contrat. L’appelant doit placer les arguments quelque part où l’appelé ira les chercher, l’appelé doit laisser le résultat quelque part où l’appelant le lira, et tous deux doivent s’accorder sur les registres qu’on a le droit d’écraser. Ce contrat, c’est la convention d’appel. Le CPU ne l’impose pas — ce sont les compilateurs qui la respectent, pour que du code produit par des compilateurs différents puisse s’appeler.
Deux conventions couvrent l’essentiel de ce que vous lirez sous Linux et macOS : System V AMD64 pour le code 64 bits et cdecl pour le code 32 bits.
call et ret
Tout commence par deux instructions :
call fempile l’adresse de retour — celle de l’instruction qui suit lecall— puis saute àf.retdépile cette adresse dansrip, et l’exécution reprend juste après lecall.
C’est grâce à la pile qu’une fonction sait où revenir. C’est aussi pourquoi la corrompre est si dangereux : si l’adresse de retour est écrasée, ret saute là où pointe la nouvelle valeur.
System V x86-64 : les arguments dans les registres
Sous Linux et macOS 64 bits, les six premiers arguments entiers ou pointeurs passent dans des registres, dans cet ordre :
| Argument | 1er | 2e | 3e | 4e | 5e | 6e |
|---|---|---|---|---|---|---|
| Registre | rdi | rsi | rdx | rcx | r8 | r9 |
Les arguments suivants sont empilés. Les arguments flottants utilisent xmm0–xmm7. La valeur de retour revient dans rax (xmm0 pour les flottants).
Voici add2(3, 4) dans sa forme la plus simple. La démo s’arrête juste après le call :
- _start:
- mov edi, 3 ; 1er argument
- mov esi, 4 ; 2e argument
- call add2 ; empile l’adresse de retour, saute
- ret
- add2:
- push rbp ; prologue : sauvegarde le pointeur de cadre de l’appelant
- mov rbp, rsp ; et ouvre notre propre cadre
- mov eax, edi
- add eax, esi ; la valeur de retour va dans eax
- pop rbp ; épilogue : restaure le pointeur de cadre de l’appelant
- ret ; dépile l’adresse de retour dans rip
Regardez la pile : call a empilé 0x401003, étiqueté return address → _start+3. Continuez :
push rbpsauvegarde le pointeur de cadre de l’appelant juste en dessous.mov rbp, rspfait pointerrbpsur cette valeur sauvegardée. Désormais, l’adresse de retour est toujours en[rbp+8].mov eax, edi/add eax, esicalculent 3 + 4 danseax.pop rbpetretdéfont le cadre et reviennent en_start+3aveceax = 7.
Le cadre de pile
push rbp / mov rbp, rsp est le prologue classique, et leave (ou mov rsp, rbp + pop rbp) suivi de ret est l’épilogue. Entre les deux, une fonction qui a besoin de variables locales retire une valeur de rsp pour réserver de la place, et adresse tout relativement à rbp :
| Emplacement | Contient |
|---|---|
[rbp+16] et au-delà | arguments sur la pile (7e et suivants) |
[rbp+8] | adresse de retour |
[rbp] | rbp sauvegardé de l’appelant |
[rbp-4], [rbp-8], … | variables locales |
C’est cette organisation qui permet aux débogueurs d’afficher la pile d’appels : chaque rbp sauvegardé pointe sur le cadre précédent, formant une liste chaînée qui remonte la pile. Le code optimisé se passe souvent de rbp et adresse les variables locales depuis rsp.
Quelques autres règles System V dont vous verrez les effets :
- Alignement :
rspdoit être multiple de 16 juste avant uncall, d’où les fonctions qui retirent « trop » àrsp. - Zone rouge : une fonction qui n’appelle rien peut utiliser les 128 octets sous
rspsans le déplacer. - Pour les fonctions variadiques comme
printf,alcontient le nombre de registres vectoriels utilisés — c’est lemov eax, 0que les compilateurs placent juste avantcall printf.
cdecl 32 bits : les arguments sur la pile
Le x86 32 bits a trop peu de registres pour en réserver aux arguments : cdecl les passe tous sur la pile, empilés de droite à gauche — le premier argument se retrouve donc au plus près de l’adresse de retour. La valeur de retour est dans eax, et c’est l’appelant qui retire les arguments ensuite.
- _start:
- push 4 ; arguments de droite à gauche…
- push 3 ; …le 1er se retrouve au sommet
- call add2
- add esp, 8 ; l’appelant retire ses 2 arguments
- ret
- add2:
- push ebp
- mov ebp, esp
- mov eax, DWORD PTR [ebp+8] ; 1er argument
- add eax, DWORD PTR [ebp+12] ; 2e argument
- pop ebp
- ret
La démo s’arrête juste après mov ebp, esp. Avec ebp = 0x7fffffcc, la pile se lit, depuis le sommet :
| Adresse | Relativement à ebp | Contient |
|---|---|---|
0x7fffffcc | [ebp] | ebp sauvegardé |
0x7fffffd0 | [ebp+4] | adresse de retour |
0x7fffffd4 | [ebp+8] | 3 — 1er argument |
0x7fffffd8 | [ebp+12] | 4 — 2e argument |
Ce motif [ebp+8] = premier argument est l’un des plus reconnaissables du désassemblage 32 bits. Après ret, le add esp, 8 de l’appelant jette les deux arguments.
D’autres conventions 32 bits diffèrent dans les détails : stdcall (utilisée par l’API Win32) fait nettoyer la pile par l’appelé avec ret 8, et fastcall passe les deux premiers arguments dans ecx et edx.
Qui sauvegarde quels registres
Une convention répartit aussi les registres en deux groupes :
| System V x86-64 | cdecl (32 bits) | |
|---|---|---|
| Préservés par l’appelé — une fonction doit les restaurer avant de revenir | rbx, rbp, r12–r15 (et rsp) | ebx, esi, edi, ebp |
| Non préservés — n’importe quel appel peut les écraser | rax, rcx, rdx, rsi, rdi, r8–r11 | eax, ecx, edx |
Si une fonction utilise rbx, vous verrez donc un push rbx dans le prologue et un pop rbx avant le retour. Et si un appelant a besoin qu’une valeur dans rcx survive à un appel, il doit la sauvegarder d’abord — ou la garder dans un registre préservé.
Windows fait autrement
Windows 64 bits a sa propre convention : les quatre premiers arguments passent dans rcx, rdx, r8, r9, l’appelant réserve 32 octets d’« espace fantôme » sur la pile, et rsi/rdi sont préservés par l’appelé. Avant de nommer les arguments d’une fonction dans un désassemblage, vérifiez le système visé par le binaire.
Pour pratiquer
Dans le simulateur, les exemples Function call et Recursion (factorial) sont du C compilé. Passez de 64-bit à 32-bit pour voir la même fonction recevoir ses arguments dans edi ou en [ebp+8], et suivez les cadres qui s’empilent dans la vue de la pile.