Réapprendre le C : l'assembleur comme fond de la piscine

Par:
francoistonic

lun, 27/07/2026 - 09:01

Par Emilie Peretti. Emilie est développeuse, arrivée au code par un chemin détourné : d’abord les langues et la traduction, puis Rust en 2025 et un DESU en data science obtenu en juin 2026. Depuis, sa curiosité la pousse toujours plus bas — du Rust au C++, puis à l’assembleur. Elle construit langages et systèmes distribués en open source, et fonde WhispHub. Elle se forme actuellement à l’École 42. 

Je ne viens pas de l'informatique. Mon parcours, ce sont d'abord les langues et la traduction — la grammaire humaine, celle des conjugaisons. J'ai commencé à coder en octobre 2025, avec Rust, et quelque part entre une table de déclinaisons et ma première erreur de compilation, un déclic s'est produit : un langage de programmation, c'est juste une autre grammaire, une où l'intention devient action.

Depuis, la curiosité m'a menée de plus en plus bas. Rust, puis un post-diplôme en data science, et aujourd'hui la piscine de l'École 42 — qui m'apprend l'humilité, un segfault à la fois. Et c'est là, en pleine piscine, que j'ai voulu voir ce qu'il y avait sous le C. Pas en lire la théorie : le construire. En assembleur, parce que l'assembleur ne laisse rien se cacher.

C'est devenu un dépôt : learn-assembly-with-em. Sa signature en dit long sur l'état d'esprit : « Drowning in C? Dive deeper — after assembly, C feels like floating. » La piscine t'apprend à nager ; ce dépôt, c'est le fond du bassin.

LA RÈGLE DU JEU
x86-64, Linux, NASM. Pas de libc, pas d'appel externe : syscall ou rien. Chaque programme réimplémente depuis zéro quelque chose que l'on prend d'habitude pour acquis, justement pour comprendre ce qui se passe réellement.

Le dépôt monte en difficulté. D'abord l'échauffement : hello world, une factorielle récursive, un quicksort — puis, toujours classées « échauffement », les commandes cat, wc, ls et grep, réécrites à la main et vérifiées octet pour octet contre leurs équivalents GNU. Ensuite, les choses sérieuses : un printf variadique dont la sortie est identique à celle de la glibc, un malloc avec ses listes libres et son alignement, un shell avec fork, execve, pipes et redirections, un serveur HTTP en syscalls bruts, un SHA-256 conforme au bit près à sha256sum, une fractale de Mandelbrot vectorisée en AVX2 (huit pixels calculés par instruction), et un Tetris jouable dans le terminal.

Puis viennent les « boss fights » — les morceaux déraisonnables. Un interpréteur Forth. Un assembleur x86-64 écrit en assembleur, dont la sortie est identique à celle de NASM. Et celui dont je veux parler ici, parce qu'il quitte complètement le confort de Linux : un bootloader.

QUITTER LE SYSTÈME D'EXPLOITATION
Tout ce qui précède tournait au-dessus de Linux : le noyau lançait le programme, lui fournissait ses appels système. Un bootloader, c'est autre chose. C'est le tout premier code qu'exécute une machine à l'allumage, avant qu'un système d'exploitation existe.

Quand un PC démarre, le firmware lit les 512 premiers octets du disque de démarrage, les charge en mémoire à l'adresse 0x7C00, et leur donne la main — le tout en mode réel 16 bits, l'antique mode de compatibilité dans lequel se réveille le processeur. Sans OS, pas d'appel système pour afficher du texte : on s'adresse directement au BIOS via ses interruptions. Et pour que le firmware accepte de démarrer ces 512 octets, les deux derniers doivent contenir une signature magique : 0xAA55.

Voici le bootloader complet. Il tient dans un secteur de disque, affiche un message, puis s'arrête :
 BITS 16 ; le CPU démarre en mode réel 16 bits
 ORG 0x7C00 ; le BIOS nous charge ici

 start:
 cli ; on coupe les interruptions le temps de s'installer
 xor ax, ax
 mov ds, ax ; segments à zéro
 mov es, ax
 mov ss, ax
 mov sp, 0x7C00 ; la pile, juste sous notre code
 sti

 mov si, msg
 call print_string
 .hang:
 hlt ; plus rien à faire : on met le CPU en pause
 jmp .hang

 ; affiche la chaîne terminée par zéro pointée par ds:si
 print_string:
 mov ah, 0x0E ; fonction BIOS 0x0E : afficher un caractère
 .next:
 lodsb ; charge [si] dans al, avance si
 test al, al ; caractère nul ? (fin de chaîne)
 jz .done
 int 0x10 ; interruption vidéo du BIOS : affiche al
 jmp .next
 .done:
 ret

 msg: db "hello from em's bootloader!", 13, 10, 0

 times 510 - ($ - $$) db 0 ; remplissage jusqu'à 510 octets
 dw 0xAA55 ; la signature de démarrage

On l'assemble en binaire brut — pas en ELF, puisque ce n'est pas un programme Linux mais des octets que le firmware lit directement — et on le teste dans un émulateur, sans jamais toucher à une vraie machine :

 nasm -f bin boot.asm -o boot.bin
 qemu-system-x86_64 -drive format=raw,file=boot.bin -nographic

Le message s'affiche. Le processeur (émulé) exécute ce que j'ai écrit, sans aucune couche entre mon code et lui. C'est un des points les plus bas où l'on puisse descendre, et le voir fonctionner à quelque chose de vertigineux : quelques dizaines d'octets, le BIOS, et le silicium.

CE QUE ÇA APPREND
Réécrire tout cela ne sert évidemment à rien « en production ». Ce n'est pas le but. Le but, c'est qu'après avoir écrit son propre printf, on ne regarde plus jamais une chaîne de format de la même façon ; qu'après avoir construit un allocateur, on comprend enfin ce qu'un pointeur est ; qu'après avoir parlé au BIOS, la frontière entre logiciel et matériel devient tangible. L'assembleur ne pardonne rien et n'explique rien : c'est précisément pour ça qu'il apprend tant. Et quand on remonte ensuite vers le C, il paraît soudain confortable — on flotte.

Le dépôt est ouvert, sous licence MIT : https://github.com/whispem/learn-assembly-with-em