Manipulation de la pile et mécanisme des appels de fonction en assembleur

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, rbx transfère le contenu de rbx vers rax.

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, %rax ré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 intel ou -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.

Étiquettes: x86-64 system-v-abi stack-frame calling-convention objdump

Publié le 24 octobre à 08h48