Salut
Quand on commence à toucher sérieusement au Win32, aux handles, aux threads, à la mémoire virtuelle, aux appels système, au debug, aux dumps mémoire ou au reverse, on finit presque toujours par tomber sur :
- des registres
- des adresses mémoire
- des instructions CPU
- du code désassemblé
- des valeurs hexadécimales
- des notions comme stack, calling convention, mov, cmp, jmp, call, ret
Le but ici n’est pas de faire de toi quelqu’un qui veut tout recoder en assembleur. Le but est beaucoup plus utile et beaucoup plus réaliste : comprendre l’essentiel pour mieux lire un désassemblage, mieux suivre un debugger, mieux comprendre la mémoire, les entiers, les appels de fonctions, et construire une vraie base pour le reverse, les internals Windows, et plus tard le kernel.
1) C’est quoi l’assembleur
L’assembleur est un langage de très bas niveau. Il est extrêmement proche du langage machine, c’est-à-dire des instructions réellement exécutées par le processeur.
Le CPU, lui, ne comprend pas directement :
- if
- while
- class
- std::vector
- printf
- CreateFileW au sens “haut niveau”
- déplacer une valeur
- additionner
- soustraire
- comparer
- faire un saut
- lire ou écrire en mémoire
- appeler une fonction
- retourner à l’appelant
Code: Select all
mov eax, 5
add eax, 2
cmp eax, 10
jne pas_egal
- mets 5 dans eax
- ajoute 2
- compare avec 10
- saute à pas_egal si ce n’est pas égal
2) Pourquoi apprendre l’assembleur
Aujourd’hui on écrit rarement une appli entière en assembleur. Les compilateurs C/C++ sont excellents, et heureusement. Mais comprendre l’assembleur reste extrêmement utile.
Pourquoi ?
Parce que ça permet de mieux comprendre :
- comment une variable vit réellement en mémoire
- comment une fonction reçoit ses paramètres
- comment elle renvoie une valeur
- comment le CPU représente les nombres
- pourquoi certains bugs existent
- ce que voit un debugger
- ce que produit réellement un compilateur
- le debug avancé
- le reverse engineering
- l’analyse de malware
- la performance
- la compréhension des crashs
- la lecture du code compilé
- le développement système
Code: Select all
tu n’as pas besoin d’écrire tout en assembleur
mais tu as besoin de le comprendre
Avant les registres, les instructions ou la stack, il faut revenir à la base absolue : la machine manipule des bits.
Un bit, c’est la plus petite unité d’information.
Il ne peut valoir que :
- 0
- 1
Ensuite, les bits sont regroupés pour former des données plus utiles.
Le byte
Un byte, ou octet, contient 8 bits.
Exemple :
Code: Select all
01000001
- le nombre décimal 65
- le caractère ASCII A
- un morceau d’instruction machine
- une partie d’adresse
- une donnée brute
Code: Select all
la mémoire ne “sait” pas ce qu’elle contient
C’est le programme, ou l’interprétation du programme, qui donne un sens à ces données.
4) La mémoire : une suite de bytes adressés
La mémoire peut être vue comme une grande suite de cases.
Chaque case contient un byte, et chaque case a une adresse.
Exemple simplifié :
Code: Select all
Adresse Valeur
1000 41
1001 42
1002 43
1003 44
Une donnée de plusieurs bytes occupe plusieurs cases mémoire. Par exemple :
- un entier 32 bits occupe 4 bytes
- un entier 64 bits occupe 8 bytes
- un float occupe 4 bytes
- un double occupe 8 bytes
5) Le binaire
Le binaire est un système de numération en base 2.
En décimal, on compte avec des puissances de 10.
En binaire, on compte avec des puissances de 2.
Exemple :
Code: Select all
1011b = 1*2^3 + 0*2^2 + 1*2^1 + 1*2^0
= 8 + 0 + 2 + 1
= 11
Code: Select all
11111111b = 255
- les flags
- les masques de bits
- les permissions
- les registres
- la représentation interne des valeurs
L’hexadécimal est une base 16.
On utilise les symboles :
- 0 à 9
- A à F
- A = 10
- B = 11
- C = 12
- D = 13
- E = 14
- F = 15
Code: Select all
0x2A = 2*16 + 10 = 42
Parce qu’elle se marie parfaitement avec le binaire :
Code: Select all
1 chiffre hexa = 4 bits
Code: Select all
0xA5 = 1010 0101b
- des adresses mémoire
- des dumps
- des opcodes
- des handles
- des offsets
- des flags
7) Entiers signés et non signés
Un entier peut être :
- non signé, donc uniquement positif ou nul
- signé, donc positif ou négatif
- unsigned : 0 à 255
- signed : -128 à 127
- unsigned : 0 à 4294967295
- signed : -2147483648 à 2147483647
Code: Select all
les mêmes bits peuvent représenter des valeurs différentes
selon l’interprétation signée ou non signée
Code: Select all
11111111b
- 255 si on la lit en unsigned
- -1 si on la lit en signed avec complément à deux
8) Le complément à deux
Les processeurs modernes représentent généralement les entiers signés avec le complément à deux.
C’est la représentation standard des nombres négatifs.
Méthode pour obtenir -x :
- prendre x en binaire
- inverser les bits
- ajouter 1
Code: Select all
1 = 00000001
inv = 11111110
+1 = 11111111
Code: Select all
-1 = 11111111
Code: Select all
2 = 00000010
inv = 11111101
+1 = 11111110
Code: Select all
-2 = 11111110
Parce que ça permet au CPU d’utiliser les mêmes circuits d’addition pour les nombres signés et non signés.
Exemple :
Code: Select all
5 = 00000101
-1 = 11111111
--------------
00000100
9) Overflow, carry et flags
Comme les entiers ont une taille fixe, il peut y avoir débordement.
Exemple sur 8 bits non signés :
Code: Select all
255 = 11111111
1 = 00000001
--------------
00000000
Le CPU garde alors des informations sur l’opération dans des flags. Quelques-uns très importants :
- CF : Carry Flag
- ZF : Zero Flag
- SF : Sign Flag
- OF : Overflow Flag
Par exemple, après un :
Code: Select all
cmp eax, ebx
Code: Select all
je cible
jne autre_cible
jg plus_grand
jl plus_petit
10) L’endianness
Quand une valeur occupe plusieurs bytes, il faut savoir dans quel ordre ces bytes sont stockés.
C’est là qu’intervient l’endianness.
Sur x86 et x64, l’architecture est little-endian.
Cela veut dire :
Code: Select all
le byte de poids faible est stocké en premier
Code: Select all
0x12345678
Code: Select all
78 56 34 12
- lire un dump mémoire
- comprendre un debugger
- interpréter des structures
- analyser des fichiers binaires
- faire du reverse
Code: Select all
12 34 56 78
11) Le langage machine
Le langage machine est la forme brute réellement comprise par le processeur.
Par exemple, une séquence de bytes comme :
Code: Select all
B8 01 00 00 00
Code: Select all
mov eax, 1
- le CPU exécute des bytes
- l’assembleur est une représentation texte lisible de ces bytes
12) Assembleur contre langage machine
Il ne faut pas mélanger les deux.
Langage machine :
- forme binaire brute
- directement exécutée par le CPU
- forme texte lisible
- représentation humaine du code machine
Code: Select all
mov eax, 1
ret
Ensuite un assembleur, le programme, transforme ce texte en bytes machine.
13) Les registres
Les registres sont de petites zones de stockage très rapides, directement dans le CPU.
Sur x64, on voit souvent :
- rax
- rbx
- rcx
- rdx
- rsp
- rbp
- rip
- eax
- ebx
- ecx
- edx
- esp
- ebp
- eip
- rsp / esp : stack pointer
- rbp / ebp : base ou frame pointer
- rip / eip : instruction pointer
Code: Select all
mov eax, 10
add eax, 5
Les registres sont centraux, parce qu’en assembleur presque tout passe par eux.
14) La stack
La stack, ou pile, est une zone mémoire fondamentale.
Elle sert notamment à :
- stocker des variables locales
- sauvegarder des registres
- stocker des adresses de retour
- passer certains paramètres
Instructions très classiques :
Code: Select all
push rax
pop rax
call fonction
ret
- push empile une valeur
- pop la récupère
- call appelle une fonction et empile l’adresse de retour
- ret récupère cette adresse et revient à l’appelant
- le debug
- la lecture d’un désassemblage
- la compréhension des fonctions
- les calling conventions
- certains bugs mémoire
Une fonction assembleur ressemble souvent à quelque chose comme ça :
Code: Select all
ma_fonction:
push rbp
mov rbp, rsp
; corps de la fonction
pop rbp
ret
- on sauvegarde l’ancien rbp
- on crée un nouveau cadre de pile
- on exécute le code
- on restaure l’ancien cadre
- on retourne
16) Instructions fondamentales à connaître
Voici quelques instructions de base qu’on voit partout.
mov
Copie une valeur :
Code: Select all
mov eax, 5
mov ebx, eax
Addition :
Code: Select all
add eax, 3
Soustraction :
Code: Select all
sub eax, 1
Incrémente ou décrémente :
Code: Select all
inc eax
dec ebx
Compare deux opérandes et met à jour les flags :
Code: Select all
cmp eax, ebx
Saut inconditionnel :
Code: Select all
jmp suite
Code: Select all
je egal
jne pas_egal
jg plus_grand
jl plus_petit
jge sup_ou_egal
jle inf_ou_egal
Appel et retour de fonction :
Code: Select all
call ma_fonction
ret
17) Exemple simple : un if
Pseudo-code C :
Code: Select all
if (a == 5)
b = 1;
else
b = 0;
Code: Select all
mov eax, [a]
cmp eax, 5
jne sinon
mov dword ptr [b], 1
jmp fin
sinon:
mov dword ptr [b], 0
fin:
- charger a
- le comparer à 5
- si différent, sauter à sinon
- sinon écrire 1 dans b
- puis sauter à la fin
18) Exemple simple : une boucle
Pseudo-code C :
Code: Select all
while (i < 10)
{
i++;
}
Code: Select all
debut:
cmp eax, 10
jge fin
inc eax
jmp debut
fin:
On voit bien qu’une boucle, au niveau réel, c’est surtout :
- une comparaison
- un saut conditionnel
- un saut de retour
Les flottants, comme float et double, sont plus complexes que les entiers.
Ils suivent en général une représentation de type IEEE 754.
Un float 32 bits contient essentiellement :
- un bit de signe
- un exposant
- une mantisse
- un float n’est pas stocké comme un entier
- beaucoup de valeurs décimales ne sont pas représentables exactement en binaire
- cela explique des comportements surprenants
Code: Select all
0.1 + 0.2 != 0.3
Pour du système débutant, le plus important est surtout de savoir que les flottants ont leurs propres règles, et qu’ils peuvent introduire des imprécisions.
20) Assembler et linker
Quand tu écris un fichier assembleur, il ne devient pas directement un EXE.
Il y a plusieurs étapes.
1. Code source
Exemple :
Code: Select all
moncode.asm
L’assembleur transforme le texte en code objet.
Exemple :
Code: Select all
moncode.obj
Code: Select all
moncode.o
Le linker relie :
- ton code
- les bibliothèques
- les symboles externes
- le point d’entrée
Code: Select all
monprogramme.exe
21) Exemple minimal de squelette assembleur
Voici un mini exemple purement pédagogique :
Code: Select all
segment .text
global main
main:
mov eax, 0
ret
- une section de code
- un symbole global
- une fonction d’entrée
- une valeur de retour
22) Ce qu’il faut voir dans un désassemblage
Quand tu regardes un désassemblage, tu dois progressivement apprendre à reconnaître des motifs simples.
- un if = une comparaison + un saut conditionnel
- une boucle = un label + une comparaison + un saut
- un appel = call
- un retour = ret
- une variable locale = stack ou registre
- une structure = bloc mémoire avec offsets
- un pointeur = une adresse
- un accès à un champ = lecture à un offset
Code: Select all
mov eax, [rcx+8]
- rcx pointe sur une structure ou un objet
- on lit la donnée située à l’offset 8
23) Ce qu’un dev système doit retenir en priorité
Si ton objectif est le Win32, les internals, le debug ou plus tard le kernel, voici le noyau dur à bien maîtriser.
1. Les nombres
- binaire
- hexadécimal
- signed / unsigned
- complément à deux
- adresses
- bytes
- données multi-bytes
- little-endian
- où passent les valeurs
- où transitent souvent les paramètres
- où arrivent souvent les retours
- variables locales
- adresses de retour
- prologues / épilogues
- calling conventions
- cmp
- jmp
- sauts conditionnels
- call
- ret
Code: Select all
le C/C++ finit toujours par devenir ça
24) Erreurs fréquentes des débutants
Parmi les pièges très classiques :
- confondre valeur et adresse
- oublier le little-endian
- confondre signed et unsigned
- croire que la mémoire “connait” les types
- penser que l’assembleur est magique ou mystique
- avoir la valeur 5 n’est pas la même chose qu’avoir l’adresse où 5 est stocké
- 0x12345678 ne sera pas vu comme 12 34 56 78 en mémoire sur x86/x64
- les mêmes bits peuvent être interprétés différemment selon le type
- la mémoire contient des bytes, pas des “int” au sens magique
Prenons :
Code: Select all
0x12345678
En mémoire sur x86/x64, on verra :
Code: Select all
78 56 34 12
Code: Select all
0x78 = 120
Code: Select all
0x12345678
- hexa
- bytes
- multi-byte
- endianness
- interprétation
- lecture mémoire
Quand tu commenceras à lire ou analyser :
- une structure PE
- une structure du noyau
- un appel de fonction désassemblé
- une stack trace
- un dump mémoire
- un contexte de thread
- des registres dans WinDbg ou x64dbg
Sans elles, le bas niveau ressemble à du bruit.
Avec elles, tu commences à voir la logique.
Et c’est précisément là que ça devient intéressant.
27) Résumé ultra simple
L’assembleur, c’est :
- une représentation lisible du code machine
- un langage très proche du CPU
- une clé pour comprendre mémoire, stack, registres et flux d’exécution
- bit
- byte
- binaire
- hexadécimal
- signed / unsigned
- complément à deux
- little-endian
- registres
- stack
- mov
- cmp
- jmp
- call
- ret
Mais tu dois comprendre suffisamment l’assembleur pour savoir ce que ton code devient réellement.
Conclusion
L’assembleur fait souvent peur au début, parce qu’il enlève beaucoup d’abstractions.
Mais il simplifie aussi beaucoup de choses.
Il n’y a plus :
- de classes complexes
- de templates
- de frameworks
- de grosses couches de confort
- des données
- des adresses
- des registres
- des instructions
- des sauts
Quand tu commences à comprendre ça, tu comprends beaucoup mieux :
- ce que fait réellement un compilateur
- ce que voit réellement un debugger
- ce qu’exécute réellement le CPU
Code: Select all
l’assembleur n’est pas juste un vieux langage
c’est une des meilleures portes d’entrée pour comprendre la machine
Code: Select all
Bit = 0 ou 1
Byte = 8 bits
Mémoire = suite de bytes adressés
Binaire = base 2
Hexa = base 16
Signed = avec signe
Unsigned = sans signe
-1 = souvent en complément à deux
x86/x64 = little-endian
ASM = représentation lisible du code machine
Après ce socle, les prochains gros sujets logiques sont :
- les registres en détail
- les flags CPU
- la stack en profondeur
- les calling conventions
- les modes d’adressage
- les instructions arithmétiques et logiques
- les prologues / épilogues
- le lien entre C/C++ et désassemblage Windows
