Skip to content

Niveau 1 · Chapitre 1.3

Conventions d’appel (System V, cdecl)

Comment les fonctions reçoivent leurs arguments et rendent leur résultat : call et ret, le cadre de pile, System V x86-64, cdecl 32 bits, et qui sauvegarde quoi.

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 f empile l’adresse de retour — celle de l’instruction qui suit le call — puis saute à f.
  • ret dépile cette adresse dans rip, et l’exécution reprend juste après le call.

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 :

Argument1er2e3e4e5e6e
Registrerdirsirdxrcxr8r9

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 :

Live · System V : add2(3, 4)
program— ▸ is the next instruction
  1. _start:
  2. mov edi, 3 ; 1er argument
  3. mov esi, 4 ; 2e argument
  4. call add2 ; empile l’adresse de retour, saute
  5. ret
  6. add2:
  7. push rbp ; prologue : sauvegarde le pointeur de cadre de l’appelant
  8. mov rbp, rsp ; et ouvre notre propre cadre
  9. mov eax, edi
  10. add eax, esi ; la valeur de retour va dans eax
  11. pop rbp ; épilogue : restaure le pointeur de cadre de l’appelant
  12. ret ; dépile l’adresse de retour dans rip
step 0
Loading emulator…

Regardez la pile : call a empilé 0x401003, étiqueté return address → _start+3. Continuez :

  1. push rbp sauvegarde le pointeur de cadre de l’appelant juste en dessous.
  2. mov rbp, rsp fait pointer rbp sur cette valeur sauvegardée. Désormais, l’adresse de retour est toujours en [rbp+8].
  3. mov eax, edi / add eax, esi calculent 3 + 4 dans eax.
  4. pop rbp et ret défont le cadre et reviennent en _start+3 avec eax = 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 :

EmplacementContient
[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 : rsp doit être multiple de 16 juste avant un call, d’où les fonctions qui retirent « trop » à rsp.
  • Zone rouge : une fonction qui n’appelle rien peut utiliser les 128 octets sous rsp sans le déplacer.
  • Pour les fonctions variadiques comme printf, al contient le nombre de registres vectoriels utilisés — c’est le mov eax, 0 que les compilateurs placent juste avant call 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.

Live · cdecl (32 bits) : add2(3, 4)
program— ▸ is the next instruction
  1. _start:
  2. push 4 ; arguments de droite à gauche…
  3. push 3 ; …le 1er se retrouve au sommet
  4. call add2
  5. add esp, 8 ; l’appelant retire ses 2 arguments
  6. ret
  7. add2:
  8. push ebp
  9. mov ebp, esp
  10. mov eax, DWORD PTR [ebp+8] ; 1er argument
  11. add eax, DWORD PTR [ebp+12] ; 2e argument
  12. pop ebp
  13. ret
step 0
Loading emulator…

La démo s’arrête juste après mov ebp, esp. Avec ebp = 0x7fffffcc, la pile se lit, depuis le sommet :

AdresseRelativement à ebpContient
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-64cdecl (32 bits)
Préservés par l’appelé — une fonction doit les restaurer avant de revenirrbx, rbp, r12–r15 (et rsp)ebx, esi, edi, ebp
Non préservés — n’importe quel appel peut les écraserrax, rcx, rdx, rsi, rdi, r8–r11eax, 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.

Dans ce niveau

  1. 1.1Les registres
  2. 1.2Drapeaux, cmp et sauts conditionnels
  3. 1.3Conventions d’appel (System V, cdecl)
  4. 1.4Assembleur, éditeur de liens et chargeur
  5. 1.5Directives, sections et macros