🛠️ Assembleur x86-64

Les instructions fondamentales du code généré par gcc pour notre factorielle, expliquées avec sérieux… mais pas trop.

1. Mouvement de données

movq / movl / movabsq

Rôle : copier une valeur d'un endroit à un autre (registre, mémoire, immédiat).

Le suffixe indique la taille : q = 64 bits (quad word), l = 32 bits (long).

# Notre code
movq  $1, -8(%rbp)      ; res = 1 (64-bit)
movl  $2, -12(%rbp)     ; i = 2 (32-bit)
movl  %edi, -20(%rbp)   ; stocker le paramètre n
movabsq $2432902008176640000, %rax ; constante 64-bit énorme
InstructionTailleUsage typique
movb8 bitscaractère, booléen
movw16 bitsshort, wchar
movl32 bitsint, pointeur 32-bit
movq64 bitslong long, pointeur 64-bit
movabsq64 bits immédiatconstante 64-bit non représentable dans un movq normal
🤡 mov est l'instruction préférée des développeurs C qui veulent oublier qu'ils font de l'assembleur. On copie, on copie, on copie… et à la fin on a un programme qui marche (ou pas).
cltq (alias cdqe)

Rôle : convertir un int 32-bit en long 64-bit avec extension de signe.

Opère implicitement sur %eax → %rax. cltq = Convert Long To Quad (notation AT&T).

# Notre code
movl  -12(%rbp), %eax   ; charger i (32-bit)
cltq                         ; sign-extension eax → rax
imulq %rdx, %rax        ; multiplication 64-bit
🧙 cltq c'est le coup de baguette magique qui transforme un petit entier en grand entier. Sans lui, votre multiplication 64-bit utiliserait des opérandes tronquées — et là, bonjour les bugs qui vous font pleurer un dimanche soir.
movzbl

Rôle : Move Zero-extend Byte to Long. Prend un octet en mémoire et le place dans un registre 32-bit, les bits de poids fort mis à zéro.

# Dans notre code — conversion d'un booléen (setg) en int
setg  %al              ; al = 1 si résultat précédent > 0
movzbl %al, %eax    ; eax = zéro-étendu de al → 0 ou 1
🎭 La version assembleur du « t’inquiète je gère » — on prend un petit truc tout rikiki et on le déguise en valeur 32-bit en mettant des zéros devant.

2. Opérations arithmétiques

addl / addq

Rôle : additionner deux valeurs. addl pour 32 bits, addq pour 64 bits.

# Notre code
addl  $1, -12(%rbp)     ; i++ (32-bit)
addq  %rbp, %rax       ; ajouter la base de la stack frame
➕ Si l'addition était une série Netflix, add serait la saison 1— tout le monde la comprend, personne ne la saute, et c'est la base de tout le reste.
imulq

Rôle : multiplication signée 64 bits. À ne pas confondre avec mulq (non signée).

# Notre code — le cœur de la factorielle
movq  -8(%rbp), %rdx    ; rdx = res
imulq %rdx, %rax       ; rax = i * res (signé)
movq  %rax, -8(%rbp)    ; res = rax
InstructionComportement
imulq src, dstdst *= src (deux opérandes, signé)
imulq srcrdx:rax *= src (une opérande, résultat 128-bit)
mulq srcidem mais multiplication non signée
⚠️ imulq avec une seule opérande, c'est la version « je bosse en équipe » : le résultat tient dans deux registres (rdx:rax). Pratique pour les très gros nombr— euh, les très grands entiers.
salq

Rôle : Shift Arithmetic Left — décalage de bits à gauche (équivaut à multiplier par une puissance de 2).

Dans notre code : salq $4, %rax = multiplier par 16 (taille de la structure de test).

# Notre code — accès au tableau de tests
movl  -12(%rbp), %eax   ; eax = index de boucle
cltq
salq  $4, %rax          ; rax = index * 16 (sizeof struct)
addq  %rbp, %rax
subq  $160, %rax       ; &tests[index]
🧮 Décaler à gauche de 4, c'est comme dire à votre nombre « tu veux une glace avec 4 boules ? » — il devient 16 fois plus gros. Moins cher qu'un multiplicateur.
subq

Rôle : soustraction. Essentiellement add qui a mal tourné.

