Assembly x86-64 - Notes et Corrigés
Les préfixes REP
| Préfixe |
Signification |
Condition |
rep |
REPeat |
Répète %rcx fois |
repe |
REPeat while Equal |
Répète tant que %rcx != 0 ET ZF = 1 |
repz |
REPeat while Zero |
Identique à repe |
repne |
REPeat while Not Equal |
Répète tant que %rcx != 0 ET ZF = 0 |
repnz |
REPeat while Not Zero |
Identique à repne |
Quand utiliser quoi ?
rep : opérations inconditionnelles (memset, memcpy)
repe/repz : comparaisons qui s'arrêtent sur différence (memcmp, strcmp)
repne/repnz : recherche qui s'arrête quand trouvé (strlen, strchr)
Instructions String (STOS, LODS, MOVS, SCAS, CMPS)
Vue d'ensemble
STOS : AL ──────→ (RDI) "STOre to String"
LODS : (RSI) ──→ AL "LOaD from String"
MOVS : (RSI) ──→ (RDI) "MOVe String"
SCAS : AL vs (RDI) → flags "SCAn String"
CMPS : (RSI) vs (RDI) → flags "CoMPare Strings"
Suffixes (taille)
| Suffixe |
Taille |
Registre utilisé |
Incrément pointeur |
b |
1 octet |
%al |
+1 |
w |
2 octets |
%ax |
+2 |
d |
4 octets |
%eax |
+4 |
q |
8 octets |
%rax |
+8 |
Mnémotechnique registres
%rsi = Source Index (d'où on lit)
%rdi = Destination Index (où on écrit)
%rcx = Counter (compteur pour rep)
%rax = Accumulateur (valeur à stocker/charger)
STOSB en détail (STOre String Byte)
Action : Écrit %al à l'adresse (%rdi), puis incrémente %rdi de 1.
Avant: %al = 'X', %rdi = 0x1000
stosb
Après: mémoire[0x1000] = 'X', %rdi = 0x1001
Équivalent C :
*rdi = al;
rdi++;
Exemple : memset (remplir un tableau)
memset_rep:
mov %rdi, %r8
mov %rdx, %rcx
mov %sil, %al
rep stosb
mov %r8, %rax
ret
Trace pour memset(buf, 'A', 3) :
Début: %rdi=buf, %rcx=3, %al='A'
rep stosb:
- stosb: buf[0]='A', rdi++, rcx=2
- stosb: buf[1]='A', rdi++, rcx=1
- stosb: buf[2]='A', rdi++, rcx=0
- rcx=0, stop
LODSB en détail (LOaD String Byte)
Action : Lit 1 byte depuis (%rsi) vers %al, puis incrémente %rsi de 1.
Avant: mémoire[0x1000] = 'H', %rsi = 0x1000
lodsb
Après: %al = 'H', %rsi = 0x1001
Équivalent C :
al = *rsi;
rsi++;
Exemple : parcourir une chaîne
print_chars:
mov %rdi, %rsi
.loop:
lodsb
test %al, %al
jz .done
call print_char
jmp .loop
.done:
ret
MOVSB en détail (MOVe String Byte)
Action : Copie 1 byte de (%rsi) vers (%rdi), puis incrémente les deux.
Avant: mémoire[0x1000] = 'X', %rsi = 0x1000, %rdi = 0x2000
movsb
Après: mémoire[0x2000] = 'X', %rsi = 0x1001, %rdi = 0x2001
Équivalent C :
*rdi = *rsi;
rsi++;
rdi++;
Exemple : memcpy
memcpy_rep:
mov %rdi, %rax
mov %rdx, %rcx
rep movsb
ret
Trace pour memcpy(dest, src, 3) :
Début: %rsi=src, %rdi=dest, %rcx=3
rep movsb:
- movsb: dest[0]=src[0], rsi++, rdi++, rcx=2
- movsb: dest[1]=src[1], rsi++, rdi++, rcx=1
- movsb: dest[2]=src[2], rsi++, rdi++, rcx=0
- rcx=0, stop
SCASB en détail (SCAn String Byte)
Action : Compare %al avec (%rdi), met à jour les flags, puis incrémente %rdi.
Avant: %al = 'X', mémoire[0x1000] = 'X', %rdi = 0x1000
scasb
Après: ZF = 1 (égaux), %rdi = 0x1001
Équivalent C :
flags = compare(al, *rdi);
rdi++;
Exemple : strlen avec repne scasb
strlen_rep:
mov %rdi, %rsi
xor %al, %al
mov $-1, %rcx
repne scasb
not %rcx
dec %rcx
mov %rcx, %rax
ret
Explication strlen :
%al = 0 (on cherche le '\0')
%rcx = -1 (0xFFFFFFFFFFFFFFFF, quasi-infini)
repne scasb : répète tant que *rdi != al ET rcx != 0
- Quand on trouve '\0', ZF=1, la boucle s'arrête
not %rcx : inverse les bits (rcx était décrémenté à chaque itération)
dec %rcx : enlève 1 (le '\0' ne compte pas)
CMPSB en détail (CoMPare String Byte)
Action : Compare (%rsi) avec (%rdi), met à jour les flags, incrémente les deux.
Avant: src[0]='A', dest[0]='A', %rsi=src, %rdi=dest
cmpsb
Après: ZF = 1 (égaux), %rsi++, %rdi++
Équivalent C :
flags = compare(*rsi, *rdi);
rsi++;
rdi++;
Exemple : memcmp
memcmp_rep:
mov %rdx, %rcx
repe cmpsb
jne .not_equal
xor %eax, %eax
ret
.not_equal:
movzbl -1(%rsi), %eax
movzbl -1(%rdi), %ecx
sub %ecx, %eax
ret
Explication memcmp :
repe cmpsb : répète tant que *rsi == *rdi ET rcx != 0
- Si différence trouvée → ZF=0, sort de la boucle
-1(%rsi) car rsi a été incrémenté après la comparaison
Récapitulatif avec REP
| Combo |
Usage |
S'arrête quand |
rep stosb |
memset |
rcx = 0 |
rep movsb |
memcpy |
rcx = 0 |
rep lodsb |
rare |
rcx = 0 |
repe cmpsb |
memcmp |
rcx = 0 OU différence |
repe scasb |
rare |
rcx = 0 OU différence |
repne scasb |
strlen/strchr |
rcx = 0 OU trouvé |
repne cmpsb |
rare |
rcx = 0 OU égalité |
Syscalls Linux x86-64
| Syscall |
Numéro |
rdi |
rsi |
rdx |
| read |
0 |
fd |
buf |
count |
| write |
1 |
fd |
buf |
count |
| open |
2 |
path |
flags |
mode |
| close |
3 |
fd |
- |
- |
| lseek |
8 |
fd |
offset |
whence |
| exit |
60 |
code |
- |
- |
lseek whence :
SEEK_SET = 0 : depuis le début
SEEK_CUR = 1 : depuis position actuelle
SEEK_END = 2 : depuis la fin
_start et argv :
(%rsp) = argc
8(%rsp) = argv[0]
16(%rsp) = argv[1]
...
Corrigés d'exercices
plusminus
Prototype : int64_t plusminus(int64_t a, int64_t b, int64_t c)
Retourne : a + b - c
.text
.global plusminus
plusminus:
movq %rdi, %rax # rax = a (1er argument)
addq %rsi, %rax # rax = a + b (2ème argument)
subq %rdx, %rax # rax = a + b - c (3ème argument)
ret # retourne rax
Ligne par ligne :
| Ligne | Instruction | Explication |
|-------|-------------|-------------|
| 1 | movq %rdi, %rax | Copie a dans le registre de retour |
| 2 | addq %rsi, %rax | Ajoute b au résultat |
| 3 | subq %rdx, %rax | Soustrait c du résultat |
| 4 | ret | Retourne (résultat dans %rax) |
memset_rep
Prototype : char *memset_rep(char *array, char value, size_t size)
Retourne : Le pointeur array original
.text
.global memset_rep
memset_rep:
mov %rdi, %r8 # sauvegarde le pointeur original dans r8
mov %rdx, %rcx # rcx = size (compteur pour rep)
mov %sil, %al # al = value (byte à écrire)
rep stosb # répète: écrit al à (rdi), rdi++, rcx--
mov %r8, %rax # retourne le pointeur original
ret
Ligne par ligne :
| Ligne | Instruction | Explication |
|-------|-------------|-------------|
| 1 | mov %rdi, %r8 | Sauvegarde array car rep stosb va modifier %rdi |
| 2 | mov %rdx, %rcx | Met size dans %rcx (compteur pour rep) |
| 3 | mov %sil, %al | Met value dans %al (stosb utilise %al) |
| 4 | rep stosb | Écrit %al à (%rdi) size fois, incrémente %rdi à chaque fois |
| 5 | mov %r8, %rax | Met le pointeur original sauvegardé dans %rax pour le retour |
| 6 | ret | Retourne |
Pourquoi %r8 ?
%rdi est modifié par rep stosb
%rax serait corrompu par mov %sil, %al (écrase le byte bas)
%r8 est un registre volatile libre qu'on peut utiliser comme scratch
endian_sum
Prototype : int32_t endian_sum(int32_t *arr, size_t n)
Retourne : La somme des éléments avec swap d'endianness
.text
.global endian_sum
endian_sum:
xor %rax, %rax # sum = 0
.loop:
cmp $0, %rsi # compare n avec 0
je .end # si n == 0, fin
dec %rsi # n--
movl (%rdi), %edx # edx = arr[i] (charge 4 bytes)
bswap %edx # inverse les bytes (endianness swap)
addl %edx, %eax # sum += edx
add $4, %rdi # avance le pointeur de 4 bytes (sizeof int32_t)
jmp .loop # continue la boucle
.end:
ret # retourne sum (dans eax/rax)
Ligne par ligne :
| Ligne | Instruction | Explication |
|-------|-------------|-------------|
| 1 | xor %rax, %rax | Initialise la somme à 0 |
| 2 | cmp $0, %rsi | Compare le compteur n avec 0 |
| 3 | je .end | Si n == 0, sort de la boucle |
| 4 | dec %rsi | Décrémente le compteur |
| 5 | movl (%rdi), %edx | Charge l'entier 32-bit pointé par %rdi |
| 6 | bswap %edx | Inverse l'ordre des 4 bytes (little↔big endian) |
| 7 | addl %edx, %eax | Ajoute la valeur swappée à la somme |
| 8 | add $4, %rdi | Avance le pointeur au prochain int32_t |
| 9 | jmp .loop | Retourne au début de la boucle |
| 10 | ret | Retourne la somme |
Note sur bswap :
Avant: edx = 0x12345678
bswap %edx
Après: edx = 0x78563412
my_atoui
Prototype : uint64_t my_atoui(const char *str)
Retourne : La valeur numérique de la chaîne
.text
.global my_atoui
my_atoui:
xor %rax, %rax # result = 0
.loop:
xor %ecx, %ecx # clear ecx (pour zero-extend)
movb (%rdi), %cl # cl = *str (charge 1 caractère)
sub $'0', %cl # cl = cl - '0' (convertit ASCII en digit)
cmp $9, %cl # compare avec 9
ja .end # si > 9 (unsigned), pas un chiffre, fin
imulq $10, %rax # result *= 10
add %rcx, %rax # result += digit
inc %rdi # str++ (caractère suivant)
jmp .loop # continue
.end:
ret # retourne result
Ligne par ligne :
| Ligne | Instruction | Explication |
|-------|-------------|-------------|
| 1 | xor %rax, %rax | result = 0 |
| 2 | xor %ecx, %ecx | Met %rcx à 0 (zero-extend implicite) |
| 3 | movb (%rdi), %cl | Charge le caractère courant dans %cl |
| 4 | sub $'0', %cl | Convertit ASCII '0'-'9' en valeur 0-9 |
| 5 | cmp $9, %cl | Compare avec 9 |
| 6 | ja .end | Si > 9 (unsigned), ce n'est pas un chiffre |
| 7 | imulq $10, %rax | Multiplie le résultat par 10 |
| 8 | add %rcx, %rax | Ajoute le nouveau chiffre |
| 9 | inc %rdi | Passe au caractère suivant |
| 10 | jmp .loop | Continue la boucle |
| 11 | ret | Retourne le résultat |
Astuce ja (Jump if Above) :
- Si le caractère < '0' : après
sub, cl devient négatif = très grand en unsigned
- Si le caractère > '9' : après
sub, cl > 9
- Dans les deux cas,
ja (comparaison unsigned) saute → fin
fibo_loop
Prototype : uint32_t fibo_loop(uint32_t n)
Retourne : Le n-ième nombre de Fibonacci (F0=0, F1=1, F2=1, F3=2...)
.text
.globl fibo_loop
fibo_loop:
cmp $0, %edi # compare n avec 0
je .ret_zero # si n == 0, retourne 0
cmp $1, %edi # compare n avec 1
je .ret_one # si n == 1, retourne 1
mov $0, %eax # a = F(0) = 0
mov $1, %ecx # b = F(1) = 1
mov $2, %edx # i = 2 (on commence à F(2))
.loop:
mov %ecx, %esi # temp = b
add %eax, %ecx # b = a + b (nouveau Fibonacci)
mov %esi, %eax # a = temp (ancien b)
inc %edx # i++
cmp %edi, %edx # compare i avec n
jbe .loop # si i <= n, continue
mov %ecx, %eax # retourne b (le dernier Fibonacci calculé)
ret
.ret_zero:
mov $0, %eax # retourne 0
ret
.ret_one:
mov $1, %eax # retourne 1
ret
Ligne par ligne :
| Ligne | Instruction | Explication |
|-------|-------------|-------------|
| 1-2 | cmp $0, %edi / je .ret_zero | Cas spécial : F(0) = 0 |
| 3-4 | cmp $1, %edi / je .ret_one | Cas spécial : F(1) = 1 |
| 5 | mov $0, %eax | a = 0 (F(i-2)) |
| 6 | mov $1, %ecx | b = 1 (F(i-1)) |
| 7 | mov $2, %edx | i = 2 (compteur) |
| 8 | mov %ecx, %esi | Sauvegarde b dans temp |
| 9 | add %eax, %ecx | b = a + b (calcule F(i)) |
| 10 | mov %esi, %eax | a = temp (ancien b devient nouveau a) |
| 11 | inc %edx | i++ |
| 12 | cmp %edi, %edx | Compare i avec n |
| 13 | jbe .loop | Si i <= n, continue |
| 14 | mov %ecx, %eax | Met le résultat dans %eax pour le retour |
Trace pour n=5 (attendu: 5) :
Début: a=0, b=1, i=2
i=2: temp=1, b=0+1=1, a=1, i=3 → F(2)=1
i=3: temp=1, b=1+1=2, a=1, i=4 → F(3)=2
i=4: temp=2, b=1+2=3, a=2, i=5 → F(4)=3
i=5: temp=3, b=2+3=5, a=3, i=6 → F(5)=5
i=6 > n=5, exit, return b=5 ✓
facto_rec
Prototype : uint64_t facto_rec(uint8_t n)
Retourne : n! (factorielle de n)
.text
.globl facto_rec
facto_rec:
movzbq %dil, %rdi # zero-extend: rdi = (uint64_t)n
cmp $0, %dil # compare n avec 0
je .base_case # si n == 0, retourne 1
push %rdi # sauvegarde n sur la pile
dec %rdi # rdi = n - 1
call facto_rec # appel récursif: rax = facto_rec(n-1)
pop %rdi # récupère n depuis la pile
imulq %rdi, %rax # rax = n * facto_rec(n-1)
ret
.base_case:
mov $1, %rax # 0! = 1
ret
Ligne par ligne :
| Ligne | Instruction | Explication |
|-------|-------------|-------------|
| 1 | movzbq %dil, %rdi | Zero-extend le byte n en 64-bit |
| 2 | cmp $0, %dil | Compare n avec 0 |
| 3 | je .base_case | Si n == 0, va au cas de base |
| 4 | push %rdi | Sauvegarde n (sera écrasé par l'appel récursif) |
| 5 | dec %rdi | n = n - 1 pour l'appel récursif |
| 6 | call facto_rec | Appel récursif → résultat dans %rax |
| 7 | pop %rdi | Récupère notre n sauvegardé |
| 8 | imulq %rdi, %rax | rax = n * facto_rec(n-1) |
| 9 | ret | Retourne le résultat |
| 10-11 | .base_case | 0! = 1 |
Pourquoi push %rdi / pop %rdi ?
%rdi est un registre caller-saved (volatile)
- Après
call facto_rec, %rdi peut avoir changé
- On doit sauvegarder
n pour le multiplier après
Trace pour n=4 (attendu: 24) :
facto_rec(4):
push 4
call facto_rec(3):
push 3
call facto_rec(2):
push 2
call facto_rec(1):
push 1
call facto_rec(0):
return 1
pop 1, rax = 1 * 1 = 1
pop 2, rax = 2 * 1 = 2
pop 3, rax = 3 * 2 = 6
pop 4, rax = 4 * 6 = 24
return 24 ✓
print_fibo
Prototype : void print_fibo(uint32_t n)
Affiche : fibo(n): F(n)\n
.section .rodata
format: .string "fibo(%u): %u\n"
.text
.global print_fibo
print_fibo:
push %rbx # sauvegarde rbx (callee-saved)
push %r12 # sauvegarde r12 (callee-saved)
mov %edi, %r12d # r12 = n (sauvegarde pour printf)
xor %eax, %eax # a = 0 (F(0))
mov $1, %ebx # b = 1 (F(1))
test %edi, %edi # teste si n == 0
jz .print # si n == 0, résultat = 0 (déjà dans eax)
dec %edi # n--
jz .use_b # si n était 1, résultat = b = 1
.loop:
mov %ebx, %ecx # temp = b
add %eax, %ebx # b = a + b
mov %ecx, %eax # a = temp
dec %edi # n--
jnz .loop # continue tant que n > 0
.use_b:
mov %ebx, %eax # résultat = b
.print:
mov %eax, %edx # 3ème arg printf = fibo(n)
mov %r12d, %esi # 2ème arg printf = n original
lea format(%rip), %rdi # 1er arg printf = format string
xor %eax, %eax # al = 0 (pas d'args vectoriels)
call printf@PLT # appelle printf
pop %r12 # restaure r12
pop %rbx # restaure rbx
ret
Ligne par ligne :
| Ligne | Instruction | Explication |
|-------|-------------|-------------|
| 1-2 | push %rbx/r12 | Sauvegarde les registres callee-saved qu'on utilise |
| 3 | mov %edi, %r12d | Garde n original pour l'afficher après |
| 4-5 | xor/mov | Initialise a=0, b=1 |
| 6-7 | test/jz | Si n==0, saute directement à print (résultat=0) |
| 8-9 | dec/jz | Si n==1, saute à .use_b (résultat=1) |
| 10-14 | .loop | Calcule Fibonacci itérativement |
| 15 | mov %ebx, %eax | Met le résultat dans %eax |
| 16-19 | .print | Prépare les arguments pour printf |
| 20 | call printf@PLT | Appelle printf (PLT pour le linking dynamique) |
| 21-22 | pop | Restaure les registres sauvegardés |
Pourquoi lea format(%rip), %rdi ?
- C'est du Position Independent Code (PIC)
%rip = adresse de l'instruction courante
- Nécessaire pour les shared libraries et ASLR
Pièges courants
Utiliser %edi au lieu de %rdi pour les pointeurs
- Les pointeurs sont 64-bit,
%edi tronque à 32-bit
Oublier que mov %sil, %al écrase le byte bas de %rax
- Si
%rax contenait un pointeur, il est corrompu
cmpb avec un registre 64-bit
cmpb $0, %rsi est invalide, utiliser test %rsi, %rsi
Mauvais incrément dans les boucles
int32_t = 4 octets → add $4, %rdi
int64_t = 8 octets → add $8, %rdi
Alignement de la pile avant call
- RSP doit être aligné à 16 octets - 8 avant le
call
- Compter les
push : nombre impair = OK, pair = ajouter sub $8, %rsp
Oublier de sauvegarder les registres callee-saved
%rbx, %rbp, %r12-%r15 doivent être préservés
- Si tu les utilises,
push au début et pop à la fin
Registres écrasés par un call
%rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11 peuvent changer
- Sauvegarde-les si tu en as besoin après le
call