Les instructions fondamentales du code généré par gcc pour notre factorielle,
expliquées avec sérieux… mais pas trop.
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
| Instruction | Taille | Usage typique |
|---|---|---|
movb | 8 bits | caractère, booléen |
movw | 16 bits | short, wchar |
movl | 32 bits | int, pointeur 32-bit |
movq | 64 bits | long long, pointeur 64-bit |
movabsq | 64 bits immédiat | constante 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).
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.
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
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
add serait la saison 1—
tout le monde la comprend, personne ne la saute, et c'est la base de tout le reste.
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
| Instruction | Comportement |
|---|---|
imulq src, dst | dst *= src (deux opérandes, signé) |
imulq src | rdx:rax *= src (une opérande, résultat 128-bit) |
mulq src | idem 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.
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]
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
subq agrandit l'espace (la pile descend).
Dans la vraie vie, soustraire agrandit — merci de vivre dans le monde à l'envers.
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.
Rôle : modifier le flot d'exécution en fonction des flags (sauvagement modifiés par cmp).
| Mnémonique | Signification | Alias | Notre usage |
|---|---|---|---|
jmp | Jump (toujours) | goto | sortie anticipée de factorielle |
jns | Jump if Not Sign (SF=0) | ≥ 0 | if (n < 0) return -1 |
jle | Jump if Less or Equal | ≤ | condition de boucle i <= n |
jne | Jump if Not Equal | ≠ | if (res != attendu) |
jl | Jump 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
%eax est plus petit que %ebx,
tournez à gauche vers .L5. » Sauf que le GPS ne plante jamais.
Vous, si.
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
leaveÉquivaut à movq %rbp, %rsp; popq %rbp — une façon compacte de quitter une fonction.
leave ; restaure rsp et rbp en une seule instr.
push),
vous le retirez (pop), et si vous faites n'importe quoi, tout s'effondre.
Contrairement aux crêpes, ça ne se mange pas.
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.
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
| Directive | Rôle |
|---|---|
.globl factorielle | rend le symbole visible à l'éditeur de liens (comme extern) |
.type factorielle, @function | déclare que c'est une fonction (utile pour le débogueur) |
.cfi_startproc / .cfi_endproc | informations de déroulement de pile (nécessaires pour les exceptions et le débogage) |
.cfi_def_cfa_offset | définit où se trouve le CFA (Canonical Frame Address) par rapport à %rsp |
.section .rodata | section en lecture seule (constantes, chaînes de caractères) |
.string | chaîne de caractères terminée par \0 |
.align 8 | alignement sur 8 octets (le processeur aime les données alignées) |
.size factorielle, .-factorielle | taille du symbole (calculée par différence d'adresses) |
.ident | laisse une trace du compilateur dans le binaire (vanité pure) |
.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.
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
ret.
Et il n'y a pas d'entracte.
| Variante | Commande | Ce qu'on découvre |
|---|---|---|
| Syntaxe Intel | objdump -d -M intel | mov rax, QWORD PTR [rbp-8] au lieu de movq -8(%rbp), %rax |
| Avec les sources | objdump -S | assembleur et C entrelacés — magique pour le débogage |
| Désassemblage complet | objdump -d -M intel | less | voir tout le binaire, pas seulement les 120 premières lignes |
| Section par section | objdump -x | en-têtes, sections, table des symboles |
| Dynamique / statique | ldd factorielle | voir les bibliothèques liées |
| R.O.P. gadget | ROPgadget --binary factorielle | parce que c'est drôle de voir ce que votre compilateur laisse traîner |
| Analyse statique | strace ./factorielle | tous les appels système : le programme raconte sa vie |
objdump -S, vous devenez le médecin légiste
de votre propre code. Autopsie comprise.
| Paramètre | Registre |
|---|---|
| 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.
%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. »
make perf — compare les versions optimisées (-O0 à -Os)make debug — lance gdb avec breakpoint sur factoriellemake hex — regarder le binaire en hexadécimal (vous verrez les chaînes "OK" et "FAIL" traîner)make asm2exe — assembler puis exécuter le .s : vérifier que ça donne le même résultatModifiez factorielle.c et observez comment le code assembleur change : c'est le meilleur moyen d'apprendre.