Introduction
L'Assembly x86-64 reste l'arme ultime pour les développeurs experts en 2026, quand chaque cycle CPU compte. Que ce soit pour des kernels custom, des optimisations de hotspots dans des engines de jeu, ou des malwares défensifs, maîtriser l'assembleur vous donne un contrôle total sur le hardware. Contrairement aux langages haut niveau qui masquent les détails, l'Assembly expose les registres, les flags et les instructions SIMD pour des gains de performance de 10x ou plus.
Ce tutoriel expert, structuré des bases aux avancées, fournit du code 100% fonctionnel et copier-collable en NASM pour Linux x86-64. Nous couvrons les syscalls natives (sans libc), les boucles optimisées, les fonctions modulaires, et l'allocation mémoire dynamique via mmap. À la fin, vous saurez assembler, linker et déboguer comme un pro. Idéal pour les ingénieurs système ou les reverse engineers. Préparez votre émulateur QEMU ou une VM Ubuntu 64-bit ! (142 mots)
Prérequis
- Système Linux x86-64 (Ubuntu 24.04+ ou équivalent)
- NASM installé (
sudo apt install nasm) - GNU Binutils (
sudo apt install binutils) - GDB pour debug (
sudo apt install gdb) - Connaissances avancées en C, registres x86 et appel système
- Éditeur comme VS Code avec extension NASM
Installation des outils et Hello World
#!/bin/bash
sudo apt update
sudo apt install nasm binutils gdb
cat > hello.asm << 'EOF'
section .data
msg db 'Hello, Assembly x86-64 !', 10
len equ $ - msg
section .text
global _start
_start:
mov rax, 1 ; syscall write
mov rdi, 1 ; stdout
mov rsi, msg ; pointeur message
mov rdx, len ; longueur
syscall
mov rax, 60 ; syscall exit
xor rdi, rdi ; code 0
syscall
EOF
nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
./hello
echo $?
rm hello.o hello hello.asmCe script installe NASM et binutils, puis crée un programme Hello World complet utilisant la syscall write (rax=1) et exit (rax=60) sans libc pour minimiser la taille binaire (<1KB). Les registres RSI/RDX transportent les args. Exécutez-le tel quel ; il compile, linke et runne, affichant le message et status 0. Piège : Oublier global _start empêche ld de trouver l'entrée.
Comprendre les syscalls et sections NASM
Les syscalls x86-64 Linux passent par syscall avec args en registres : RAX=numéro, RDI/RSI/RDX/R10/R8/R9 pour params. Imaginez-les comme des portes hardware : pas de stack overhead comme en C. NASM structure le code en sections (.data pour constantes, .text pour exécutable). _start est l'entrée linker par défaut, remplaçant main().
Lecture clavier et écho avec boucle
section .bss
buffer resb 256
len resq 1
section .text
global _start
_start:
; Lire input
mov rax, 0 ; syscall read
mov rdi, 0 ; stdin
mov rsi, buffer ; buffer
mov rdx, 255 ; max len
syscall
mov [len], rax ; stocker longueur réelle
; Écho
mov rax, 1 ; write
mov rdi, 1 ; stdout
mov rsi, buffer ; buffer
mov rdx, [len] ; len
syscall
; Boucle pour uppercase (exemple simple)
mov rcx, [len]
mov rsi, buffer
.loop:
cmp byte [rsi], 10 ; fin ligne?
je .done
cmp byte [rsi], 'a'
jl .next
cmp byte [rsi], 'z'
jg .next
add byte [rsi], 'A' - 'a'
.next:
inc rsi
dec rcx
jnz .loop
.done:
; Ré-écho uppercase
mov rax, 1
mov rdi, 1
mov rsi, buffer
mov rdx, [len]
syscall
mov rax, 60
xor rdi, rdi
syscallCe programme lit du stdin (syscall 0), stocke en .bss (zone non-initialisée), convertit en majuscules via boucle avec flags CF/ZF et jumps conditionnels (JE/JL/JG), puis ré-écrit. Utilise RCX/RSI pour compteur/pointeur comme un "itérateur hardware". Fonctionnel : tapez "hello" + Enter, output "HELLO". Piège : Ne pas null-terminer si strcat ; ici raw bytes.
Boucles, conditions et optimisation
- Boucles : Utilisez RCX pour count-down (DEC/JNZ rapide), flags auto-mis à jour.
- Conditions : Jcc (JE/JNE/JL/etc.) branchent sur ZF/SF/CF ; prédiction branch moderne >90% hit.
Factorielle récursive et itérative
section .data
fmt db '%ld! = %ld', 10, 0
section .bss
result resq 1
section .text
global _start
extern printf
; Fonction itérative
factorial_iter:
mov rax, 1
mov rcx, rdi
.loop:
mul rcx
dec rcx
jnz .loop
ret
; Appel (avec libc pour printf)
_start:
mov rdi, 10 ; n=10
call factorial_iter
mov [result], rax
mov rdi, fmt
mov rsi, 10
mov rdx, [result]
xor rax, rax ; float args=0
call printf
mov rax, 60
xor rdi, rdi
syscallPremière fonction : factorial_iter utilise MUL (RAX*RCX→RAX) en boucle descendante. Appel depuis _start avec libc pour printf (linkez avec nasm -f elf64 fact.asm && gcc -no-pie fact.o -o fact). Calcule 10! = 3,628,800. Récursivité évitée pour perf (stack overflow risk). Piège : MUL clobbre RDX ; utilisez toujours 64-bit.
Fonctions, calling convention et libc
Calling convention System V AMD64 : Args en RDI/RSI/RDX/RCX/R8/R9, callee sauve RBX/RBP/R12-15, caller RAX/RCX/RDX. Pour libc, liez avec gcc -nostdlib non, ici gcc pour simplicité. Fonctions = blocs réutilisables comme macros CPU.
Allocation dynamique avec mmap
section .bss
heap resq 1
section .text
global _start
_start:
; mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
mov rax, 9 ; mmap syscall
xor rdi, rdi ; addr=0
mov rsi, 4096 ; length
mov rdx, 3 ; PROT_READ|WRITE=1|2
mov r10, 0x22 ; MAP_PRIVATE|ANONYMOUS=2|32
mov r8, -1 ; fd=-1
xor r9, r9 ; offset=0
syscall
mov [heap], rax ; base pointeur
; Écrire 'ABCD' à heap
mov rdi, rax
mov word [rdi], 0x44434241 ; 'ABCD' little-endian
; Dump via write
mov rax, 1
mov rsi, [heap]
mov rdx, 4
mov rdi, 1
syscall
; munmap
mov rax, 11
mov rdi, [heap]
mov rsi, 4096
syscall
mov rax, 60
xor rdi, rdi
syscallUtilise mmap (syscall 9) pour allouer 4KB anonyme, écrit bytes directs (little-endian !), dump et munmap (11). Output 'ABCD'. Parfait pour heaps custom sans malloc. Piège : Alignement 4KB ; R10 au lieu de RCX pour 6e arg (convention syscall). Vérifiez /proc/sys/kernel/randomize_va_space=0 pour addr fixe en debug.
SIMD avec AVX pour vectorisation experte
section .data
vec1 dq 1.0, 2.0, 3.0, 4.0
vec2 dq 5.0, 6.0, 7.0, 8.0
fmt db 'Résultat AVX: %f %f %f %f', 10, 0
section .bss
result resq 4
section .text
global _start
extern printf
_start:
; Load doubles en YMM0/YMM1
mov rsi, vec1
movups ymm0, [rsi]
mov rsi, vec2
movups ymm1, [rsi]
; Addpd vectoriel
vaddpd ymm2, ymm0, ymm1
; Store
mov rsi, result
vmovupd [rsi], ymm2
; Print (simplifié, assume fpu)
mov rdi, fmt
mov rsi, [result]
mov rdx, [result+8]
mov rcx, [result+16]
mov r8, [result+24]
xor rax, rax
call printf
vzeroupper
mov rax, 60
xor rdi, rdi
syscallIntro AVX (256-bit) : VMOVUPS charge doubles, VADDPD additionne 4 floats64 en parallèle (théorique 4x speedup). Stocke et print. Liez gcc -mavx2 avx.o -o avx. Nécessite CPU AVX (Intel SandyBridge+). Piège : VZEROUPPER avant ret pour éviter AVX-SSE transition penalty (30+ cycles !).
Bonnes pratiques
- Minimisez branches : Utilisez CMOVcc au lieu de Jcc pour prédiction.
- Registre allocation : Privilégiez RAX/RDI/RSI ; sauvez callee-saved.
- Taille binaire : Évitez libc ; syscalls pures <2KB.
- Debug :
objdump -d progetgdb progavecsi(step instr). - Perf :
perf stat ./progpour cycles/instr ; optimisez IPC >2.
Erreurs courantes à éviter
- Oublier flags : JNZ sans TEST/CMP met ZF faux.
- Endianness : MOV DWORD [mem], 1 → 01 00 00 00 (little).
- 64-bit overflow : MUL sans check RDX haut.
- Syscall args : RCX→R10 pour >5 args ; pas stack.
Pour aller plus loin
- Docs officielles : Intel x86-64 Manual
- Livre : "Professional Assembly Language" par Blum
- Outils avancés : Godbolt.org pour asm de C
- Formations expertes : Découvrez nos formations Learni sur le bas niveau
- Projet : Implémentez un mini-OS bootloader.