Débuter avec l'assembleur

Salut ce forum est dédié à comprendre les entrailles de l'ordinateur au plus bas niveau bienvenue dans le monde du reverse et de l'assembleur

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Débuter avec l'assembleur

Post by Hydraxx »

Les bases de l’assembleur

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

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
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 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”
Tout ça, ce sont des abstractions. Au niveau réel, le processeur exécute des opérations beaucoup plus simples, par exemple :
  • déplacer une valeur
  • additionner
  • soustraire
  • comparer
  • faire un saut
  • lire ou écrire en mémoire
  • appeler une fonction
  • retourner à l’appelant
Exemple très simple :

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
Donc l’assembleur est surtout une représentation lisible par l’humain du langage machine.

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
En pratique, l’assembleur aide énormément pour :
  • 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
Donc le vrai message est simple :

Code: Select all

tu n’as pas besoin d’écrire tout en assembleur
mais tu as besoin de le comprendre
3) Un ordinateur manipule des bits

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
C’est tout.

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
Ce byte peut représenter plein de choses différentes :
  • le nombre décimal 65
  • le caractère ASCII A
  • un morceau d’instruction machine
  • une partie d’adresse
  • une donnée brute
Le point fondamental ici, c’est :

Code: Select all

la mémoire ne “sait” pas ce qu’elle contient
Elle contient juste des bits et des bytes.
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
Les adresses sont souvent affichées en hexadécimal, parce que c’est beaucoup plus pratique.

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
Quand le CPU lit ou écrit une variable, il manipule donc une suite de bytes à une adresse donnée.

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

Code: Select all

11111111b = 255
Le binaire devient très utile quand on veut comprendre :
  • les flags
  • les masques de bits
  • les permissions
  • les registres
  • la représentation interne des valeurs
6) L’hexadécimal

L’hexadécimal est une base 16.

On 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’hexa est partout en système ?
Parce qu’elle se marie parfaitement avec le binaire :

Code: Select all

1 chiffre hexa = 4 bits
Exemple :

Code: Select all

0xA5 = 1010 0101b
Donc l’hexa est très pratique pour lire :
  • des adresses mémoire
  • des dumps
  • des opcodes
  • des handles
  • des offsets
  • des flags
En debug et reverse, tu verras de l’hexa partout.

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
Exemples sur 8 bits :
  • unsigned : 0 à 255
  • signed : -128 à 127
Exemples sur 32 bits :
  • unsigned : 0 à 4294967295
  • signed : -2147483648 à 2147483647
Le point très important :

Code: Select all

les mêmes bits peuvent représenter des valeurs différentes
selon l’interprétation signée ou non signée
Exemple sur 8 bits :

Code: Select all

11111111b
Cette valeur peut être lue comme :
  • 255 si on la lit en unsigned
  • -1 si on la lit en signed avec complément à deux
C’est une base essentielle pour comprendre beaucoup de bugs et beaucoup de comparaisons en C/C++ et en assembleur.

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
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, et on obtient bien 4.

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 résultat mathématique réel serait 256, mais sur 8 bits il n’y a pas assez de place. Le bit supplémentaire déborde.

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
Ces flags sont essentiels, parce qu’ils sont testés ensuite par les instructions de saut conditionnel.

Par exemple, après un :

Code: Select all

cmp eax, ebx
le CPU met à jour des flags, puis une instruction comme :

Code: Select all

je cible
jne autre_cible
jg plus_grand
jl plus_petit
peut décider où aller selon ces drapeaux.

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
Exemple avec :

Code: Select all

0x12345678
En mémoire sur 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
  • interpréter des structures
  • analyser des fichiers binaires
  • faire du reverse
Beaucoup de débutants s’attendent à voir :

Code: Select all

12 34 56 78
mais 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.

Par exemple, une séquence de bytes comme :

Code: Select all

B8 01 00 00 00
peut correspondre à une instruction lisible comme :

Code: Select all

mov eax, 1
Le point à retenir :
  • le CPU exécute des bytes
  • l’assembleur est une représentation texte lisible de ces bytes
Quand on désassemble un programme, on transforme du code machine brut en instructions compréhensibles par un humain.

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
Assembleur :
  • forme texte lisible
  • représentation humaine du code machine
Exemple :

Code: Select all

mov eax, 1
ret
L’assembleur est écrit par l’humain.
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
Sur x86 32 bits, leurs versions historiques sont :
  • eax
  • ebx
  • ecx
  • edx
  • esp
  • ebp
  • eip
