3 points par GN⁺ 2025-07-14 | 1 commentaires | Partager sur WhatsApp
  • 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
  • rsp est le pointeur de pile, rsi/rdi servent d’index pour le traitement de chaînes, et certains registres ont donc un rôle spécifique
  • rip est le pointeur d’instruction, et rflags est 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 GUI
  • entry start : définit le point d’entrée du programme ; l’exécution commence à l’emplacement du label start
  • section '.text' code readable executable : indique qu’il s’agit de la section de code du PE, exécutable
  • start: : donne un nom au point d’entrée défini précédemment
  • int3 : point d’arrêt pour le débogueur, utilisé pour suspendre le programme et inspecter son état
  • ret : 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ématiquement ExitProcess

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 registre rcx, premier argument utilisé ici comme code de sortie

  • call [ExitProcess] : saute vers l’adresse de fonction de ExitProcess ré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 registre rcx, 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

 
GN⁺ 2025-07-14
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/esi via sil)
    C’est un concept similaire à l’accès à al depuis ax/eax
    Je 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 .text pour utiliser un accès RIP-relative
    Le programme de Fibonacci n’avait pas besoin de beaucoup de bytecode
    J’aimerais vraiment avoir des conseils sur les points à améliorer

    • Je me demande si tu as comparé directement ton code avec celui généré par le compilateur C++
      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émoire
      En 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

    • Je me demande si vous faites une validation des entrées
      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

    • Je me demande quelles autres syntaxes d’assembleur existent
  • J’aimerais bien faire quelque chose en assembleur, mais aucune idée particulière ne me vient

    • Je recommande le jeu TIS-100
      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