Du firmware au noyau : explication du processus de démarrage
Tout système d’exploitation commence avant d’être vraiment lui-même. Le CPU démarre dans un environnement défini par la plateforme, le firmware initialise assez de matériel pour charger le premier exécutable, et cet exécutable prépare la machine pour le noyau. Ce n’est qu’après le travail de cette chaîne que le système d’exploitation peut commencer à appliquer ses propres règles.
Il est facile de traiter ce chemin initial comme de la simple plomberie, mais le processus de démarrage fait partie du modèle de sécurité. Pour EriX, le boot est le premier endroit où des octets non fiables deviennent une exécution fiable, et c’est aussi le premier endroit où l’autorité de la machine est traduite en structure explicite : images vérifiées, cartes mémoire, descripteurs de modules, adresses d’entrée, métadonnées de framebuffer, pointeurs ACPI et, à terme, objets du noyau.
Si ce chemin est négligé, le système de capacités commence sur un mensonge. Cet article parcourt le chemin de démarrage d’EriX du firmware au noyau : ce que fournit UEFI, ce que le bootloader doit faire, ce que contient le handoff, et pourquoi le noyau est entré comme un exécutable de moitié haute.
Le boot commence avec l’autorité du firmware⌗
Sur les cibles actuelles d’EriX, le chemin de démarrage commence avec UEFI
sur x86_64. UEFI est du firmware, et il s’exécute avant le système
d’exploitation. Il fournit les services de boot qui permettent à une application
EFI de lire des fichiers, d’allouer de la mémoire, d’inspecter la carte mémoire
de la plateforme, de découvrir des tables de configuration comme ACPI,
d’interroger la sortie graphique via GOP et de quitter les services de boot
avant que le noyau prenne le contrôle.
Le bootloader d’EriX est construit comme une application UEFI, donc le firmware
charge d’abord le bootloader et appelle son point d’entrée UEFI. Dans
l’implémentation, ce point d’entrée est efi_main, qui utilise la convention
d’appel UEFI x86_64. À ce moment-là, EriX ne contrôle pas encore la machine ; il
s’exécute dans un environnement fourni par le firmware.
Le bootloader peut demander à UEFI de lire un fichier, d’allouer de la mémoire, d’inspecter des tables du firmware et de préparer des tables de pages, mais toute cette autorité est temporaire. Les services de boot UEFI ne sont pas le système d’exploitation. Ce sont des échafaudages que le bootloader doit utiliser avec soin, résumer en faits explicites, puis laisser derrière lui.
Le bootloader est du code de confiance⌗
Le bootloader s’exécute avant que le noyau puisse appliquer quoi que ce soit, ce qui le place dans la base de calcul de confiance. Si le bootloader charge les mauvais octets, saute à la mauvaise adresse, accepte une image altérée, étiquette mal la mémoire ou invente une autorité qui ne provient pas d’une entrée vérifiée, le noyau démarre sur une fondation compromise. Pour que ce risque reste auditable, EriX garde le travail du bootloader étroit et explicite :
- localiser et charger
boot.img - valider l’image avant de lui faire confiance
- valider le dynamic boot store
- relocaliser le noyau dynamique et ses objets précoces requis
- construire une structure de handoff déterministe
- préparer les tables de pages et une pile de bootstrap
- quitter les services de boot UEFI
- sauter une seule fois vers le noyau
Ce n’est volontairement pas un environnement de boot généraliste. Il n’y a pas de politique de menu de démarrage dans le périmètre actuel, pas de chemin de récupération distant, et aucune tentative de continuer après une entrée malformée critique pour le boot. La règle est de vérifier avant d’exécuter : si l’image de boot ne peut pas être parsée, vérifiée, chargée, mappée ou décrite de façon cohérente, la tentative de démarrage échoue avant que le noyau reçoive le contrôle.
Charger boot.img⌗
Le chemin UEFI actuel cherche ce fichier sur le volume de boot. Le chemin est fixe pour le profil actuel, ce qui garde le traitement précoce du média étroit et évite de transformer le boot en problème de découverte de politique :
\ERIX\BOOT.IMG
Le bootloader ouvre le volume de boot via les protocoles de fichiers UEFI, lit
le fichier dans de la mémoire allouée par UEFI, et effectue une première
vérification peu coûteuse du magic de conteneur ERIXBOOT avant un parsing plus
profond. Ensuite, lib-bootimg prend le relais : le bootloader parse l’image
comme un BootImage, vérifie sa signature et ses hashes, vérifie l’architecture
du manifeste, puis seulement commence à extraire les payloads.
Cette séparation compte parce que le média de boot n’est pas fiable. Le fichier
peut être absent, tronqué, malformé ou altéré, et le bootloader ne peut pas
traiter un descripteur de section comme vrai simplement parce qu’il vient du
disque. Dans EriX, le parseur et vérificateur de boot.img font partie du
chemin de boot de confiance, et ils doivent rejeter une mauvaise structure, de
mauvaises bornes, de mauvais hashes, des versions non prises en charge et des
données d’architecture incompatibles avant qu’un octet chargé devienne
exécutable.
Les prochains articles iront plus loin dans le format boot.img et la
vérification d’image. Pour cet article, le point important est l’ordre :
- lire les octets
- parser la structure
- vérifier l’intégrité
- vérifier la compatibilité de la cible
- valider et relocaliser les artefacts de boot dynamiques
- construire les métadonnées de handoff
L’exécution vient après la validation, pas avant. Cet ordre est la différence entre traiter l’image de boot comme une entrée et la traiter comme une autorité.
Charger le noyau⌗
Une fois boot.img accepté, le bootloader a besoin d’un noyau, et dans le
chemin de boot d’EriX le noyau est chargé depuis le dynamic boot store signé. Le
bootloader valide le noyau dynamique comme un objet ELF64 ET_DYN, vérifie les
métadonnées de dynamic-link d’EriX, vérifie les noms de dépendances et les hashes
d’objets contre les métadonnées signées, charge les objets partagés requis,
applique les relocalisations approuvées, impose les contraintes W^X et prépare
un catalogue de boot dynamique pour le noyau.
Cela paraît beaucoup parce que c’est beaucoup, mais la forme de sécurité reste directe et révisable :
- les données viennent d’une image de boot vérifiée
- le parsing du format exécutable est explicite
- les plages de segments sont vérifiées
- les effets de bord des relocalisations sont contraints
- les métadonnées dynamiques manquantes ou incohérentes échouent avant l’entrée du noyau
Le bootloader existe pour valider et relocaliser le noyau dynamique, puis décrire au noyau le graphe d’objets précoces vérifié. Il ne devient pas un éditeur de liens dynamique général pour le système en cours d’exécution ; ce rôle appartient à plus tard, une fois que le noyau et le modèle de services en espace utilisateur existent.
Préserver les métadonnées de boot⌗
Le noyau dynamique n’est pas la seule chose dans l’image de boot. Le bootloader préserve aussi les métadonnées de boot non exécutables requises par le noyau et le système précoce en espace utilisateur, tandis que les services exécutables appartiennent au graphe d’objets dynamique. Ces artefacts exécutables sont décrits par le catalogue dynamique comme des objets, des segments et des arêtes de dépendance dérivés du store signé et du manifeste signé.
D’autres sections requises sont des blobs non exécutables. Les exemples incluent la configuration de boot, les stores de métadonnées de dynamic-link et les données de police de console. Ils sont copiés dans de la mémoire mappée et décrits comme des modules en lecture seule sans point d’entrée, parce qu’un blob n’est pas exécutable simplement parce qu’il apparaît dans l’image de boot.
Cette distinction est importante pour la conception à capacités. Un payload de configuration de boot doit être lisible par le système précoce, mais il ne doit pas être traité comme du code. Un blob de police peut être nécessaire pour la continuité du framebuffer, mais il n’a pas besoin d’autorité d’exécution. EriX préserve cette distinction dans le handoff :
- descripteurs d’objets dynamiques pour les artefacts exécutables
- descripteurs de segments dynamiques pour les plages exécutables et de données mappées
- descripteurs de dépendances dynamiques pour les relations d’objets précoces
SECTION_TYPE_BOOT_CONFIGpour les données de politique de boot- descripteurs de modules pour les blobs de boot non exécutables
Le handoff transporte cette différence vers l’avant afin que le noyau et rootd
n’aient pas à deviner si une donnée de boot est une autorité exécutable, une
configuration en lecture seule ou une métadonnée ordinaire.
Le handoff est le contrat⌗
Le bootloader n’appelle pas une API du noyau, parce qu’il n’y a pas encore de
noyau en cours d’exécution. À la place, le bootloader construit un blob de
handoff : un contrat binaire versionné défini par lib-handoff. Dans le profil
actuel bootloader-vers-noyau, il commence par le magic ERIXHK01, les champs de
version majeure et mineure, la taille totale, les identifiants d’architecture et
de plateforme, puis les offsets et les compteurs pour les tables qui suivent.
Le handoff peut inclure plusieurs classes de données dont le noyau a besoin avant de pouvoir construire sa propre vue runtime de la machine :
- entrées de carte mémoire normalisées
- descripteurs de modules chargés
- pointeur ACPI RSDP
- build ID et hash d’image vérifiés
- métadonnées de continuité du framebuffer
- descripteurs d’objets dynamiques
- descripteurs de segments dynamiques
- arêtes de dépendance dynamiques
C’est le premier transfert structuré d’autorité. Le firmware a donné au bootloader des faits bruts et des services temporaires, et l’image de boot vérifiée lui a donné des métadonnées de payload signées. Le bootloader combine tout cela en une description déterministe de ce qu’il a chargé, de l’endroit où il l’a placé et de ce que le noyau peut croire.
Le handoff n’est pas une indication ni une métadonnée “best effort”. C’est l’entrée à partir de laquelle le noyau commence à construire sa propre vision de la machine, et c’est pourquoi il porte des compteurs, des tailles d’entrée, des offsets, des hashes, des types, des plages et des champs de version. Il doit être possible pour le noyau de le rejeter.
Les cartes mémoire doivent être normalisées⌗
Les cartes mémoire du firmware ne sont pas automatiquement formées pour la politique du noyau. UEFI rapporte des régions avec des types mémoire de firmware, tandis que le bootloader connaît aussi la mémoire qu’il a allouée pour le noyau dynamique, les mappages d’objets précoces, les blobs de boot requis, la pile de bootstrap, les pages de handoff, les mappages de framebuffer et l’image de boot originale. Ces vues doivent être fusionnées avant que le noyau puisse raisonner sur la mémoire disponible.
Dans EriX, le bootloader prend un instantané de la carte mémoire UEFI, ajoute les régions explicitement possédées par le boot, et normalise le résultat en plages non superposées avec des types mémoire EriX comme :
- RAM utilisable
- mémoire réservée
- mémoire ACPI reclaimable
- mémoire ACPI NVS
- mémoire MMIO/périphérique
- mémoire possédée par le bootloader
- mémoire possédée par l’image de boot
Les régions explicitement possédées par le boot gagnent sur les classifications génériques du firmware. Cela compte parce que le noyau ne doit pas traiter par accident la mémoire qui contient l’image de boot, le blob de handoff, les mappages d’objets dynamiques, les blobs de boot requis ou la pile de bootstrap comme de la RAM libre ordinaire. La mémoire est une autorité dans EriX, donc le système fait attention dès ce stade à qui peut réutiliser quels octets.
Laisser UEFI derrière soi⌗
Les services de boot UEFI sont utiles, mais temporaires. Avant de sauter vers le
noyau, le bootloader appelle ExitBootServices, ce qui est en pratique une
transition à sens unique. Après une sortie réussie, le bootloader doit traiter
les pointeurs de services de boot comme invalides ; il ne peut plus demander au
firmware d’allouer de la mémoire ou de lire des fichiers après la prise de
contrôle par le système d’exploitation.
Cette transition est délicate parce qu’UEFI exige que le bootloader sorte avec une clé de carte mémoire courante. Si la carte change entre l’instantané et la sortie, l’appel peut échouer et le loader doit réessayer avec une carte fraîche. L’adaptateur UEFI d’EriX gère cette boucle de réessai dans la couche plateforme.
Le point de conception important est que le noyau est entré après que le bootloader a fini d’utiliser les services du firmware. Le noyau ne doit pas hériter d’une dépendance firmware à moitié ouverte ; il doit hériter de données explicites.
Construire les premières tables de pages⌗
Le noyau est entré avec la pagination déjà active. Pour le profil UEFI
x86_64 actuel, le bootloader construit des tables de pages minimales avec deux
types de mappages : une région de mémoire basse mappée à l’identique pour le
bring-up précoce, et des mappages spécifiques de moitié haute pour les objets
que le noyau utilisera immédiatement.
L’implémentation actuelle mappe à l’identique le premier 1 GiB avec des pages de 2 MiB et mappe aussi à l’identique les fenêtres MMIO d’APIC. Elle mappe ensuite des plages virtuelles de moitié haute pour le noyau, les objets dynamiques chargés, les blobs de boot requis, le blob de handoff, le framebuffer et la pile de bootstrap. Cela suffit pour que le noyau commence dans l’espace d’adressage qu’il attend.
Les tables de pages ne sont pas le système final de mémoire virtuelle. Elles sont un pont qui permet au noyau de s’exécuter, de valider le handoff, d’installer l’état CPU précoce et de commencer à construire le véritable environnement noyau et espace utilisateur. Le pont doit tout de même être correct : si la page d’entrée du noyau n’est pas mappée, la machine faute immédiatement ; si le blob de handoff est mappé à la mauvaise adresse virtuelle, le noyau lit des absurdités ; si la pile manque, l’entrée échoue avant que le code Rust puisse faire grand-chose.
Pourquoi un noyau en moitié haute ?⌗
EriX entre dans le noyau dans la moitié haute de l’espace d’adressage virtuel. Cela signifie que le noyau s’exécute à des adresses virtuelles hautes au lieu d’être lié et exécuté seulement dans la plage basse mappée à l’identique. C’est une conception courante de noyau parce qu’elle donne au noyau une région d’adresses virtuelles stable, indépendante de l’endroit où la mémoire physique a été allouée.
Le layout de moitié haute sépare aussi l’espace virtuel du noyau des plages ordinaires de l’espace utilisateur et permet au noyau de garder ses propres mappages présents à travers les changements d’espace d’adressage futurs, tandis que les mappages utilisateur peuvent varier par tâche. Le layout d’adresses n’impose pas à lui seul tout le modèle de sécurité, mais il soutient la frontière en rendant la mémoire du noyau distincte de la mémoire normale d’un processus.
Dans EriX, le bootloader est responsable de rendre possible l’entrée initiale en
moitié haute. Il charge le noyau selon les adresses virtuelles ELF, mappe ces
adresses virtuelles vers des pages physiques allouées, crée une pile de
bootstrap en moitié haute, mappe le blob de handoff à une adresse connue de
moitié haute, charge cr3 et saute au point d’entrée du noyau.
Au saut, le contrat x86_64 actuel est volontairement petit et explicite. Le bootloader fournit seulement l’état dont le noyau a besoin pour commencer à valider le handoff et installer son propre état CPU précoce :
- ABI SysV
rdicontient le pointeur de handoffrsppointe vers la pile de bootstrap avec l’alignement attendu- la pagination est active
- le contrôle ne revient pas
Cette ABI est petite par conception. Moins l’entrée du noyau dépend d’état implicite, plus la frontière est facile à auditer.
Entrer dans le noyau⌗
Côté noyau, l’entrée commence avant que le runtime complet du noyau existe. Le
noyau dynamique expose erix_dynlink_entry, qui entre dans le chemin précoce du
noyau avec le pointeur de handoff, et le premier travail est défensif plutôt que
chargé de politique.
Le noyau désactive les interruptions, initialise la sortie série précoce,
vérifie que le pointeur de handoff n’est pas nul, lit la taille du handoff depuis
l’en-tête, impose une taille maximale de handoff, puis valide le blob complet
via lib-handoff. Après la validation structurelle, le noyau applique ses
propres vérifications de politique :
- l’architecture doit être
x86_64 - la plateforme doit être UEFI
- le build ID ne doit pas être vide
- les tables du catalogue dynamique doivent être cohérentes en interne
- les noms d’objets dynamiques, hashes, segments, dépendances et plages du store doivent être validés avant utilisation
Le bootloader a déjà construit le handoff, mais le noyau le valide quand même. Les composants de confiance ne peuvent pas ignorer les contrats simplement parce qu’un autre composant de confiance a produit les données. L’intérêt d’un handoff versionné est que les deux côtés puissent s’accorder exactement sur ce qui a été transféré.
Ce n’est qu’après cela que le noyau avance plus loin dans l’initialisation précoce : installation de la GDT, installation de l’IDT, configuration du chemin syscall, initialisation optionnelle de la console précoce, et finalement orchestration du bootstrap pour la première tâche root. Le noyau devient le noyau progressivement, et la première chose qu’il fait est de vérifier le sol sous ses pieds.
Le boot est une traduction d’autorité⌗
Il est tentant de décrire le boot comme “charger le noyau et sauter”. C’est techniquement vrai, mais cela manque le point de conception du système d’exploitation. Pour EriX, le boot est une traduction d’autorité : l’autorité du firmware devient des faits de boot explicites, les octets de l’image de boot deviennent des sections vérifiées, les fichiers ELF deviennent des plages exécutables mappées, les blobs deviennent des descripteurs de modules non exécutables, les cartes mémoire du firmware deviennent des régions mémoire normalisées, les métadonnées de dynamic-link deviennent un graphe d’objets borné et l’état du framebuffer devient des métadonnées de continuité.
- Tout cela devient un blob de handoff, et le noyau reçoit ce blob et décide s’il
- est acceptable. Ce n’est qu’alors qu’il peut commencer à transformer les
- ressources de la machine en objets du noyau, capacités, espaces d’adressage,
- endpoints et première tâche en espace utilisateur. C’est pourquoi le processus
- de boot appartient à une discussion sur les systèmes d’exploitation à capacités
- le modèle de capacités ne commence pas après le boot comme un ajout ; il dépend du fait que le boot n’introduise pas d’autorité ambiante.
Le bootloader ne doit pas simplement dire qu’il a chargé certaines choses. Il doit dire exactement ce qu’il a chargé, où cela se trouve, ce que c’est, comment cela a été vérifié et quels faits de plateforme il a observés. C’est la différence entre un saut et un handoff.
Ce qu’EriX garde hors du bootloader⌗
Le bootloader est puissant parce qu’il s’exécute tôt, et c’est précisément pour cela qu’il doit rester petit. EriX ne veut pas que le bootloader décide de la politique runtime : il ne doit pas décider quel service possède la politique mémoire, comment les processus sont supervisés, comment les pilotes sont gérés, ou comment les systèmes de fichiers sont composés.
Ces décisions appartiennent au noyau et aux services système en espace utilisateur. Le travail du bootloader est plus étroit : valider l’artefact de boot, préparer l’environnement minimal d’exécution, décrire ce qu’il a fait et transférer le contrôle.
Cette ligne compte pour la taille du TCB. Un bootloader avec plus de fonctionnalités n’est pas automatiquement meilleur, parce que chaque fonctionnalité en boot précoce est du code qui s’exécute avant que le noyau puisse l’isoler. Chaque parseur, chemin de fallback, mode interactif et exception de politique augmente la quantité de comportement de confiance qui doit être correct avant que le système démarre.
Suite⌗
Cet article a surtout traité boot.img comme un conteneur vérifié. La prochaine
étape consiste à ouvrir ce conteneur et à examiner directement sa conception.
Le prochain article expliquera le format boot.img d’EriX : pourquoi le système
utilise une image unifiée, comment les sections sont disposées, quelles
métadonnées sont transportées, et comment le format soutient des artefacts de
boot reproductibles et déterministes.