En programmation bas niveau, la gestion de la pile et le protocole d’appel de fonction sont des concepts fondamentaux qui régissent l’exécution des sous-programmes. Ce texte explore ces mécanismes dans le contexte de l’architecture x86-64, avec un accent sur les conventions d’appel, l’alignement de la pile, et la présevration des états entre les appels.
Styles d’écriture en assembleur
Les instructions assembleur peuvent être exprimées selon plusieurs conventions syntaxiques, chacune associée à des environnements ou outils spécifiques.
Style Intel
- L’ordre des opérandes suit la forme
destination, source. - Les registres (ex.
rax,rbp) s’écrivent sans préfixe ; les valeurs immédiates n’utilisent pas de symbole particulier. - Exemple :
mov rax, rbxtransfère le contenu derbxversrax.
Style AT&T
- L’ordre est inversé :
source, destination. - Les registres sont préfixés par
%, les constantes par$, et les accès mémoire utilisent des parenthèses. - Exemple :
movq %rbx, %raxréalise la même opération que ci-dessus.
Style NASM
- Syntaxe proche du style Intel, mais avec une interprétation stricte des tailles d’opérandes.
- Le suffixe (
byte,dword,qword) peut être requis pour lever l’ambiguïté. - Exemple :
mov eax, dword 42.
Outils d’analyse : objdump
Pour inspecter le code binaire généré, objdump permet une désassemblage contrôlé :
-d: désassemble la section de code exécutable.-M intelou-M att: force le format de sortie.
Commande typique :
objdump -d -M intel ./program > disasm.s
Structure d’un appel de fonction
Dans la convention System V ABI (utilisée sur Linux x86-64), les six premiers arguments entiers ou pointeurs sont transmis via les registres rdi, rsi, rdx, rcx, r8 et r9. Les suivants sont placés sur la pile, en ordre inverse d’appel.
Entrée dans une fonction (prologue)
Chaque fonction commence par un prologue standard :
push rbp ; sauvegarde le cadre précédent
mov rbp, rsp ; établit un nouveau cadre de pile
sub rsp, 32 ; réserve de l’espace pour les variables locales (alignement requis)
Cette séquence crée un *stack frame* isolé, permettant une gestion locale des données sans interférer avec l’appelant.
Gestion des paramètres
Voici comment une fonction peut stocker les paramètres reçus :
mov DWORD PTR [rbp-4], edi ; premier argument → emplacement local
mov DWORD PTR [rbp-8], esi ; second argument
mov DWORD PTR [rbp-12], edx ; troisième argument
mov DWORD PTR [rbp-16], ecx ; quatrième argument
mov DWORD PTR [rbp-20], r8d ; cinquième argument
mov DWORD PTR [rbp-24], r9d ; sixième argument
Cette copie explicite garantit que les valeurs restent accessibles même si les registres sont réutilisés plus tard dans la fonction.
Appel depuis main
La fonction appelante (main) prépare les arguments comme suit :
mov edi, 1 ; arg1
mov esi, 2 ; arg2
mov edx, 3 ; arg3
mov ecx, 4 ; arg4
mov r8d, 5 ; arg5
mov r9d, 6 ; arg6
push 7 ; arg7 → pile
push 8 ; arg8 → pile
call calculate_sum
Après l’appel, la pile contient deux valeurs supplémentaires, nécessitant un nettoyage explicite.
Sortie d’une fonction (épilogue)
L’épilogue restaure l’état précédent :
mov rsp, rbp ; détruit le cadre local
pop rbp ; restaure le cadre appelant
ret
Pour les fonctions appelantes qui ont poussé des arguments, un ajustement post-appel est requis :
add rsp, 16 ; libère les 2 arguments de 8 octets chacun
Alignement de la pile
L’ABI exige que la pile soit alignée sur une frontière de 16 octets avant tout appel de fonction. Cette contrainte permet l’utilisation efficace d’instructions SIMD et garantit la compatibilité avec les bibliothèques système.
Par exemple, après avoir poussé deux valeurs de 8 octets, le pointeur rsp diminue de 16 octets — ce qui maintient l’alignement si sa valeur initiale était déjà multiple de 16.
Visualisation du cadre de pile
Lors de l’appel de calculate_sum depuis main, la pile se structure ainsi (croissante vers le bas) :
[rbp + 24] → huitième argument (8)
[rbp + 16] → septième argument (7)
[rbp + 8] → adresse de retour
[rbp] → ancien rbp (sauvegardé)
[rbp - 4] → premier argument (copié)
[rbp - 8] → second argument (copié)
...
[rbp - 24] → sixième argument (copié)
Cette organisation permet un accès direct aux paramètres via des décalages constants relativement à rbp.