Page 1 of 1

Débuter avec l'assembleur

Posted: Tue Mar 10, 2026 5:01 pm
by Hydraxx
# Les bases de l’assembleur

# Les bases de l’assembleur

Salut aujourd’hui 8-) on va voir **un sujet fondamental pour tout dev système, reverseur, debugger ou futur dev kernel** :
**les bases de l’assembleur**. 8-)

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
Ici on dit au CPU :

* 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
Ce byte peut représenter :

* 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
Les adresses sont souvent écrites en **hexadécimal**, car c’est beaucoup plus pratique que le binaire.

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
Autre exemple :

Code: Select all

11111111b = 255
Quand on lit du code machine, des flags, des masks ou des permissions, comprendre le binaire devient très utile.

---

# 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
## Pourquoi l’hexadécimal est partout en système

Parce qu’il correspond très bien au binaire.

**1 chiffre hexadécimal = 4 bits**.

Exemple :

Code: Select all

0xA5 = 1010 0101b
Conversion :

* `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
Cette valeur peut être lue comme :

* `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
Donc :

Code: Select all

-1 = 11111111
Exemple pour `-2` :

Code: Select all

2  = 00000010
inv = 11111101
+1  = 11111110
Donc :

Code: Select all

-2 = 11111110
## Pourquoi c’est pratique ?

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
```

Résultat : `4`

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 résultat mathématique réel serait `256`, mais sur 8 bits on ne peut pas le stocker.
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
En mémoire sur une machine x86/x64, on verra :

Code: Select all

78 56 34 12
C’est extrêmement important pour :

* 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
Pour un humain, ce n’est pas très lisible.

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
L’idée importante est la suivante :

* **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
L’assembleur est écrit par l’humain.
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
Ici :

* `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
## Idée simple

* `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
```

Ici :

* 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
## `add`

Addition.

Code: Select all

add eax, 3
## `sub`

Soustraction.

Code: Select all

sub eax, 1
## `inc` / `dec`

Incrémente / décrémente.

Code: Select all

inc eax
dec ebx
## `cmp`

Compare deux opérandes et met à jour les flags.

Code: Select all

cmp eax, ebx
## `jmp`

Saut inconditionnel.

Code: Select all

jmp suite
## Sauts conditionnels

Code: Select all

je egal
jne pas_egal
jg plus_grand
jl plus_petit
## `call` / `ret`

Appel et retour de fonction.

Code: Select all

call ma_fonction
ret
Tu verras ces instructions littéralement partout dans un désassemblage.

---

# 17) Exemple simple : condition

Voici un mini exemple conceptuel.

Pseudo-code C :

Code: Select all

if (a == 5)
b = 1;
else
b = 0;
Version assembleur simplifiée :

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:
Logique :

* 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++;
}
Version assembleur simplifiée :

Code: Select all

debut:
cmp eax, 10
jge fin
inc eax
jmp debut
fin:
Ici `eax` joue le rôle de `i`.

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
au sens strict de l’égalité binaire

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
## 2. Assemblage

L’assembleur transforme le texte en **code objet**.

Exemple :

Code: Select all

moncode.obj
ou selon l’outil :

Code: Select all

moncode.o
## 3. Linking

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
Le linker résout par exemple les références vers des fonctions externes.

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
L’idée ici n’est pas de coller exactement à un assembleur précis, mais de comprendre la logique :

* 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]
Ça veut souvent dire :

* `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
## En hexadécimal

Elle s’écrit directement comme ça.

## En bytes sur x86/x64

Code: Select all

78 56 34 12
## Si on lit le premier byte seul

On obtient :

Code: Select all

0x78 = 120
## Si on lit les 4 bytes comme un entier 32 bits little-endian

On retrouve :

Code: Select all

0x12345678
Ce petit exercice résume déjà plusieurs notions essentielles :

* 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**.