# Notre code — allocation de l'espace pour les variables locales
subq  $160, %rsp        ; réserver 160 octets sur la pile
🙃 Petit rappel : sur la pile, subq agrandit l'espace (la pile descend). Dans la vraie vie, soustraire agrandit — merci de vivre dans le monde à l'envers.

3. Comparaisons et sauts

cmpq / cmpl

Rôle : comparer deux valeurs (met à jour les flags du processeur sans rien modifier d'autre).

# Notre code — diverses comparaisons
cmpl  $0, -20(%rbp)     ; cmp n, 0
cmpl  -20(%rbp), %eax   ; cmp i, n (AT&T: source, destination)
cmpq  %rax, -24(%rbp)   ; cmp res, attendu
🧐 cmp est l'arbitre de votre code : il regarde, il juge, mais il n'intervient jamais. Ce sont ses potes j* qui mettent les mains dans le cambouis.
jmp · jns · jle · jne · jl

Rôle : modifier le flot d'exécution en fonction des flags (sauvagement modifiés par cmp).

MnémoniqueSignificationAliasNotre usage
jmpJump (toujours)gotosortie anticipée de factorielle
jnsJump if Not Sign (SF=0)≥ 0if (n < 0) return -1
jleJump if Less or Equalcondition de boucle i <= n
jneJump if Not Equalif (res != attendu)
jlJump if Less<for (; i < nb_tests; )
# Notre code — extrait des sauts
jns   .L2                  ; if (n >= 0) goto L2
jmp   .L3                  ; return rax (inconditionnel)
jle   .L5                  ; if (i <= n) goto L5
jne   .L8                  ; if (res != attendu) goto FAIL
jl    .L10                 ; if (i < nb_tests) boucler
🚦 Les sauts conditionnels, c'est le GPS de votre processeur : « À la prochaine intersection, si %eax est plus petit que %ebx, tournez à gauche vers .L5. » Sauf que le GPS ne plante jamais. Vous, si.

4. Appels de fonction et pile

pushq / popq

Rôle : empiler / dépiler une valeur sur la pile.

pushq décrémente %rsp puis écrit. popq lit puis incrémente.

# Notre code — prologue / épilogue
pushq %rbp               ; sauver l'ancienne base
popq  %rbp               ; restaurer

Variation : leave

Équivaut à movq %rbp, %rsp; popq %rbp — une façon compacte de quitter une fonction.

leave                      ; restaure rsp et rbp en une seule instr.
🥞 La pile c'est comme une pile de crêpes : vous mettez quelque chose dessus (push), vous le retirez (pop), et si vous faites n'importe quoi, tout s'effondre. Contrairement aux crêpes, ça ne se mange pas.
call / ret

Rôle : call pousse l'adresse de retour et saute à la fonction ; ret dépile l'adresse et y retourne.

# Notre code — appel à notre fonction et à printf
call  factorielle          ; res = factorielle(n)
call  printf               ; affichage
...
ret                       ; return
📞 call c'est « je t'appelle, tu me diras quoi ». ret c'est « je te rappelle ». Si vous oubliez un ret, votre programme part explorer des adresses inconnues. C'est ce qu'on appelle un aller simple pour Segfault-les-Bains.
subq $N, %rsp (allocation de la frame)

Rôle : réserver de l'espace pour les variables locales en descendant le sommet de pile.

# Notre code — 160 octets pour les locals de main()
subq  $160, %rsp
📦 Quand le compilateur a besoin de place pour poser ses variables, il pousse le mur du fond. C'est un peu comme faire de la place dans un frigo : on descend tout d'un cran. La bière au frais reste accessible.

5. La méta : directives et autres bricoles

DirectiveRôle
.globl factoriellerend le symbole visible à l'éditeur de liens (comme extern)
.type factorielle, @functiondéclare que c'est une fonction (utile pour le débogueur)
.cfi_startproc / .cfi_endprocinformations de déroulement de pile (nécessaires pour les exceptions et le débogage)
.cfi_def_cfa_offsetdéfinit où se trouve le CFA (Canonical Frame Address) par rapport à %rsp
.section .rodatasection en lecture seule (constantes, chaînes de caractères)
.stringchaîne de caractères terminée par \0
.align 8alignement sur 8 octets (le processeur aime les données alignées)
.size factorielle, .-factorielletaille du symbole (calculée par différence d'adresses)
.identlaisse une trace du compilateur dans le binaire (vanité pure)
📜 Les directives .cfi_* sont le formulaire administratif de l'assembleur : personne ne sait exactement à quoi ça sert, mais si tu ne les mets pas, le débugueur râle, les exceptions C++ pleurent, et ta grand-mère a honte.

6. Anatomie d'une fonction : factorielle

Voyons comment tout s'assemble dans notre fonction star :

; int factorielle(int n)  →  %edi contient n
pushq  %rbp              ; prologue : sauver rbp
movq   %rsp, %rbp       ; rbp = rsp (nouvelle frame)
movl   %edi, -20(%rbp)  ; n est sur la pile
cmpl   $0, -20(%rbp)   ; if (n < 0)
jns    .L2               ; >= 0 : on continue
movq   $-1, %rax       ; return -1
jmp    .L3               ; sauter la fin

.L2:
movq   $1, -8(%rbp)    ; res = 1
movl   $2, -12(%rbp)   ; i = 2
jmp    .L4               ; aller à la condition

.L5:                           ; corps de la boucle
movl   -12(%rbp), %eax ; eax = i
cltq
movq   -8(%rbp), %rdx  ; rdx = res
imulq  %rdx, %rax      ; rax = i * res
movq   %rax, -8(%rbp)  ; res = rax
addl   $1, -12(%rbp)   ; i++

.L4:
movl   -12(%rbp), %eax ; eax = i
cmpl   -20(%rbp), %eax ; cmp n, i  (AT&T!)
jle    .L5               ; if (i <= n) boucler

movq   -8(%rbp), %rax  ; return res

.L3:
popq   %rbp             ; épilogue
ret
🏗️ C'est tout ! Prologue, test, boucle, épilogue. C'est comme une pièce de théâtre en 4 actes, sauf que le public applaudit avec des ret. Et il n'y a pas d'entracte.

7. Variations et pistes d'exploration

VarianteCommandeCe qu'on découvre
Syntaxe Intelobjdump -d -M intelmov rax, QWORD PTR [rbp-8] au lieu de movq -8(%rbp), %rax
Avec les sourcesobjdump -Sassembleur et C entrelacés — magique pour le débogage
Désassemblage completobjdump -d -M intel | lessvoir tout le binaire, pas seulement les 120 premières lignes
Section par sectionobjdump -xen-têtes, sections, table des symboles
Dynamique / statiqueldd factoriellevoir les bibliothèques liées
R.O.P. gadgetROPgadget --binary factorielleparce que c'est drôle de voir ce que votre compilateur laisse traîner
Analyse statiquestrace ./factorielletous les appels système : le programme raconte sa vie
🔬 Les outils ci-dessus, c'est un peu le scalpel, le microscope et le détecteur à métaux du développeur. Avec objdump -S, vous devenez le médecin légiste de votre propre code. Autopsie comprise.

8. Convention d'appel (System V AMD64) en 30 secondes

ParamètreRegistre
1er arg (int n)%edi
2e arg%esi
3e arg%edx
4e arg%ecx
5e arg%r8
6e arg%r9
Valeur de retour%rax
Registre de pile%rsp
Frame pointer%rbp (optionnel avec -fomit-frame-pointer)

Les registres %rax, %rcx, %rdx, %rsi, %rdi, %r8%r11 sont caller-saved (la fonction appelée peut les écraser). Les autres sont callee-saved.

📖 La convention d'appel, c'est le règlement intérieur de l'immeuble : « Les poubelles (%rax) se sortent le mardi, le salon (%rbp) se remet en ordre avant de partir, et les pièces rapportées (%r8%r11) peuvent être mises à l'envers par les visiteurs. »

9. Pour aller plus loin

Modifiez factorielle.c et observez comment le code assembleur change : c'est le meilleur moyen d'apprendre.

🧠 Le dernier conseil : si vous bidouillez le source et que l'assembleur devient plus long, c'est mauvais signe. Si GCC produit du code plus court que ce que vous écririez à la main… peut-être que GCC a raison. (Ou pas.)