GRUB, installation sur le MBR et démarrage QEMU
Technologie

Fabriquer son propre Linux (3) : initramfs, GRUB et premier démarrage

Un initramfs cpio écrit à la main, un GRUB installé sur le MBR d'une image disque, un démarrage QEMU en console série — et un « Hello World » qui valide toute la chaîne d'un seul coup.

Le disque contient un noyau, un rootfs et un PID 1. Reste à fabriquer l'initramfs, à installer un chargeur de démarrage — puis à appuyer sur le bouton et regarder la chaîne complète se dérouler sous les yeux.

Un initramfs écrit à la main

Les distributions génèrent le leur avec des outils qui embarquent des centaines de modules « au cas où ». Ici, on l'écrit : une arborescence minimale, le BusyBox statique, et un script /init d'une trentaine de lignes qui fait exactement quatre choses :

  1. monter /proc, /sys et /dev ;
  2. attendre l'apparition du disque — quelques secondes au plus, car le périphérique VirtIO n'est pas toujours prêt à la milliseconde où le script démarre. Cette attente est le genre de détail qui distingue un initramfs qui fonctionne d'un initramfs qui fonctionne une fois sur deux ;
  3. monter le vrai système de fichiers racine sur /new_root, avec une vérification de cohérence — le fichier /init attendu est-il bien là ? — avant d'aller plus loin ;
  4. appeler switch_root.

L'archive est ensuite produite au format cpio compressé, le seul que le noyau sache lire nativement :

find . | cpio -H newc -o | gzip -9 > ../out/initramfs.img

Le format newc n'est pas facultatif : c'est celui que le noyau attend. Quelques centaines de kilo-octets suffisent — à comparer aux dizaines de mégaoctets d'un initramfs de distribution.

GRUB : une configuration entièrement explicite

Configuration GRUB, installation sur le MBR et démarrage sous QEMU

Un menu sans détection automatique, un GRUB installé sur le secteur 0 de l'image, et une VM qui démarre en console série.

set timeout=0
set default=0
menuentry "Rocket Linux Demo (final)" {
    set root=(hd0,msdos1)
    linux /boot/vmlinuz console=ttyS0 root=/dev/vda1 ro loglevel=7
    initrd /boot/initramfs.img
}

Chaque ligne est volontairement explicite — aucune détection automatique, ce qui est exactement ce qu'on veut pour comprendre :

  • set root=(hd0,msdos1) — premier disque BIOS, première partition MBR. On désigne, on ne devine pas ;
  • console=ttyS0 — la sortie va sur la console série, donc dans le terminal qui a lancé QEMU. Cela n'a de sens que parce que le support série a été compilé en dur dans le noyau ;
  • root=/dev/vda1 — le paramètre que lira l'initramfs pour savoir quoi monter ;
  • ro puis loglevel=7 — racine montée en lecture seule au départ, et journalisation maximale : en phase de mise au point, on veut tout voir.
sudo grub-install --target=i386-pc --boot-directory=mnt/boot /dev/nbd0

La cible i386-pc désigne le GRUB pour BIOS — pas l'UEFI. C'est cette commande qui écrit l'étage 1 dans les 446 octets du MBR et installe les étages suivants. La boucle est bouclée avec l'article sur la chaîne de démarrage.

Fermer proprement

sync
sudo umount mnt
sudo qemu-nbd --disconnect /dev/nbd0

Trois commandes, dans cet ordre, et aucune n'est facultative. Le sync force l'écriture effective des données encore en cache ; le démontage libère le système de fichiers ; la déconnexion NBD rend l'image cohérente. Démarrer une image encore attachée en NBD, c'est la corrompre.

Le premier démarrage

qemu-system-x86_64 -enable-kvm -m 256M -nographic \
  -drive file=out/disk.qcow2,format=qcow2,if=virtio \
  -netdev user,id=n1,hostfwd=tcp::8080-:80 \
  -device virtio-net-pci,netdev=n1

Les options qui comptent : -nographic renvoie la console série dans le terminal — cohérent avec le console=ttyS0 passé au noyau ; if=virtio donne le /dev/vda attendu ; et hostfwd=tcp::8080-:80 redirige le port 8080 de l'hôte vers le port 80 de la machine.

Le déroulé s'affiche alors intégralement : GRUB, la décompression du noyau, l'initramfs, le switch_root, puis le script /init qui obtient une adresse en DHCP et démarre le serveur. 256 Mo de mémoire suffisent largement, et le démarrage se compte en secondes.

La preuve, depuis l'hôte

curl --max-time 3 http://127.0.0.1:8080/
Hello World - Rocket Linux Demo

Cette ligne valide toute la chaîne d'un coup : le MBR, GRUB, un noyau compilé à la main, un initramfs écrit à la main, un switch_root, un PID 1 maison, un client DHCP et un serveur HTTP — le tout dans une image de quelques dizaines de mégaoctets.

Ce que l'exercice apprend

Il ne s'agit pas de remplacer une distribution : personne ne maintiendra un système pareil en production. L'intérêt est ailleurs.

  • On comprend enfin pourquoi l'initramfs existe — en s'en passant, puis en en écrivant un.
  • On sait ce que fait réellement PID 1, et pourquoi il n'a pas le droit de se terminer.
  • On mesure ce qu'apporte une distribution : gestion de paquets, mises à jour de sécurité, pilotes universels, init robuste — tout ce qu'on n'a pas ici.
  • Et devant un serveur qui ne démarre plus, on sait à quel étage regarder : la question « GRUB s'affiche-t-il ? » ou « voit-on l'invite initramfs ? » divise immédiatement le problème en deux.

C'est là tout l'intérêt d'un lab : passer une soirée sur un système qui ne sert à rien, pour ne plus jamais être perdu devant un système qui sert à quelque chose.

La page de référence est sur le wiki

Cet article ne remplace pas la documentation : il en reprend le fil et en explique les raisons. Le détail complet — toutes les commandes, tous les scripts, les options de configuration une par une — est publié sur mon wiki :

La série

Deux articles pour comprendre ce qui se passe au démarrage, trois pour fabriquer un système Linux complet à la main :

  1. Du bouton Power au noyau : la chaîne de démarrage x86
  2. initramfs et switch_root : le sas de démarrage
  3. Fabriquer son Linux (1) : noyau et BusyBox
  4. Fabriquer son Linux (2) : disque, rootfs et PID 1
  5. Fabriquer son Linux (3) : initramfs, GRUB et premier boot — vous y êtes
Retour au blog