
Dans GLMF 270, nous avons intégré au code de démarrage du noyau NetBSD, locore.S, les modifications nécessaires pour permettre à ce dernier de trouver les informations relatives à une VM et ainsi booter en mode PVH. Muni de ces informations et de cette nouvelle fonctionnalité, le code plus haut niveau du noyau va pouvoir avancer dans le processus de boot.
La question que je me suis posée le plus souvent pendant l’avancée dans cette aventure fut « où sommes-nous ? ». En effet, ici nous travaillons à la pince à épiler, en corrigeant ou modifiant des composants très bas niveau qui pourraient avoir des conséquences indéterminées dans la suite du déroulement du programme « noyau ». Aussi, après avoir a priori réussi à copier les données fournies par l’hôte au système invité, là où le noyau s’attend à les trouver : où atterrit-on ?
1. Gawwwww (bruit d’un rebond sur un millier d’élastiques)
Après avoir compilé notre noyau avec les modifications effectuées dans le précédent article, nous allons constater jusqu’où cela nous permet d’avancer. Pour rappel, on démarre qemu de cette façon :
Inutile de préciser un média disque, nous n’en sommes pas encore là. Les drapeaux -S et -s servent respectivement à démarrer la machine virtuelle en mode « pause » de façon à pouvoir avancer pas à pas dans le serveur gdb, invoqué grâce au drapeau -s.
Le démarrage de qemu s’effectue sur un hôte GNU/Linux, pour déboguer un noyau NetBSD nous avons besoin d’un gdb fonctionnant sur un système NetBSD qui va se connecter au serveur gdb précédemment démarré.
Si l’opération de copie de start_info, le nom de la structure de données qui contient les informations de démarrage, a porté ses fruits, nous devrions pouvoir traverser les étapes d’initialisation de l’espace d’adressage virtuel du noyau, et par conséquent, poursuivre notre route en 64 bits, finalisée au label longmode_hi de locore.S.
Attachons nos ceintures, car là où on va, il n’y a pas... de route.
Livre de chevet
Pour la suite de la lecture de cet article, il sera judicieux d’avoir le fichier sys/arch/amd64/amd64/locore.S ouvert non loin de vous [1].
Voici le code que nous allons traverser :
On passe d’abord par le label compat:, car nous ne pouvons pour le moment pas directement sauter sur une adresse 64 bits depuis notre programme démarré en 32 bits :
Arriver à cette étape signifie que notre précédent patch a fonctionné, nous ne nous arrêtons plus dans une boucle qui vient écraser les données du noyau, l’étape de l’initialisation des adresses virtuelles de ce dernier a réussi.
On constate par ailleurs que le code à venir correspond bien au code source :
Dans la perspective où nous allons passer dans un autre mode d’adressage, il convient d’effacer notre breakpoint 32 bits qui n’aura plus de sens si tout se déroule comme prévu :
Tentons maintenant d’exécuter les 9 instructions précédentes afin de passer en mode 64 bits :
Gagné ! On voit maintenant que KERNBASE, soit 0xffffffff80000000, a bien été ajouté à l’adresse 32 bits. On remarque également que puisque nous sommes désormais en mode 64 bits, gdb « reconnaît » que nous sommes dans la fonction start(), dont le symbole a une adresse 64 bits.
Mais alors… est-ce à dire que nous pouvons placer un breakpoint sur la première fonction en C appelée depuis locore.S, voire mieux, y rentrer ?
C’est pour des moments comme celui-là que je suis prêt à voir des heures et des jours partir en fumée ; concrètement, nous avons réussi, nous avons bien branché sur le boot PVH, et les instructions bas niveau de mise en place de la mémoire virtuelle ainsi que le passage en 64 bits sont un succès, nous pouvons continuer avec les étapes de démarrage.
2. Le nouvel invité
Il est temps de mettre un nom sur notre nouvelle fonctionnalité ! E que s’apelerio GENPVH.
Le choix de ce nom fut simple, on dérive du type VM_GUEST_XENPVH, mais GENérique, c.-à-d. pas tributaire d’un hyperviseur Xen.
On ajoute ce nouveau type dans le fichier sys/arch/x86/include/cpu.h, où sont définis les différents types de VM invités dans un type enum :
L’utilité de ce type est de pouvoir identifier le boot PVH générique tout au long du démarrage du noyau, aussi est-il impératif de renseigner locore.S lorsqu’il est identifié. Nous ferons cela dans la fonction start_xen32 en remplaçant :
par :
Attention ! Lorsque nous sortirons de la phase de prototypage, il faudra bien entendu réaliser les tests nécessaires pour vérifier que nous démarrons via Xen ou un autre hyperviseur. Ici, nous remplaçons le type pour poursuivre le périple rapidement.
Afin de pouvoir utiliser ce nouveau symbole, nous le déclarons également dans le fichier sys/arch/amd64/amd64/genassym.cf :
Voilà, nous pouvons désormais faire référence à notre type de machine virtuelle dans le reste du code du noyau.
3. Non-Xen
Il faut comprendre que dans le processus de démarrage de notre machine virtuelle, nous utilisons 90 % de code prévu pour le boot Xen PVH, plus particulièrement le code qui gère le rangement des variables du fameux start_info. A contrario, toute la mécanique liée à la gestion de l’invité, et en particulier les demandes d’accès à des ressources privilégiées, est gérée nativement par l’hyperviseur, KVM dans notre cas. La surcouche de gestion des syscalls, appelés hypercalls dans Xen, est donc superflue.
De façon assez surprenante, les modifications à apporter furent assez minimales. Tout d’abord, nous ajoutons dans sys/arch/xen/include/hypervisor.h une variable qui pourra être accédée par l’ensemble du code du noyau, qui permettra de savoir si nous avons démarré en mode PVH :
Cette variable sera renseignée dans sys/arch/xen/xen/hypervisor.c, code que nous allons réorganiser pour prendre en compte une plateforme non-Xen. La fonction init_xen_early() est l’une des premières fonctions C invoquées par locore.S, voici un pseudopatch commenté pour cette fonction :
4. Une nouvelle façon de démarrer
Notre noyau démarrant sur une machine virtuelle doit maintenant globalement emprunter le même chemin qu’un noyau sur un invité de type VM_GUEST_XENPVH, mais en ignorant les subtilités propres à Xen, ainsi, un brutal grep -r VM_GUEST_XENPVH sys/ nous informe des fichiers à considérer.
Nous allons comprendre l’importance de cette fameuse structure start_info. Techniquement, nous sommes en train de démarrer une machine x86, qui anciennement recevait ses informations du bootloader (qui lui-même en recevait une partie du BIOS), parmi ces informations cruciales se trouve le périphérique de démarrage, rien que ça ! Ce paramètre, dans notre cas, est passé comme argument au noyau : -append "root=ld0a", et une fonction existe déjà dans sys/arch/xen/xen/xen_machdep.c pour décoder les informations passées en ligne de commande, il s’agit de xen_parse_cmdline(), dont voici la section qui nous intéresse :
Ce scénario est présent dans sys/arch/x86/x86/x86_autoconf.c à la fonction cpu_bootconf, mais ne prend en compte que VM_GUEST_XENPVH, or, nous disposons maintenant d’une variable, pvh_boot, qui couvre les cas de figure VM_GUEST_XENPVH et VM_GUEST_GENPVH, il suffit donc d’appliquer ce patch pour connaître notre device de boot :
Les autres éléments incontournables habituellement délivrés par le BIOS sont les zones mémoire disponibles pour l’invité, leur préparation pour être utilisées par le noyau est effectuée dans sys/arch/x86/x86/x86_machdep.c dans la fonction init_x86_clusters(). Là encore, le cas est prévu pour Xen, et utilise x86_add_xen_clusters() au lieu de x86_parse_clusters(), le patch est trivial :
Il manque également une zone mémoire dont les informations sont normalement fournies par le BIOS ou l’UEFI : les tables ACPI. Et si vous vous souvenez de la liste des informations fournies par start_info vues dans le précédent épisode, on y trouve effectivement ces adresses. Même cause, même conséquence, cette fois dans la fonction acpi_md_OsGetRootPointer dans le fichier sys/arch/x86/acpi/acpi_machdep.c :
Nous y sommes presque, il nous manque encore une information classiquement passée par le bootloader, ce sont les drapeaux passés au noyau pour lui indiquer son mode de démarrage, par exemple -s pour démarrer en mode utilisateur unique, habituellement pour déboguer son système, -v pour être verbeux, -z pour afficher le minimum…
Ce patch ciblera la fonction init_x86_64() dans sys/arch/amd64/amd64/machdep.c, et nous allons à nouveau utiliser la fonction xen_parse_cmdline() :
5. Y fait tout noir ici !
Afin de visualiser les premiers pas de notre noyau sur la console série, nous pouvons forcer cette dernière à l’aide de l’option CONSDEVNAME du noyau, qui se déclare de cette façon :
Néanmoins, il est bien plus pratique et portable de spécifier ces informations comme des paramètres du noyau, typiquement dans la ligne de commande qemu : -kernel ${KERNEL} -append "console=com". Seulement voilà, à ce stade, ce paramètre n’a aucun effet, et pour cause, nous nous trouvons dans une situation bâtarde, en effet, d’un côté, les informations sur le type de console à utiliser pour l’affichage sont typiquement transmises par le bootloader, qui est absent de notre amorce ; de l’autre, lorsque l’hyperviseur est Xen, sys/arch/xen/x86/consinit.c implémente bien l’interprétation de start_info, mais est truffé de spécificités liées à Xen, et dans la mesure du possible, j’aimerais éviter l’inclusion de trop de symboles et conditions inutiles.
La solution : modifier sys/arch/x86/x86/consinit.c, le code responsable de l’initialisation de la console pour une plateforme x86, pour y implémenter l’interprétation des paramètres passés au noyau dans un boot PVH.
Et en effet, après application de ce dernier patch, enfin, le résultat de plusieurs mois de travail s’affiche sous nos yeux humides :
Le noyau démarre en mode PVH, invoqué par le drapeau -kernel de qemu.
6. FASTER!
Notre noyau boote vite. Relativement. Une centaine de millisecondes. Mais nous pouvons faire mieux, beaucoup mieux. Et puis, il y a autre chose, ce noyau démarre avec qemu, qui est capable d’utiliser un bus PCI pour attacher disques et cartes réseau virtuelles de type VirtIO, seulement voilà, un autre gestionnaire léger de machine virtuelle, Firecracker [2], ne dispose pas de support PCI, il utilise à sa place un bus virtuel, MMIO, plus léger et plus rapide, qui a le bon goût d’être supporté par qemu.
Vous l’aurez compris, la série n’est pas terminée. Alors on se retrouve bientôt pour l’implémentation d’un bus virtuel, et l’utilisation de MMIO avec VirtIO. Tout un programme…
7. Épilogue
À l’heure où j’écris ces lignes, le patchset complet de l’ajout de ces fonctionnalités au noyau NetBSD est en cours de review, en effet, ces patchs modifient des éléments fondamentaux du démarrage du noyau, il est donc important qu’ils soient bien revus et analysés par plusieurs yeux expérimentés. Si tout se passe bien, cette feature fera partie de NetBSD 11 !
Références
[1] https://github.com/NetBSDfr/NetBSD-src/blob/trunk/sys/arch/amd64/amd64/locore.S