- À partir d’un secteur d’amorçage de 512 octets, l’article construit étape par étape le flux minimal d’un chargeur d’amorçage qui fait passer un CPU x86_64 du real mode 16 bits au long mode 64 bits
- Il commence par vérifier l’affichage via les interruptions BIOS, puis relie
nasm,ld,objcopyet QEMU pour valider que l’image de boot s’exécute réellement - À cause de la limite de taille du secteur d’amorçage, le code est séparé en stage 1 / stage 2, et le code suivant est lu depuis le disque via le BIOS
int 0x13avant l’entrée en protected mode - À partir du protected mode 32 bits, les routines BIOS ne peuvent plus être utilisées, ce qui impose une initialisation matérielle comme la GDT, la segmentation plate et l’écriture directe dans le buffer VGA
- L’entrée en long mode 64 bits exige d’aligner les tables de pages, le PAE,
EFER.LME,cr0.PGet une GDT 64 bits, après quoi il devient possible d’appeler du code C freestanding comme un noyau
Environnement 16 bits au démarrage via le BIOS
- Après un reset, un CPU x86 est en real mode et la taille d’opérande par défaut est de 16 bits
- Le real mode utilise la segmentation pour créer un espace d’adressage de 20 bits et peut gérer jusqu’à 1 Mo de mémoire
- Le premier code exécuté par le BIOS se trouve dans le secteur d’amorçage du disque
- Le BIOS cherche un disque dont le premier secteur se termine par le nombre magique
0xaa55 - Il charge ce secteur à l’adresse mémoire
0x7c00
- Le BIOS cherche un disque dont le premier secteur se termine par le nombre magique
- Comme le BIOS ne fournit que 512 octets, ce code se concentre sur son rôle de bootstrap chargé de récupérer le reste du chargeur d’amorçage
- Les routines BIOS ne peuvent être utilisées que tant qu’on reste en real mode
Pré requis et environnement de build
- Pour suivre l’article, il faut Intel 64 and IA-32 Architectures Software Developer’s Manual, un assembleur et QEMU
- L’assembleur utilisé dans l’exemple est nasm, et le texte suppose une connaissance de l’assembleur x86 et de la syntaxe nasm
- Même sans CPU x86_64, il est possible d’exécuter l’exemple en émulant un CPU x86 dans QEMU
- Comme ressource de base jusqu’au mode 32 bits, l’article recommande Writing a Simple Operating System — from Scratch
Créer un secteur d’amorçage et vérifier son exécution
- Le premier secteur d’amorçage affiche
"Hello, world!"via une routine BIOS, puis s’arrête avechltet une boucle - Pour afficher la chaîne, il utilise l’appel BIOS de services vidéo
int 0x10avecah = 0x0e - Le
Makefilegénère l’objet avec nasm, effectue l’édition de liens avec un linker script, puis produit une image de boot brute viaobjcopy -O binary make bootexécute l’image dans QEMUqemu-system-x86_64 -no-reboot -drive file=$<,format=raw,index=0,media=disk
- Le linker script configure le placement du secteur d’amorçage à partir de
0x7c00- L’origine de
boot_sectordansMEMORYest0x7c00et sa taille est512 - La section
.bootsignajoute0x55,0xaaà l’adresse0x7c00 + 510
- L’origine de
- Il serait possible de gérer directement l’offset et le nombre magique dans l’assembleur du secteur d’amorçage, mais ici c’est le linker script qui s’en charge
Le stage 1 charge le stage 2 depuis le disque
- Le stage 1 est le code du secteur d’amorçage chargé par le BIOS, et son objectif est de charger le stage 2 en mémoire
- Le stage 2 contient le code qui fait passer du real mode 16 bits au protected mode 32 bits
- Une fois en protected mode, les routines BIOS ne sont plus disponibles ; la lecture des secteurs disque doit donc être terminée avant la transition
- L’accès au disque utilise les services disque du BIOS
int 0x13ah = 0x42correspond à la fonction BIOS extended readdl = 0x80est le numéro du lecteur- Le disk address packet contient le nombre de secteurs à lire, l’adresse cible et le secteur de départ
- L’exemple lit 64 secteurs avec
READ_SECTORS_NUM equ 64- Comme le secteur d’amorçage est le secteur 0, le stage 2 est lu à partir du secteur 1
- L’adresse cible est
BOOT_LOAD_ADDR + SECTOR_SIZE, soit0x7c00 + 512
- Le code contient encore un traitement provisoire qui accepte qu’un nombre de secteurs inférieur à la demande soit lu
- Au début, le stage 2 recopie
print_stringpour le real mode afin d’afficher"Hello from stage 2"et vérifier que le saut depuis le stage 1 s’enchaîne correctement
Passer au protected mode 32 bits
- Pour entrer en protected mode, il faut d’abord définir une Global Descriptor Table (GDT)
- En protected mode, la segmentation sert fondamentalement à la protection mémoire
- Le long mode 64 bits nécessite la pagination, mais l’entrée préalable en protected mode demande d’abord une configuration de la segmentation
- La GDT d’exemple suit le modèle plat du manuel Intel
- Elle comporte un segment de code et un segment de données
- Ces deux segments couvrent tout l’espace d’adresses linéaire
- Comme il s’agit d’une étape intermédiaire vers le long mode, le modèle le plus simple est utilisé
- La GDT est une structure continue placée en mémoire
- Elle commence par un descripteur nul pour intercepter les traductions invalides
- Elle est suivie d’un descripteur de segment de code puis d’un descripteur de segment de données
- La transition suit l’ordre suivant
clidésactive les interruptionslgdt [gdt32_pseudo_descriptor]charge l’adresse et la taille de la GDT dans le GDTRcr0.PE, c’est-à-dire le bit 0 decr0, active le protected mode- Un far jump vide le pipeline d’instructions et met à jour
csavec le nouveau segment de code
- Après l’entrée en protected mode, les anciennes valeurs de segment n’ont plus de sens ;
ds,ss,es,fs,gssont donc configurés avec le nouveau sélecteur du segment de données - Pour réactiver les interruptions ensuite, du travail supplémentaire est nécessaire
Afficher à l’écran sans le BIOS
- En protected mode, il n’est plus possible d’appeler les routines BIOS
- L’affichage de chaînes passe donc par l’écriture directe dans le buffer VGA
print_string32écrit des octets de caractère et de couleur à l’adresse0xb8000- La valeur de couleur est
0xf - Chaque cellule de caractère utilise 2 octets
- La valeur de couleur est
- Cette fonction d’affichage est très simple, si bien que les messages apparaissent toujours en haut à gauche de l’écran
Tables de pages pour entrer en long mode
- Le mode IA-32e de la documentation Intel correspond au long mode du manuel AMD64
- Pour passer en long mode, le CPU doit déjà être en protected mode et la pagination doit aussi être activée
- Pour les concepts de pagination, l’article renvoie à Introduction to Paging et à OSTEP
- En long mode avec PAE activé, on utilise des tables de pages à 4 niveaux
build_page_tableconstruit ces tables à l’adresse indiquée- La taille de page est
0x1000 - La taille de chaque table de pages est
0x1000 - Le nombre d’entrées est
512 - Les quatre tables sont d’abord initialisées à 0 pour que toutes les entrées soient marquées non présentes
- Les premières entrées de PML4 → PDP → PD → table de pages sont reliées entre elles
- Au niveau le plus bas, 512 entrées sont configurées
- La taille de page est
GDT 64 bits et ordre de transition vers le long mode
- Même si la pagination gère l’espace d’adresses virtuel et les permissions, une GDT reste nécessaire en long mode
- La GDT 64 bits suit aussi un modèle plat et ressemble beaucoup à celle du protected mode
- La différence tient aux bits liés au long mode
- Le drapeau de segment de code 64 bits est activé dans le segment de code
- Quand ce drapeau est activé, le bit de taille d’opération par défaut doit être à 0
- La transition vers le long mode suit alors le flux suivant
- Construction des tables de pages à 4 niveaux à l’adresse
0x1000 - Chargement de l’adresse de la table PML4 dans
cr3 - Activation du PAE en positionnant le bit 5 de
cr4 - Lecture du MSR
0xc0000080puis activation deEFER.LME, c’est-à-dire le bit 8 - Activation de la pagination en positionnant le drapeau
PG, bit 31 decr0 - Chargement de la GDT 64 bits avec
lgdt - Far jump vers le segment de code 64 bits pour entrer en mode 64 bits
- Construction des tables de pages à 4 niveaux à l’adresse
- Juste après l’activation de la pagination, le processeur est en mode de compatibilité IA-32e ; il passe ensuite en mode 64 bits lors du saut vers une GDT dont le drapeau de segment 64 bits est activé
- Le message confirmant la réussite s’affiche en haut à gauche de l’écran via le buffer VGA
Appeler du code C freestanding
- Une fois le long mode 64 bits atteint, il devient possible d’appeler du code C freestanding
kernel.cefface le buffer VGA0xb8000puis affiche"Hello from C"- Côté assembleur,
start_long_modeaffiche d’abord une chaîne en 64 bits, déclareextern _start_kernelpuis appelle_start_kernel - Le linker script découpe la mémoire en trois régions
boot_sector:0x7c00, taille512stage2:0x7e00, taille512kernel:0x8000, taille0x10000
- Les sections
.text,.data,.rodata,.bsssont placées dans la région kernel - Le
Makefileest modifié pour construire à la fois l’assembleur et le C- Le compilateur C est
gcc - Les principaux
CFLAGSsont-std=c99 -ffreestanding -m64 -mno-red-zone -fno-builtin -nostdinc -Wall -Wextra
- Le compilateur C est
- Le code complet de l’exemple est fourni via un lien de téléchargement
1 commentaires
Commentaires sur Hacker News
Il est possible d’entrer directement en long mode avec bien moins de code, sans passer par le mode protégé : https://wiki.osdev.org/Entering_Long_Mode_Directly
Il existait un petit bootloader pour un noyau 64 bits construit de cette manière, et même en incluant le code pour lire le noyau depuis le disque et configurer le mode VESA, tout tenait largement dans le secteur d’amorçage. Il n’y avait même pas besoin d’un chargeur de deuxième étape
Si l’on utilise réellement des disques modernes, il vaut mieux regarder GPT que MBR. Cela permet de dépasser les limites des tables de partitions et de gérer aussi les disques de plus de 2 To. UEFI supprime ce problème et permet d’utiliser une disposition de disque correcte sans difficulté particulière
Il n’est pas nécessaire de passer par le mode protégé pour entrer en mode 64 bits. En revanche, mieux vaut éviter d’utiliser le BIOS. C’est sale et cela ne fait que compliquer les choses
Il vaut mieux utiliser UEFI avec EDK2 ou GnuEFI, et les deux sont assez faciles et confortables à mettre en œuvre. Il faut un peu de temps pour se familiariser avec les concepts de base d’UEFI, mais en regardant les projets d’exemple sur GitHub, on peut vite comprendre la structure. EDK a ses fichiers .dec et .inf un peu pénibles, et avec GnuEFI il faut lire les fichiers d’en-tête pour trouver les fonctionnalités, mais c’est malgré tout bien mieux que les interfaces BIOS aux spécifications floues. Sur du vrai matériel, on ne peut même pas supposer que int 0x10, int 0x15, etc. soient correctement présents
Sur les systèmes UEFI, on peut supposer une base minimale stable, et les fonctions matérielles ou de plateforme peuvent aussi être énumérées de manière raisonnable. De plus, comme l’UEFI a déjà largement configuré la plateforme, le composant de chargement de l’OS n’a pas besoin d’effectuer beaucoup d’initialisation : il suffit de charger directement les éléments correspondant à la conception de l’OS, des pilotes et du noyau. Il suffit d’obtenir la carte mémoire, d’accéder au système de fichiers EFI et d’y lire ce dont on a besoin
Le 80286 possédait un registre 16 bits appelé Machine Status Word (MSW), et le 80386 l’a étendu en registre 32 bits CR0. Plus tard, le long mode 64 bits a ajouté l’EFER MSR et a étendu CR0 à 64 bits, mais aujourd’hui encore seuls 11 bits sont utilisés dans CR0, et EFER n’a que 8 bits actifs
Je me demande pourquoi Intel/AMD ont fait deux fois de nouveaux choix au lieu de simplement utiliser les bits libres dans les registres existants : https://wiki.osdev.org/CPU_Registers_x86-64#CR0
Le fait que CR1 et CR5 à CR7 soient encore réservés alors que CR8 a été ajouté semble relever de la même logique
Ce qui semble le plus inutilement complexe dans cet article, c’est le Makefile et le script de l’éditeur de liens. NASM prend en charge la sortie en binaire plat, mais apparemment cela a été jugé trop « hacky »
Dans le temps, mon OS avait un fichier nommé make.sh juste pour se moquer de ça. Maintenant, j’utilise des choses sophistiquées comme des « formats de fichier », donc
-fbinet--oformat=binarysont de lointains souvenirs. J’ai longtemps essayé de séparer les fichiers C de données et les fichiers C de code, de les dumper en binaire, puis d’assembler un monstre à partir de ça, mais l’édition de liens et le chargement deviennent trop difficiles. Mieux vaut simplement utiliser ELF ou PE, et c’est d’ailleurs ce à quoi ces formats semblent servirCela a l’air sympa et constitue un bon exercice, mais je ne sais pas si c’est utile. Je me demande s’il y a au moins une UX façon jouet Fisher-Price permettant de vérifier ou modifier la configuration pendant l’exécution
Le boot est le processus qui mène d’un mode minimaliste, mono-utilisateur ou de récupération, à un état opérationnel
J’utilise Unix avec des produits Microsoft depuis l’époque de Xenix/DOS, cela doit faire environ 40 ans. Je me demande quels progrès ont réellement été accomplis pendant ce temps
J’utilise aussi Linux depuis la version suédoise, c’est-à-dire sa toute première version, et j’ai même utilisé GNU 0.1
Je m’excuse d’avoir appelé Xenix un Unix. Xenix était un produit foutraque de type me-too qui, dès sa sortie et jusqu’à son déclin, semblait déjà vouloir appartenir au passé
Microsoft ressemble à une entreprise qui ne lance pas des produits, mais vide une litière pour chat sur ses clients. Les exemples récents sont Copilot et 22H2
Quand on voit l’évolution des F1, des crayons ou des calculatrices de poche, on se demande à quel point nous sommes proches d’un idéal réellement utilisable
Je me demande aussi pourquoi le bootloader n’est pas en mode noyau statique. C’était le cas autrefois, et quelqu’un a récemment proposé de revenir à cela, ce avec quoi j’étais d’accord
https://wiki.osdev.org/A20_Line
Il est surprenant de voir que toutes les étapes nécessaires pour faire passer le CPU dans le bon mode semblent totalement inutiles. On dirait que la plupart des procédures ne sont nécessaires qu’à cause de la rétrocompatibilité
Je me demande si Intel n’aurait pas pu fournir dès le départ un drapeau ou une instruction permettant de démarrer directement dans le bon mode, ou bien supprimer la rétrocompatibilité
Il me semble qu’ARM64 a aussi eu un peu ce genre de problème. Je me demande s’il existe un CPU conçu dès l’origine pour le 64 bits, sans besoin de rétrocompatibilité, et qui arrive par défaut dans l’état souhaité. C’était peut-être l’objectif ou la philosophie d’Itanium
Itanium, alias Itanic, a connu le même sort
La rétrocompatibilité est précisément la raison principale de choisir x86 plutôt qu’ARM, MIPS ou RISC-V. Malheureusement, certaines personnes chez Intel et AMD ne semblent pas bien le comprendre
Excellent projet. Si les partisans d’UEFI ici se demandent pourquoi quelqu’un irait créer une nouvelle méthode de bootloader, j’ai l’impression qu’ils passent à côté de la raison pour laquelle les gens font ce genre de travail
Comme l’auteur l’écrit à la fin, « si vous êtes arrivé jusque-là, c’est génial » — et oui, c’est vraiment génial
Je me demande depuis combien de temps UEFI existe. C’est dommage qu’ils n’aient pas aussi abandonné le BIOS avec l’arrivée du long mode
En voie d’abandon ne veut pas dire supprimé, mais qu’on ne le met plus à jour ni ne le développe davantage, avec pour objectif de le retirer
Je me demande si cette procédure de démarrage fonctionne aussi avec EFI/UEFI. Si oui, je me demande si le superviseur UEFI émule les transitions entre mode réel, mode protégé et long mode, ou si cela s’exécute sur le vrai matériel
J’ai décrit ici le processus de création d’un bootloader UEFI pour un OS amateur : https://0xc0ffee.netlify.app/osdev/05-bootloader-p1.html
Je me demande si c’est plus simple sur ARM