Rôles importants :
  • rsp / esp : stack pointer
  • rbp / ebp : base ou frame pointer
  • rip / eip : instruction pointer
Exemple :

Code: Select all

mov eax, 10
add eax, 5
Ici, eax reçoit 10, puis devient 15.

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
Sur x86/x64, la pile grandit généralement vers les adresses basses.

Instructions très classiques :

Code: Select all

push rax
pop rax
call fonction
ret
Idée simple :
  • 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
Comprendre la stack est absolument vital pour :
  • le debug
  • la lecture d’un désassemblage
  • la compréhension des fonctions
  • les calling conventions
  • certains bugs mémoire
15) Une fonction en assembleur : vue pédagogique

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
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 optimiser tout ça, supprimer rbp, réorganiser le prologue, ou utiliser d’autres schémas. Mais cette forme pédagogique reste très utile.

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
add

Addition :

Code: Select all

add eax, 3
sub

Soustraction :

Code: Select all

sub eax, 1
inc / dec

Incrémente ou 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
jge sup_ou_egal
jle inf_ou_egal
call / ret

Appel et retour de fonction :

Code: Select all

call ma_fonction
ret
Ces instructions forment déjà le cœur de la lecture d’un désassemblage.

17) Exemple simple : un if

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 :
  • charger a
  • le comparer à 5
  • si différent, sauter à sinon
  • sinon écrire 1 dans b
  • puis sauter à la fin
C’est exactement ce genre de transformation que fait le compilateur.

18) Exemple simple : une 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 qu’une boucle, au niveau réel, c’est surtout :
  • une comparaison
  • un saut conditionnel
  • un saut de retour
19) Les flottants : à retenir sans se noyer

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
Le point important pour un premier cours n’est pas de tout détailler, mais de retenir ceci :
  • 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
Exemple classique :

Code: Select all

0.1 + 0.2 != 0.3
au sens strict de l’égalité binaire.

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
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 :
  • ton code
  • les bibliothèques
  • les symboles externes
  • le point d’entrée
Puis il produit le binaire final :

Code: Select all

monprogramme.exe
Le linker résout aussi les références vers des fonctions externes comme des APIs système.

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
Le but n’est pas ici d’être parfait pour un assembleur particulier, mais de comprendre la logique :
  • une section de code
  • un symbole global
  • une fonction d’entrée
  • une valeur de retour
Selon l’outil utilisé, MASM, NASM, FASM, etc., la syntaxe varie un peu, mais les principes restent proches.

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

Code: Select all

mov eax, [rcx+8]
Ça veut souvent dire :
  • rcx pointe sur une structure ou un objet
  • on lit la donnée située à l’offset 8
C’est exactement le genre de lecture mentale qu’il faut développer en reverse, debug ou système.

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
2. La mémoire
  • adresses
  • bytes
  • données multi-bytes
  • little-endian
3. Les registres
  • où passent les valeurs
  • où transitent souvent les paramètres
  • où arrivent souvent les retours
4. La stack
  • variables locales
  • adresses de retour
  • prologues / épilogues
  • calling conventions
5. Le flux d’exécution
  • cmp
  • jmp
  • sauts conditionnels
  • call
  • ret
Et surtout il faut comprendre :

Code: Select all

le C/C++ finit toujours par devenir ça
Le compilateur masque énormément de choses, mais en dessous, tout redevient instructions, registres, mémoire et sauts.

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
Quelques rappels utiles :
  • 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
25) Petit exercice mental complet

Prenons :

Code: Select all

0x12345678
En hexa, on l’écrit comme ça.

En mémoire sur x86/x64, on verra :

Code: Select all

78 56 34 12
Si on lit seulement le premier byte :

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 exemple résume déjà beaucoup de choses :
  • hexa
  • bytes
  • multi-byte
  • endianness
  • interprétation
  • lecture mémoire
26) Pourquoi tout ça aide énormément en Windows internals

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
… 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 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
Les notions 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 il simplifie aussi beaucoup de choses.

Il n’y a plus :
  • de classes complexes
  • de templates
  • de frameworks
  • de grosses couches de confort
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 réellement un compilateur
  • ce que voit réellement un debugger
  • ce qu’exécute réellement le CPU
Bref :

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
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 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 prologues / épilogues
  • le lien entre C/C++ et désassemblage Windows
Là, on commencera vraiment à entrer dans l’assembleur utile pour le debug, le reverse et le système. 8-)

Who is online

Users browsing this forum: No registered users and 0 guests