# Les bases de l’assembleur
Salut aujourd’hui
**les bases de l’assembleur**.
Quand on commence le Win32, les handles, la mémoire virtuelle, les threads ou les appels système, on finit tôt ou tard par tomber sur :
* des registres
* des adresses mémoire
* des instructions CPU
* du code désassemblé dans un debugger
* des valeurs en hexadécimal
* des notions comme `stack`, `calling convention`, `mov`, `cmp`, `jmp`, etc.
Beaucoup de développeurs utilisent le C ou le C++ **sans vraiment voir ce qui se passe sous le capot**.
L’assembleur, lui, nous rapproche du fonctionnement réel de la machine.
Le but ici n’est **pas** de faire de toi un puriste qui code tout en assembleur.
Le but est de comprendre **l’essentiel utile** pour :
* mieux comprendre le code compilé
* mieux débugger
* mieux lire un désassemblage
* mieux comprendre comment le CPU manipule les données
* mieux comprendre la mémoire, les entiers, les flottants, les appels de fonctions
* avoir une vraie base pour le reverse, Windows internals et plus tard le kernel
---
# 1) C’est quoi l’assembleur
L’assembleur est un **langage de très bas niveau**.
Il est très proche du **langage machine**, c’est-à-dire des instructions réellement exécutées par le processeur.
Le CPU, lui, ne comprend pas :
* `if`
* `while`
* `class`
* `std::vector`
* `printf`
Tout ça, ce sont des abstractions de haut niveau.
Au niveau réel, le CPU exécute des instructions simples du style :
* déplacer une valeur
* additionner
* comparer
* sauter à une autre adresse
* lire ou écrire en mémoire
* appeler une fonction
* retourner d’une fonction
Exemple d’instructions assembleur très classiques :
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
L’assembleur est donc une **représentation lisible par l’humain** du langage machine.
---
# 2) Pourquoi apprendre l’assembleur
Aujourd’hui, on écrit rarement une application entière en assembleur.
Les compilateurs C/C++ sont excellents.
Mais apprendre l’assembleur reste extrêmement utile.
## Pourquoi ?
Parce que ça permet de comprendre :
* comment une variable est stockée en mémoire
* comment une fonction reçoit ses paramètres
* comment une valeur de retour circule
* comment le CPU représente les entiers signés et non signés
* pourquoi certains bugs existent
* ce que voit réellement un debugger
* ce que produit réellement un compilateur
En pratique, l’assembleur est très utile pour :
* le **debug avancé**
* le **reverse engineering**
* l’**analyse de malware**
* la **performance**
* la **compréhension des crashs**
* la **compréhension du code compilé**
* le **développement système**
Bref :
**tu n’as pas besoin d’écrire tout en assembleur, mais tu as besoin de le comprendre**.
---
# 3) Un ordinateur manipule des bits
À la base, un ordinateur ne manipule pas des `int`, des `float` ou des `string`.
Il manipule des **bits**.
Un **bit** est la plus petite unité d’information.
Il peut valoir :
* `0`
* `1`
C’est tout.
Ensuite, plusieurs bits sont regroupés pour former des données plus utiles.
## Le byte
Un **byte** (octet) = **8 bits**.
Exemple :
Code: Select all
01000001
* le nombre décimal `65`
* le caractère ASCII `A`
* un morceau d’une instruction machine
* un bout d’adresse
* une composante d’une couleur
* etc.
**Le sens dépend du contexte.**
C’est une idée fondamentale en bas niveau :
**la mémoire ne “sait” pas ce qu’elle contient**.
Elle contient juste des bits.
C’est le programme qui interprète ces bits.
---
# 4) Mémoire : tout n’est qu’une suite de bytes
La mémoire peut être vue comme une grande suite de cases numérotées.
Chaque case contient un byte.
Exemple très simplifié :
Code: Select all
Adresse Valeur
1000 41
1001 42
1002 43
1003 44
Une variable 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` classique occupe **4 bytes**
* un `double` occupe **8 bytes**
Quand le CPU lit ou écrit une variable, il manipule donc en réalité **un ensemble de bytes à une adresse donnée**.
---
# 5) Le binaire
Le système binaire est un système de numération en base 2.
En décimal, on utilise les puissances de 10.
En binaire, on utilise les 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
---
# 6) L’hexadécimal
L’hexadécimal est une base 16.
On y utilise les symboles :
* `0` à `9`
* `A` à `F`
Donc :
* `A = 10`
* `B = 11`
* `C = 12`
* `D = 13`
* `E = 14`
* `F = 15`
Exemple :
Code: Select all
0x2A = 2*16 + 10 = 42
Parce qu’il correspond très bien au binaire.
**1 chiffre hexadécimal = 4 bits**.
Exemple :
Code: Select all
0xA5 = 1010 0101b
* `A` = `1010`
* `5` = `0101`
Donc l’hexa permet de lire rapidement :
* des adresses mémoire
* des opcodes
* des flags
* des dumps mémoire
* des handles / valeurs techniques
En debug et reverse, tu verras de l’hexa partout.
---
# 7) Entiers signés et non signés
Un entier peut être :
* **non signé** (`unsigned`) : uniquement positif ou nul
* **signé** (`signed`) : positif ou négatif
## Exemples classiques
Sur 8 bits :
* non signé : `0` à `255`
* signé : `-128` à `127`
Sur 32 bits :
* non signé : `0` à `4294967295`
* signé : `-2147483648` à `2147483647`
La taille en bits change donc **la plage de valeurs possibles**.
Mais surtout :
**les mêmes bits peuvent représenter des choses différentes selon l’interprétation**.
Exemple sur 8 bits :
Code: Select all
11111111b
* `255` si on la considère comme `unsigned`
* `-1` si on la considère comme `signed` en complément à deux
Et ça, c’est fondamental.
---
# 8) Le complément à deux (two’s complement)
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.
## Idée générale
Pour obtenir `-x` en complément à deux :
1. on prend la représentation binaire de `x`
2. on inverse les bits
3. on ajoute `1`
Exemple pour `-1` sur 8 bits :
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 ; on ignore la retenue finale
```
C’est élégant, simple pour le matériel, et très utilisé partout.
---
# 9) Overflow, carry et pièges classiques
En bas niveau, la taille fixe des entiers compte énormément.
Exemple sur 8 bits non signés :
Code: Select all
255 = 11111111
* 1 = 00000001
---
```
00000000 ; débordement
```
Le bit supplémentaire déborde.
C’est pour ça qu’en assembleur et en C/C++, il faut faire très attention à :
* la taille des types
* le signed / unsigned
* les débordements
* les promotions implicites
* les comparaisons mixtes
Le CPU garde souvent des informations sur l’opération dans des **flags** :
* `CF` : Carry Flag
* `ZF` : Zero Flag
* `SF` : Sign Flag
* `OF` : Overflow Flag
On ne les détaille pas à fond ici, mais retiens déjà que **les opérations arithmétiques mettent à jour des drapeaux** que les instructions de saut peuvent ensuite tester.
---
# 10) L’endianness
Quand une valeur occupe plusieurs bytes, il faut savoir **dans quel ordre ces bytes sont stockés en mémoire**.
C’est là qu’intervient l’endianness.
## Little-endian
Sur x86 et x64, l’architecture est **little-endian**.
Ça veut dire que le **byte de poids faible** est stocké en premier.
Exemple avec la valeur :
Code: Select all
0x12345678
Code: Select all
78 56 34 12
* lire un dump mémoire
* comprendre un debugger
* analyser des structures
* manipuler des fichiers binaires
* faire du reverse
Beaucoup de débutants voient `0x12345678` et s’attendent à voir `12 34 56 78` en mémoire.
Sur x86/x64, **ce n’est pas le cas**.
---
# 11) Le langage machine
Le langage machine est la forme brute réellement comprise par le processeur.
Ce sont des **octets d’instructions**, par exemple :
Code: Select all
B8 01 00 00 00
Le processeur, lui, décode ces bytes comme une instruction.
Par exemple, cette séquence peut correspondre à quelque chose comme :
Code: Select all
mov eax, 1
* **le CPU exécute des octets**
* **l’assembleur est une représentation textuelle de ces octets**
Donc quand on désassemble un programme, on convertit des bytes machine en instructions lisibles.
---
# 12) Assembleur vs langage machine
Ne mélangeons pas les deux :
## Langage machine
C’est la forme binaire brute exécutée par le CPU.
## Assembleur
C’est une forme texte, plus lisible, qui représente ces instructions.
Par exemple :
Code: Select all
mov eax, 1
ret
Ensuite, un programme appelé **assembleur** 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 x86/x64, tu verras souvent :
* `rax`, `rbx`, `rcx`, `rdx`
* `rsp` : stack pointer
* `rbp` : base/frame pointer
* `rip` : instruction pointer
En 32 bits, leurs versions historiques sont :
* `eax`, `ebx`, `ecx`, `edx`
* `esp`, `ebp`, `eip`
Le CPU travaille énormément avec ces registres.
Exemple :
Code: Select all
mov eax, 10
add eax, 5
* `eax` reçoit `10`
* puis devient `15`
Les registres sont centraux en assembleur, car presque tout passe par eux.
---
# 14) La pile (stack)
La **stack** est une zone mémoire très importante.
Elle sert notamment à :
* stocker des variables locales
* sauvegarder des registres
* stocker des adresses de retour
* passer certains paramètres de fonctions
Sur x86/x64, la pile grandit généralement **vers les adresses basses**.
Les instructions classiques sont :
Code: Select all
push rax
pop rax
call fonction
ret
* `push` empile une valeur
* `pop` dépile une valeur
* `call` appelle une fonction et empile l’adresse de retour
* `ret` récupère cette adresse et revient à l’appelant
Comprendre la stack est vital pour :
* débugger
* lire un désassemblage
* comprendre les fonctions
* comprendre les calling conventions
* comprendre certains exploits mémoire
---
# 15) Une fonction en assembleur : idée générale
Une fonction assembleur ressemble souvent à ç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
Dans la vraie vie, les compilateurs peuvent produire quelque chose de plus optimisé, mais cette forme pédagogique est utile pour comprendre.
---
# 16) Instructions fondamentales à connaître
Voici quelques instructions de base qu’on retrouve 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 / 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
Appel et retour de fonction.
Code: Select all
call ma_fonction
ret
---
# 17) Exemple simple : condition
Voici un mini exemple conceptuel.
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:
* on charge `a`
* on compare à `5`
* si différent, on saute à `sinon`
* sinon on met `1` dans `b`
* puis on évite la branche `else`
C’est exactement ce genre de logique qu’un compilateur transforme à partir d’un `if` en C/C++.
---
# 18) Exemple simple : 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 que :
* la boucle n’est qu’un enchaînement de `cmp`
* puis de sauts conditionnels
* puis d’un saut de retour vers le début
---
# 19) Les flottants : à retenir sans se noyer
Les nombres à virgule (`float`, `double`) sont plus complexes que les entiers.
Ils sont généralement stockés selon une représentation de type **IEEE 754**.
Un `float` 32 bits contient essentiellement :
* un bit de signe
* un exposant
* une fraction (mantisse)
Le détail mathématique peut devenir vite technique.
Pour un premier cours d’introduction, ce qu’il faut surtout retenir est :
* un float n’est **pas** stocké comme un entier
* certains nombres décimaux ne sont **pas représentables exactement** en binaire
* ça explique des comportements comme :
Code: Select all
0.1 + 0.2 != 0.3
Pour le dev système débutant, le plus important est surtout de savoir que :
* les entiers et les flottants sont stockés différemment
* les flottants ont leurs propres règles
* 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 généralement plusieurs étapes :
## 1. Écriture du 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 les morceaux ensemble :
* ton code
* les bibliothèques
* les symboles externes
* le point d’entrée
Et produit le binaire final :
Code: Select all
monprogramme.exe
Si tu appelles une API ou une fonction d’une bibliothèque, c’est lui qui fait le travail de liaison.
---
# 21) Exemple minimal d’idée de programme assembleur
Voici une version volontairement simple et pédagogique d’un squelette de programme.
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
Selon l’assembleur utilisé (`MASM`, `NASM`, `FASM`, etc.), la syntaxe change un peu.
Mais les principes restent proches.
---
# 22) Ce qu’il faut absolument comprendre pour le debug et le reverse
Quand tu regardes un désassemblage, tu dois progressivement apprendre à voir ceci :
## Un `if`
=
une comparaison + un saut conditionnel
## Une boucle
=
un label + une comparaison + un saut
## Un appel de fonction
=
`call`
## Un retour
=
`ret`
## Une variable locale
=
quelque chose sur la stack ou dans un registre
## Une structure
=
un bloc mémoire avec des offsets
## Un pointeur
=
une adresse
## Un accès à un champ
=
lecture mémoire à un offset donné
Exemple conceptuel :
Code: Select all
mov eax, [rcx+8]
* `rcx` pointe vers une structure
* on lit la donnée située à l’offset `8`
C’est exactement le genre de lecture mentale qu’il faut développer quand on fait du système ou du reverse.
---
# 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 l’essentiel vital :
## 1. Comprendre les bases des nombres
* binaire
* hexadécimal
* signed / unsigned
* complément à deux
## 2. Comprendre la mémoire
* adresses
* bytes
* variables multi-bytes
* little-endian
## 3. Comprendre les registres
* où transitent les données
* où passent souvent les paramètres et valeurs de retour
## 4. Comprendre la stack
* appels de fonctions
* variables locales
* adresses de retour
## 5. Comprendre le flux d’exécution
* `cmp`
* `jmp`
* sauts conditionnels
* `call`
* `ret`
## 6. Comprendre que le C/C++ finit toujours par devenir ça
Le compilateur masque la complexité, mais en dessous :
**tout redevient instructions, registres, mémoire et sauts**.
---
# 24) Erreurs fréquentes des débutants
Voici quelques pièges très classiques.
## Confondre valeur et adresse
Avoir `5` et avoir l’adresse où est stocké `5`, ce n’est pas la même chose.
## Oublier le little-endian
Voir `0x12345678` et s’attendre à lire `12 34 56 78` en mémoire sur x86/x64.
## Confondre signed et unsigned
Les mêmes bits peuvent représenter des valeurs différentes.
## Penser que la mémoire “connaît” les types
Non.
La mémoire contient des bytes.
Le type est une interprétation.
## Penser que l’assembleur est “magique”
Non plus.
C’est juste une vue plus proche du fonctionnement réel.
---
# 25) Petit exemple mental complet
Prenons cette valeur :
Code: Select all
0x12345678
Elle s’écrit directement comme ça.
## En bytes sur x86/x64
Code: Select all
78 56 34 12
On obtient :
Code: Select all
0x78 = 120
On retrouve :
Code: Select all
0x12345678
* hexa
* byte
* multi-byte
* mémoire
* endianess
* interprétation
---
# 26) Pourquoi tout ça aide énormément en Windows internals
Quand tu liras ou analyseras :
* une structure du noyau
* une structure PE
* un appel de fonction désassemblé
* une stack trace
* un dump mémoire
* un contexte de thread
* un registre dans WinDbg ou x64dbg
… toutes ces bases vont te servir directement.
Sans elles, le bas niveau ressemble à du bruit.
Avec elles, tu commences à voir la logique.
Et c’est là que tout devient beaucoup plus 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 la mémoire, la stack, les registres et le flux d’exécution
Les notions absolument vitales au début sont :
* bit
* byte
* binaire
* hexadécimal
* signed / unsigned
* complément à deux
* little-endian
* registres
* stack
* `mov`, `cmp`, `jmp`, `call`, `ret`
Tu n’as pas besoin d’être un expert assembleur pour faire du C/C++ système.
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 en réalité, il simplifie aussi énormément les choses :
* plus de classes
* plus de templates
* plus de frameworks
* plus de magie
Il reste surtout :
* des données
* des adresses
* des registres
* des instructions
* des sauts
Et pour un dev système, c’est une base énorme.
Quand tu commences à comprendre ça, tu comprends beaucoup mieux :
* ce que fait vraiment un compilateur
* ce que voit vraiment un debugger
* ce qu’exécute vraiment le CPU
Bref :
**l’assembleur n’est pas juste “un vieux langage”**.
C’est une des meilleures portes d’entrée pour comprendre la machine.
---
# Mini bloc récapitulatif
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 représenté en complément à deux
x86/x64 = little-endian
ASM = représentation lisible du code machine
# Pour la suite
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 fonctions et prologues/epilogues
* le lien avec le C/C++ et le désassemblage Windows
Là, on commencera vraiment à entrer dans **l’assembleur utile pour le debug, le reverse et le système**.
