Apprendre l’assembleur x86-64
(gpfault.net)- Présentation du premier article d’une série d’introduction à l’assembleur x86-64
- Explication de l’installation des outils et de la structure de base dans le contexte des systèmes 64 bits modernes
- Recommandation d’utiliser Flat Assembler (FASM) et WinDbg comme principaux outils de développement et de débogage
- Inclut un résumé des connaissances essentielles utiles en pratique, comme le format PE, l’import de DLL et la convention d’appel Windows
- Présentation centrée sur l’expérience pratique, avec l’écriture d’un programme qui se termine simplement et un exercice de procédure de débogage
Introduction et intérêt
- Lors d’une première découverte de l’assembleur x86, l’enseignement universitaire repose souvent sur un environnement ancien (16 bits, DOS, mémoire segmentée)
- Aujourd’hui, les processeurs 64 bits étant devenus la norme, cette série ne traite que de l’environnement x86-64 réellement utilisé et écarte tous les éléments obsolètes
- Ce tutoriel se concentre sur le développement de programmes 64 bits fonctionnant sous Windows
- Il commence par le code minimal permettant d’accéder directement au système d’exploitation, sans utiliser de bibliothèque
- Cet article s’adresse aux développeurs qui souhaitent apprendre l’assembleur et suppose des connaissances de base en C/C++
Préparation des outils de développement
Assembleur (Assembler)
- Le CPU ne peut interpréter que du code machine, difficile à comprendre pour un humain, et l’assembleur est la forme lisible par l’être humain de ce code
- Le programme qui transforme le langage assembleur en code machine est l’assembleur
- L’assembleur x86-64 ne suit pas de standard unique, et chaque assembleur a sa propre syntaxe et son propre mode de fonctionnement
- Cette série utilise Flat Assembler (FASM), qui est léger, facile à utiliser et fournit un système de macros puissant ainsi qu’un éditeur
Débogueur (Debugger)
- Pour analyser le code assembleur écrit et observer le flux d’exécution, le débogueur est un outil indispensable
- WinDbg est recommandé, car il permet de consulter et manipuler séparément les registres, la mémoire, le code assembleur, etc.
- Il peut être installé en sélectionnant uniquement les composants nécessaires depuis le SDK Windows 10
- Le débogueur permet d’observer directement l’état interne du programme, la structure mémoire et les changements de registres
Perspective sur la programmation en assembleur
Structure du CPU et jeu d’instructions
- Le CPU ne peut exécuter qu’un ensemble limité d’opérations défini par un jeu d’instructions
- Une instruction est l’unité élémentaire de travail que le CPU peut exécuter
- Chaque instruction fonctionne de manière très simple avec ses paramètres (stockage de valeur, opérations arithmétiques, etc.)
- En programmation bas niveau comme en débogage, il est essentiel de comprendre que cette structure constitue la base de tous les concepts de haut niveau
Registres (Registers)
- Les registres sont des zones de mémoire spécialisées extrêmement rapides, intégrées au CPU
- En x86-64, il existe 16 registres à usage général, tous d’une taille de 64 bits
- Chaque registre peut être partiellement accessible au niveau de l’octet, du mot et du double mot
| Registre | Octet bas | Mot bas | Double mot bas |
|---|---|---|---|
| rax | al | ax | eax |
| rbx | bl | bx | ebx |
| rcx | cl | cx | ecx |
| rdx | dl | dx | edx |
| rsp | spl | sp | esp |
| rsi | sil | si | esi |
| rdi | dil | di | edi |
| rbp | bpl | bp | ebp |
| r8~r15 | r8b~r15b | r8w~r15w | r8d~r15d |
rspest le pointeur de pile,rsi/rdiservent d’index pour le traitement de chaînes, et certains registres ont donc un rôle spécifiqueripest le pointeur d’instruction, etrflagsest un registre spécial qui contient les indicateurs d’état des résultats d’opérations
Mémoire et adresses
- La mémoire fonctionne comme un tableau continu d’octets à partir de l’index 0
- Sur l’ancienne architecture x86, le modèle segment-offset était indispensable, mais en x86-64 toute la mémoire est traitée comme un espace d’adressage plat (Flat)
- En pratique, le système d’exploitation et le matériel mappent dynamiquement un espace d’adressage virtuel de chaque processus vers la mémoire physique
- Autrement dit, une même adresse virtuelle peut correspondre à des mémoires physiques différentes selon les processus
- Les instructions et les données résident dans la même mémoire (architecture de von Neumann), à la différence d’une architecture Harvard comme celle de l’AVR utilisé sur Arduino, où les données sont stockées séparément
Écrire un premier programme en assembleur
- Après l’installation de FASM, exercice consistant à écrire et compiler le programme simple ci-dessous
format PE64 NX GUI 6.0
entry start
section '.text' code readable executable
start:
int3
ret
Explication du code
format PE64 NX GUI 6.0: spécifie le format du fichier exécutable généré par FASM ; ici, un PE (Portable Executable) 64 bits GUIentry start: définit le point d’entrée du programme ; l’exécution commence à l’emplacement du labelstartsection '.text' code readable executable: indique qu’il s’agit de la section de code du PE, exécutablestart:: donne un nom au point d’entrée défini précédemmentint3: point d’arrêt pour le débogueur, utilisé pour suspendre le programme et inspecter son étatret: instruction qui récupère une adresse depuis la pile et transfère le contrôle à cet emplacement ; dans ce programme, cela provoque une fin immédiate
Exercice de débogage
-
Dans WinDbg, ouvrir le fichier exécutable (.exe) du programme ci-dessus et préparer les différentes fenêtres, comme le désassemblage et les registres
-
Appuyer sur F5 pour amener le programme jusqu’au point d’arrêt, puis sur F8 pour exécuter une instruction à la fois
-
Il est possible d’observer en temps réel les changements des registres (
rip, etc.) -
Après l’exécution de
ret, le contrôle est rendu au système d’exploitation, puis la fermeture du thread et du processus se poursuit avec l’appel àRtlExitUserThread -
Attention : si l’on termine uniquement avec
ret, le processus peut rester actif selon l’existence d’autres exécutions en arrière-plan en dehors du thread ; pour une fin correcte, il est donc préférable d’appeler systématiquementExitProcess
Format PE et import de DLL
Vue d’ensemble de la structure d’import de fonctions DLL
- Les fonctions WinAPI comme ExitProcess se trouvent dans KERNEL32.DLL
- Pour utiliser ce type de fonction externe, il faut construire la table d’import du fichier exécutable (section
.idata) - L’Import Directory Table (IDT) de la section idata contient des informations comme le nom de la DLL, le nom des fonctions et les adresses RVA de l’IAT/ILT
- L’IAT (Import Address Table) est remplacée à l’exécution par les adresses réelles des fonctions par le chargeur du système d’exploitation
- La Hint/Name Table contient le nom de chaque fonction ainsi que les informations d’indice associées
Exemple de définition de la section .idata dans FASM
section '.idata' import readable writeable
idt:
dd rva kernel32_iat
dd 0
dd 0
dd rva kernel32_name
dd rva kernel32_iat
dd 5 dup(0)
name_table:
_ExitProcess_Name dw 0
db "ExitProcess", 0, 0
kernel32_name: db "KERNEL32.DLL", 0
kernel32_iat:
ExitProcess dq rva _ExitProcess_Name
dq 0
- db/dw/dd/dq : insèrent des valeurs au format octet/mot/double mot/quad word (8 octets)
- rva : calcule l’adresse virtuelle relative (Relative Virtual Address) d’un symbole
- Il est possible de référencer des fonctions de DLL en construisant manuellement l’IAT et la Name Table
Convention d’appel 64 bits Windows (MS x64 Calling Convention)
- Il s’agit de la convention standard qui définit la manière de passer les arguments à une fonction et d’utiliser la pile
- En 64 bits sous Windows, on utilise la Microsoft x64 Calling Convention
- Principales caractéristiques :
- Le pointeur de pile doit toujours être aligné sur 16 octets
- Les 4 premiers arguments entiers/pointeurs utilisent les registres rcx, rdx, r8, r9
- Les 4 premiers arguments en virgule flottante sont placés dans xmm0~xmm3
- Les arguments supplémentaires passent par la pile
- Il faut réserver 32 octets de shadow space sur la pile, quel que soit le nombre d’arguments
- Le nettoyage de la pile est à la charge de l’appelant
Exemple d’appel à ExitProcess
format PE64 NX GUI 6.0
entry start
section '.text' code readable executable
start:
int3
sub rsp, 8 * 5
xor rcx, rcx
call [ExitProcess]
section '.idata' import readable writeable
idt:
dd rva kernel32_iat
dd 0
dd 0
dd rva kernel32_name
dd rva kernel32_iat
dd 5 dup(0)
name_table:
_ExitProcess_Name dw 0
db "ExitProcess", 0, 0
kernel32_name db "KERNEL32.DLL", 0
kernel32_iat:
ExitProcess dq rva _ExitProcess_Name
dq 0
Analyse du nouveau code
-
sub rsp, 8 * 5: ajuste le pointeur de pile (réserve 40 octets), ce qui permet en une seule fois l’alignement sur 16 octets et la réservation du shadow space -
xor rcx, rcx: affecte la valeur 0 au registrercx, premier argument utilisé ici comme code de sortie -
call [ExitProcess]: saute vers l’adresse de fonction deExitProcessréellement écrite dans la table d’import -
Dans WinDbg, une exécution pas à pas permet d’observer directement les changements du pointeur de pile (
rsp) et du registrercx, ainsi que le déroulement de la fin du processus
Conclusion
- Cet article présente, sous un angle très pratique, le flux général de l’assembleur x86-64, de la configuration des outils de base au format PE, à l’import de DLL, à la convention d’appel x64, à l’écriture du premier programme et au débogage
- La partie suivante abordera des fonctionnalités plus variées et du code réel
1 commentaires
Commentaires Hacker News
Je voulais partager un projet que je développe depuis quelques années
https://asm-editor.specy.app
C’est un IDE interactif en ligne qui prend en charge plusieurs langages d’assemblage, dont M68K, MIPS, RISC-V et X86
Il propose de nombreuses fonctionnalités pour enseigner la programmation en assembleur
Il peut aussi être intégré à d’autres sites web
Je ne savais pas qu’il existait une fonctionnalité d’accès direct à l’octet de poids faible des registres d’index/pointeur (par ex. en 16/32 bits, accéder à
si/esiviasil)C’est un concept similaire à l’accès à
aldepuisax/eaxJe me demande s’il existe réellement un opcode ajouté spécialement pour cela en x86_64
Ça me donne envie de revérifier la spécification de la plateforme
Je pose la question par pure curiosité
Je partage un document d’introduction à l’assembleur que j’ai rédigé moi-même
https://www.nayuki.io/page/a-fundamental-introduction-to-x86-assembly-programming
Je me suis demandé si je pouvais rendre le dispatch de mon émulateur CPU plus rapide qu’en C++, alors j’ai essayé de l’optimiser en assembleur
J’ai lancé un programme de Fibonacci, mais le résultat n’était pas du tout à la hauteur
Au final, je ne l’ai fusionné qu’avec une option pour le désactiver par défaut
Je suis quand même convaincu qu’il y a moyen d’aller plus vite
https://github.com/libriscv/libriscv/blob/master/lib/libriscv/amd64/inaccurate_dispatch.nasm
J’ai un peu amélioré les performances en apprenant à mieux gérer les accès mémoire
J’ai réduit la table de saut de 64 bits à 32 bits et l’ai placée dans la section
.textpour utiliser un accès RIP-relativeLe programme de Fibonacci n’avait pas besoin de beaucoup de bytecode
J’aimerais vraiment avoir des conseils sur les points à améliorer
Je n’ai pas bien le contexte, mais il est possible que l’écart ne vienne pas du mécanisme de dispatch (la façon de récupérer les instructions), mais plutôt de la différence d’implémentation des instructions elles-mêmes
Une piste d’optimisation consiste à mapper les registres émulés sur de vrais registres x86-64, sans jamais les faire repasser par la mémoire
De cette façon, pour une opération comme
add, on peut calculer directement sans recharger depuis la mémoireEn revanche, cette approche rend l’écriture de l’émulateur bien plus pénible
Voici une introduction à l’assembleur x86 avec pratique directe dans le navigateur
On peut exécuter les exemples immédiatement, sans configuration locale particulière
https://shikaan.github.io/assembly/x86/guide/2024/09/08/x86-64-introduction-hello.html
Pour information, c’est moi qui l’ai écrite
Comme cela semble assembler directement avec NASM puis exécuter le binaire, je m’interroge sur la sécurité
En voyant seulement la photo de profil, j’ai cru que c’était junferno
Le simple fait de toucher un peu à l’assembleur permet déjà d’approfondir sa compréhension globale, donc c’est toujours une bonne expérience
Pas besoin de construire un gros projet : je recommande de prendre son courage à deux mains et d’essayer soi-même, ne serait-ce qu’un peu
Je partage le lien vers la discussion HN de l’époque (2020)
https://news.ycombinator.com/item?id=24195627
Je suis content que ce soit en syntaxe assembleur de style Intel
J’aimerais bien faire quelque chose en assembleur, mais aucune idée particulière ne me vient
C’est un jeu de puzzle basé sur une sorte de pseudo-assembleur
Je pense que ce genre de jeu peut combler cette envie d’assembleur