Processus de démarrage
Avant de passer à la pratique, il est nécessaire de savoir comment une machine passe de sa mise sous tension à l'exécution de notre kernel.
BIOS/UEFI
Sous x86 64-bits, la mise sous tension de la machine se suit par l'exécution d'un BIOS/UEFI, firmware présent sur la carte mère de la machine. Son rôle principal est de charger en mémoire et d'exécuter un binaire sur la machine.
L'UEFI cherche dans plusieurs emplacements le binaire à exécuter.
Généralement ces emplacements sont des périphériques de stockage tel qu'un disque
dur ou une clé USB, mais cela peut aussi être un serveur comme PXE.
Une liste de ses emplacements peut être définie et ordonnée par l'utilisateur,
c'est ce qu'on appelle le bootorder
.
Les emplacements indiqués à l'UEFI doivent suivre une spécification lui permettant d'exécuter le bon binaire.
On dit alors que l'emplacement est bootable.
Partition EFI
Pour qu'un périphérique de stockage, comme notre futur clé USB, soit bootable celle-ci doit avoir une partition EFI. C'est un format de système de fichiers qui suit les spécifications FAT, nous y placerons notre binaire à exécuter.
Binaire .efi
Une fois la partiton EFI detectée, l'UEFI va essayer d'exécuter le binaire se trouvant
à l'emplacement /EFI/Boot/bootx64.efi
pour une architecture 64 bits et /EFI/Boot/bootx32.efi
en 32 bits.
Le binaire bootx64.efi
exécuté par l'UEFI peut être libre tant qu'il est au format .efi
.
Bootloader
Généralement le binaire exécuté par l'UEFI est un bootloader. Son rôle est de charger en mémoire et d'exécuter un système d'exploitation présent sur la machine. Le bootloader propose aussi des alternatives de démarrage, permettant de démarrer sur un autre système pour réparer le principal par exemple. L'usage d'un bootloader est donc recommandé. En effet, l'UEFI est capable de démarrer l'image kernel sans passer par un bootloader. Cela demande quelques modifications au niveau de la configuration kernel. Malgré le gain de performance au démarrage, cette solution perd les avantages du bootloader.
Nous utiliserons le bootloader GRUB, globalement utilisé sur les distributions Debian, pour démarrer notre kernel Linux. Nous verrons comment configurer le bootloader GRUB pour charger notre kernel.
Kernel
Le bootloader passe l'exécution au kernel. Ce dernier rentre dans sa phase d'initialisation essayant de monter le système de fichiers racine, le rootfs, surlequel il compte exécuter le processus init. Sans système de fichiers ou de processus init, le kernel tombe en erreur et s'arrête. Nous allons voir comment générer un petit système de fichiers valide.
Système de fichiers
Nous allons dans cette partie nous concentrer sur la création d'un petit système de fichiers pour notre kernel. Avec son Process ID (PID) n°1, le processus init est le père de tous les processus, ça mort équivaut à l'arrêt du kernel (par un kernel panic). Ce processus init peut différer en fonction des utilisations. Sur la majorité des distributions Linux le processus init est Systemd. Ainsi, il est évident que le processus init n'est pas embarqué dans le binaire du kernel : ce sont deux choses à part entière. Le processus init est, comme le reste des applicatifs, présent dans le système de fichiers.
Nous allons donc générer un système de fichiers, qui contiendra
notre fameux processus init, mais aussi un ensemble de binaire, bibliothèque, fichiers de
configurations, etc, dont le processus init et ses processus enfant sont éventuellement dépendant.
Le contenu d'un système de fichiers est une arborescence de fichier
avec un dossier racine. Vous pouvez donc naturellement créer ce contenu en amont
à partir d'une machine existante en créant un dossier racine, root
par exemple,
et y placer l'ensemble des fichiers nécessaires.
Plutôt que de générer le contenu de notre système de fichier à la main, des outils
nous existent pour nous simplifier la tâche.
Busybox
Pour cette article nous allons utiliser l'outil BusyBox. Cet outil nous permet de générer le contenu d'un système de fichiers minimaliste, souvent utilisé dans l'informatique embarqué où les espaces mémoire peuvent être restreint.
Téléchargez la dernière version de BusyBox
(1.35.0 à la rédaction) dans le dossier ~/usb-bootable
:
Pour éviter des dépendances et s'assurer du bon fonctionnement de notre système
de fichier, nous allons produire des binaires statiques.
Pour ce faire, nous devons modifier la configuration de BusyBox.
Son fonctionnement étant similaire au kernel, un fichier .config
définit les variables
et un menuconfig permet de les modifiers via une interface graphique :
Allez dans Settings
et activez Build static binary
.
Quittez et sauvegardez la configuration (dans .config
).
/
et avoir de l'aide ?
pour chaque options.
Voici la configuration busybox générée de mon côté. Lancez la production des fichiers via la commande suivante :
Cette commande produit l'arborescence de notre système de fichiers dans _install
.
Ce dernier contient les dossiers bin
, sbin
et usr
.
Exécutez la commande tree
dans le dossier :
_install/
├── bin
│ ├── arch -> busybox
│ ├── ash -> busybox
│ ├── base32 -> busybox
│ ├── base64 -> busybox
│ ├── busybox
│ ├── cat -> busybox
│ ├── chattr -> busybox
│ ├── chgrp -> busybox
│ ├── chmod -> busybox
...
Vous remarquerez que chaque binaire est un lien symbolique vers le binaire bin/busybox
.
La raison ? L'ensemble des binaires sont unifiés dans le binaire busybox
permettant un gain de place (suppression de l'overhead )
dû au format de fichier d'exécution comme ELF.
Les binaires cp
, echo
, dd
, etc, se succèdent dans l'espace mémoire.
Ainsi, exécuter la commande cp
revient à se brancher à l'addresse
mémoire du binaire cp
dans le binaire bin/busybox
.
De part sa taille légère (2,7M), ce type de génération de système de fichiers minimaliste est très souvent
utilisé dans l'informatique embarqué.
Processus init
Par défaut le processus init généré se nomme linuxrc
. Pour notre utilisation, nous
allons créer notre propre processus init.
Copiez et renommez le dossier du rootfs _install
dans ~/usb-bootable
:
Depuis la copie du rootfs, supprimez le fichier linuxrc
et créez un nouveau
fichier init
à la racine de votre rootfs et ajoutez-y le contenu suivant :
#!/bin/sh
mkdir -p /dev
mount -t devtmpfs none /dev
mkdir -p /proc
mount -t proc none /proc
mkdir -p /sys
mount -t sysfs none /sys
setsid cttyhack /bin/sh
Dans ce processus init nous allons créer 3 dossiers à la racine :
- /dev : dossier listant les périphériques détectés par le kernel.
- /proc : dossier donnant des informations temps réel sur le kernel.
- /sys : dossier permettant de contrôler les périphériques.
Le contenu de ces dossiers sont générés par le kernel, il s'agit de
pseudo-système de fichiers,
ils doivent être montés via la commande mount
au démarrage.
La dernière commande cttyhack /bin/sh
lance un shell.
Archive CPIO
Une fois notre rootfs minimal prêt il suffit de le compresser en une archive cpio. Il ne faut pas oublier au préalable de donner les droits d'exécution à nos fichiers :
Notre système de fichiers rootfs.gz
est prêt à l'utilisation.
Compilation d'un kernel Linux x86 64-bits
Une fois le système de fichiers prêt, compilons notre image kernel Linux mainline. Dans un premier temps, téléchargez la dernière version du kernel Linux :
Puisque nous souhaitons exécuter notre kernel sur un ordinateur portable,
nous voulons avoir accès à la console système directement sur son écran.
Par défaut, le kernel n'affiche pas celle-ci sur l'écran.
La console peut aussi être redirigée sur un port série avec l'argument console
dans la commandline du kernel.
L'affichage de contenu sur l'écran passe par le framebuffer du kernel.
Cette couche d'abstraction, représentée par le device /dev/fb0
, permet d'afficher
des éléments graphiques.
/dev/fb0
reviendra à écrire du contenu sur l'écran.
Par exemple, $ cat /dev/random > /dev/fb0
affichera de la "neige", tandis que
$ cat /dev/zero > /dev/fb0
"effacera" le contenu du framebuffer.
Pour activer l'affichage de la console sur le framebuffer et donc sur l'écran de notre ordinateur, il faut activer les options suivantes dans la configuration du kernel :
- CONFIG_FRAMEBUFFER_CONSOLE
- CONFIG_SYSFB_SIMPLEFB
- CONFIG_FB
Depuis un ordinateur en x86 64-bits vous pouvez directement charger la configuration du kernel x86_64 avec :
Sinon en spécifiant le fichier :
Ouvrez le menuconfig et activez les options. Affichez l'aide pour comprendre leur utilité. Si besoin, activez aussi les dépendances nécessaires. Voici la configuration kernel générée de mon côté. Une fois la configuration modifiée et sauvegardée, construisez le kernel :
Notre image kernel ~/usb-bootable/linux/arch/x86/boot/bzImage
est prête.
Génération du bootloader GRUB
Pour générer notre bootloader GRUB nous utiliserons l'outil grub-mkimage
.
Sous Debian, l'outil est présent dans le paquet grub-common.
Binaire bootx64.efi
Une fois le binaire installé, nous pouvons générer facilement un binaire bootx64.efi
de notre bootloader :
Voici la signification des différentes arguments :
- L'option
-o
définit le nom du binaire généré. Par convention le nom doit êtrebootx64.efi
pour une architecture 64 bits etbootx32.efi
pour une architecture 32 bits. Ce fichier sera détecté et exécutable par l'UEFI. - L'option
-p
définit le préfixe du répertoire où se situe le binaire. Nous verrons son utilité pour le fichier de configuration GRUB. - L'option
-O
définit le format généré, ici un binairex86_64
pour UEFI. - Le reste des arguments sont les modules à embarquer dans le binaire. Par exemple
le module
linux
nous permettra de charger l'image kernel, et le moduleboot
de le démarrer.
Fichier de configuration grub.cfg
Au démarrage, le bootloader GRUB va proposer un interpréteur de commande.
Cet interpréteur permet d'exécuter des commandes, tel que charger le kernel et l'exécuter.
Pour simplifier la tâche et rendre l'utilisation du bootloader plus intuitif,
nous allons créer un fichier de configuration GRUB pour proposer un menu
d'entrée au démarrage.
Chaque entrée du menu définit à des commandes à exécuter avant l'exécution de la commande boot
qui lance l'exécution du kernel.
Nous allons créer une entrée pour charger notre kernel et son rootfs.
Le rootfs n'étant pas présent sur le périphérique de stockage de la machine,
mais sur notre clé USB avec l'image kernel, il est nécessaire de le charger en RAM
via le bootloader. Le rootfs sera alors accessible au démarrage du kernel
et notre processus init sera exécuté.
Créez à la racine de votre dossier le fichier grub.cfg
:
menuentry 'Boot Linux kernel' {
linux /efi/boot/bzImage
initrd /efi/boot/rootfs.gz
}
Remarquez ici l'utilisation du préfixe /efi/boot
définit lors de la génération
du bootloader GRUB.
initrd
GRUB permet de charger un système de fichiers
en RAM. Le nom "initrd" fait référence à l'initial ramdisk,
un système de fichiers temporaire d'initialisation permettant de monter le système de fichier final.
Cependant l'utilisation de cette commande n'est pas réservée à l'initrd.
En effet, du point de vue du bootloader notre rootfs ou un initrd reste un système
de fichiers à charger en RAM.
Clé USB bootable
Les éléments constituant notre clé USB sont prêts : le kernel, le rootfs avec un processus init et le bootloader et sa configuration. Nous allons maintenant voir comment produire une clé USB bootable embarquant ces éléments.
Image
En informatique, une image désigne la réplique du contenu, bit à bit, d'un périphérique de stockage.
L'image peut ensuite être stockée sur un autre périphérique de stockage.
Par exemple, on peut créer l'image d'un disque dur et le stocker ailleurs
(pour des sauvegardes) avec des binaires comme dd
.
Ainsi, plutôt que de manipuler directement le stockage de notre clé USB pour la
rendre bootable ― c'est-à-dire, pour rappel, de créer une partition EFI au format
FAT et d'y placer nos éléments ― nous allons créer une image de notre clé USB
que nous copierons ensuite sur le stockage de celle-ci.
Cela à plusieurs avantages, donc être capable de copier la même image sur plusieurs périphériques.
Fichier image
Commençons par créer le fichier image vide d'une taille de 34M.
Nous verrons par la suite pourquoi la taille 34M est importante.
Pour ce faire, on utilise la commande dd
avec en entrée le fichier spécial /dev/zero
générant un
flux constant de cactère nul vers image :
34+0 enregistrements lus
34+0 enregistrements écrits
35651584 octets (36 MB, 34 MiB) copiés, 0,022835 s, 1,6 GB/s
Loop device
Nous venons de créer le fichier image vierge. Pour ajouter notre contenu, nous allons rendre accessible notre image (qui est à l'état de fichier) en un loop device, c'est à dire un pseudo-périphérique accessible par bloc où nous serons désormais capable d'écrire correctement dans de notre image.
La première étape consiste à récupérer un pseudo-périphérique loop libre présent dans /dev
:
/dev/loop0
Nous pouvons attacher, avec les droits super-utilisateur, notre image sur ce loopdevice
(remplacez le /dev/loop0
par celui retourné à la commande précédente) :
Notre image est maintenant attachée au pseudo-périphérique /dev/loop0
.
Affichez ses informations via la commande fdisk
:
Disque /dev/loop31 : 34 MiB, 35651584 octets, 69632 secteurs
Unités : secteur de 1 × 512 = 512 octets
Taille de secteur (logique / physique) : 512 octets / 512 octets
taille d'E/S (minimale / optimale) : 512 octets / 512 octets
Dans la suite des manipulations utilisez la commande fdisk
pour votre les informations
de votre image évoluer.
Essayons de monter ce pseudo-périphérique dans un dossier pour accéder à son contenu :
mount: ~/usb-bootable/img: wrong fs type, bad option, bad superblock on /dev/loop0, missing codepage or helper program, or other error.
Voilà une jolie erreur tout à fait normale !
Notre image étant vide (remplie de caractère nul par /dev/zero
)
aucune partition ni système de fichiers ne sont présents comme en témoigne
les informations affichées par fdisk
.
Le kernel est incapable de monter et manipuler notre image.
Formatage
Avant d'y copier nos éléments, nous allons devoir formater notre image en y ajoutant une partition EFI avec un système de fichiers FAT.
Table de partition
Pour ajouter des partitions il est nécessaire d'écrire une table de partitionnement.
Créez une table de partition MBR msdos
sur le pseudo-périprérique de notre image :
Disque /dev/loop31 : 34 MiB, 35651584 octets, 69632 secteurs
Unités : secteur de 1 × 512 = 512 octets
Taille de secteur (logique / physique) : 512 octets / 512 octets
taille d'E/S (minimale / optimale) : 512 octets / 512 octets
Type d'étiquette de disque : dos
Identifiant de disque : 0xc22ab48d
Deux nouvelles lignes, le type et l'identifiant du disque, sont affichées par
la commande fdisk
.
Partition primaire
Pour simplifier, nous allons créer une seule partition FAT32 (de la famille FAT) pour notre image qui contiendra l'ensemble de nos éléments, c'est la partition primaire. Le format FAT32 est nécessaire pour que notre UEFI puisse considérer cette partition à explorer pour y trouver le binaire de notre bootloader GRUB.
Notre table de partition étant prête, nous pous ajotuer notre partition primaire. Pour une question d'alignement mémoire la partition doit commencer à 1 MiB :
Disque /dev/loop31 : 34 MiB, 35651584 octets, 69632 secteurs
Unités : secteur de 1 × 512 = 512 octets
Taille de secteur (logique / physique) : 512 octets / 512 octets
taille d'E/S (minimale / optimale) : 512 octets / 512 octets
Type d'étiquette de disque : dos
Identifiant de disque : 0xc22ab48d
Périphérique Amorçage Début Fin Secteurs Taille Id Type
/dev/loop0p1 2048 69631 67584 33M 83 Linux
On aperçoit dès à présent notre partition primaire p1
dans /dev/loop0
de type Linux
.
Elle commence au secteur n°2048. Un secteur faisant 512 octets, nous retrouvons
notre alignement de 1MiB : 2048 * 512 = 1048576 octets = 1 MiB
.
La taille minimal d'une partition FAT 32 doit être supérieur à 32M,
nous somme obligé avec l'alignement de dépasser un peu, d'où les 34M (histoire d'être sûr).
Flag ESP
À ce stade, notre partition primaire est vierge et sans système de fichiers.
Nous devons la transformer en une partition EFI capable d'être détecter et démarrer
par l'UEFI.
L'ajout du flag esp
à la partition primaire permet de faire ce changement :
Disque /dev/loop0 : 34 MiB, 35651584 octets, 69632 secteurs
Unités : secteur de 1 × 512 = 512 octets
Taille de secteur (logique / physique) : 512 octets / 512 octets
taille d'E/S (minimale / optimale) : 512 octets / 512 octets
Type d'étiquette de disque : dos
Identifiant de disque : 0x0957412c
Périphérique Amorçage Début Fin Secteurs Taille Id Type
/dev/loop0p1 2048 69631 67584 33M ef EFI (FAT-12/16/32)
On remarque que le type est passé de Linux
à EFI
avec la supposition que la partition
contient un système de fichiers FAT.
Essayez de monter cette partition :
mount: ~/usb-bootable/img: wrong fs type, bad option, bad superblock on /dev/loop1p1, missing codepage or helper program, or other error.
Même erreur que tout à l'heure ! En effet, bien que notre partition soit déclaré comme EFI elle ne contient cependant toujours pas de système de fichiers.
FAT 32
Formattons la partition primaire EFI en FAT 32 :
mkfs.fat 4.1 (2017-01-24)
Disque /dev/loop31 : 34 MiB, 35651584 octets, 69632 secteurs
Unités : secteur de 1 × 512 = 512 octets
Taille de secteur (logique / physique) : 512 octets / 512 octets
taille d'E/S (minimale / optimale) : 512 octets / 512 octets
Type d'étiquette de disque : dos
Identifiant de disque : 0xc22ab48d
Périphérique Amorçage Début Fin Secteurs Taille Id Type
/dev/loop0p1 2048 69631 67584 33M ef EFI (FAT-12/16/32)
Aucun changement n'est observable via fdisk
, cependant notre kernel est maintenant
en mesure de monter notre partition /dev/loop0p1
.
Arborescence
Nous pouvons désormais monter correctement la partition dans le dossier img
:
La dernière étape consiste à copier dans notre image nos éléments produits dans les
parties précédentes, à savoir : notre bootloader GRUB et sa configuration,
l'image kernel et son rootfs compressé.
Copier nos fichiers à la racine du système de fichiers de notre image
ne fonctionnera pas.
En effet, nous avons vu que l'UEFI va chercher le binaire bootx64.efi
dans
le dossier /EFI/Boot
et que nous avons indiqué dans la configuration GRUB que
notre image kernel et son rootfs se trouve dans ce même dossier avec le préfixe /efi/boot
.
/EFI/Boot
et /efi/boot
déclaré comme préfix dans GRUB pointe sur le même dossier.
Nous devons donc copier l'ensemble des éléments dans le dossier /EFI/Boot
:
img
└── EFI
└── Boot
├── bootx64.efi
├── bzImage
├── grub.cfg
└── rootfs.gz
Le contenu de notre image est prêt !
Démontage
Démontez le pseudo-périphérique du dossier img
et détachez l'image :
fdisk -l
sur notre fichier image usb.img
vous affichera
le même contenu que lorsque la commande était exécutée sur le pseudo-périphérique,
à savoir les partitions de notre image.
Notre fichier image usb.img
est désormais prêt à être flashé sur un
autre périphérique de stockage. Nous opterons pour une clé USB, de préférence vierge.
Flashage
Branchez la clé USB sur votre ordinateur et récupèrez le périphérique
associé via lsblk
ou en lisant les logs du kernel via dmesg | tail
.
Dans mon exemple, la clé USB est associé au périphérique /dev/sdd
.
Avec la commande fdisk -l /dev/sdd
vous pouvez observer le partitionnement actuel
de votre clé USB. Nous allons le remplacer avec notre image via la commande dd
(n'oubliez pas de remplacer la destination d'écriture avec votre périphérique) :
Avant de débranchez votre clé USB exécutez la commande sync
pour assurer que toutes
les écritures sur le périphérique soient faites.
Démarrage sur ordinateur x86 64-bits
Munissez-vous d'un ordinateur x86 64-bits et branchez-y votre clé USB bootable. Démarrez l'ordinateur et interrompez l'UEFI pour changer son bootorder et ainsi démarrer en premier sur votre clé USB bootable. Une fois la configuration modifiée, sauvegardez et quittez l'UEFI avec un redémarrage "Save and reset". Au redémarrage, l'UEFI détectera votre clé USB et sa partition ESP et exécutera votre bootloader. Vous devrez voir afficher le menu d'entrée GRUB avec le choix "Boot Linux kernel".
e
vous permet de modifier l'entrée sélectionnée avant de l'exécuter.
La touche c
vous permet d'ouvrir la console GRUB pour exécuter des commandes manuelles.
Sélectionnez cette entrée en appuyant sur "Entrée" : votre kernel s'exécute !
Conclusion
Nous avons vu comment créer une simple clé USB bootable. Le format FAT 32 pose cependant certaines limites, tel que la taille maximale d'un fichier ou son insensibilité à la casse. Vous pouvez modifier le rootfs pour ajouter vos propres binaires (et ses dépendances) en fonction de vos besoins.