Les principaux outils

Le but de cette section est de décrire brièvement les outils utilisés pour compiler et exécuter ManuX .

L’essentiel du code de ManuX est écrit en C, et un peu d’assembleur est nécessaire ici ou là, …Le processus de compilation est évidemment automatisé avec l’outil make. Je vais décire sommairement cette étape dans la sous-section [subsection:compilation].

ManuX peut ensuite être exécuté sur une machine réelle, mais il est évidemment possible d’utiliser un émulateur. Nous évoquerons les possibilités dans la sous-section [subsection:execution].

Le débugage est un élément important dans la phase de développement d’un logiciel, et il se révèle parfois un peu délicat lorsqu’il s’agit de développer un noyau. Nous évoquerons des outils dans les sous-sections 4.1 et 4.2.

Le compilateur C

Le compilateur GCC est utilisé, la version 11.3.0 avec une cible x86_64-linux-gnu fonctionne parfaitement. Je n’ai jamais eu de problème avec aucune version de GCC jusqu’à présent.

L’assembleur

J’utilise NASM[1] pour les quelques fichiers écrits en assembleur, dans sa version 2.14.

La compilation

La commpilation générale du noyau est fondée sur l’outil make, et j’utilise la version 4.2.1 de gnumake.

Les scripts

Quelques outils de scripts tels que awk sont utilisés, rien de très original, et tout système Linux correctement installé devrait fournir ce qui est nécessaire.

La gestion des fichiers iso

Les fichiers iso son générés par la commande grub-mkrescue qui utilise xorriso.

L’exécution dans un émulateur

Bien sûr, l’idée n’est pas de faire fonctionner ManuX sur du matériel réel, même si c’est tout à fait possible, mais plutôt dans un émulateur.

J’utilise pour cela Qemudans sa version 3.1.0. On peut également utiliser bochs ainsi que VirtualBox.

La documentation

La maigre documentation (que vous êtes en train de lide) est écrite essentiellement en LaTeX. N’importe quelle version convient parfaitement.

J’utilise également DoxyGen et mkdocs pour générer ensuite une version navigable en ligne.

Compilation de ManuX

Le Makefile principal défini un certain nombre de cibles utilisables pour compiler et exécuter ManuX.

La création du noyau

Sans grande surprise, on compile avec la commande

  make

Différentes cibles peuvent être créées

  • manux permet de compiler une image du noyau ;

  • run permet de lancer un émulateur (par défaut pour le moment c’est Qemu) ;

  • clean permet d’effacer tous les fichiers issus de compilation ;

  • iso permet de créer une image iso.

Gestion de configurations multiples

L’essentiel de la configuration de ManuX se fait dans le fichier

include/manux/config.h (dox) . Il est cependant possible de demander à maked’utiliser un fichier différent.

Le répertoire multiconf contient différents fichiers de configuration. Chacun de ces fichiers permet alors de générer une version différente de ManuX.

Ainsi, le fichier multiconf/printk.h (dox) définit la configuration permettant de compiler un noyau ManuX montrant l’utilsation de printk. On le compile de la façon suivante

  make CFG=printk

Il peut alors être exécuté par exemple avec

  make run

Création d’un fichier iso

Il est également possible de créer un fichier isocontenant une ou plusieurs configuration de ManuX.

Pour créer un fichier isocontenant la version actuellement définie dans le fichier include/manux/config.h (dox) on utilisera simplement

  make iso

L’exécution pourra alors se faire via la commande

  make runiso

Il est également possible de générer un fichier isocontenant toutes les versions de ManuX décrites par les fichiers du répertoire multiconf de la façon suivante

  make multiso

On l’exécutera de la même façon :

  make runiso

L’édition de liens

Afin d’assembler tous les éléments en un seul fichier noyau.elf qui pourra ensuite être utilisé par Multiboot, nous allons utiliser l’éditeur de liens ld. Le comportement de ld est conditionné par un fichier qui s’appelle un linker script. Un tel script décrit en particulier comment les différentes sections des fichiers d’entrée sont disposées dans le fichier de sortie et comment la mémoire est organisée.

Le code suivant montre un exemple simple de script :

ENTRY(start)
SECTIONS {
    . = 1M 
    .boot :
    {
        *(.multiboot)
    }
    adresseDebutManuX = .;
    .text : {
       *(.text)
    }        
    .rodata : {
       *(.rodata)
    }
    .data : {
       *(.data)
    }
    .bss : {
       *(.bss)
    }
}

Dans ce script, nous avous défini le nom du point d’entrée (ici start), et nous avons décrit un certain nombre de sections avec leur ordre d’apparition dans le fichier de sortie et leur contenu. Vous remarquerez également que l’on utilise la notation “.” pour spécifier l’adresse courante et que cette adresse peut être consultée mais également modifiée. Ainsi la ligne . = 1M stipule que l’on se place ) l’adresse 1M. D’un autre côté la ligne adresseDebutManuX = . stipule que l’on enregistre dans la variable adresseDebutManuX la valeur de l’adresse courante (celle du début de la section . text donc).

Exécution de ManuX

Le but n’est évidemment pas de faire tourner cette merveille sur du vrai matériel, mais plutôt dans un émulateur.

L’émulateur Qemu

C’est maintenant Qemu[2] que j’utilise, par exemple de la façon suivante

   qemu-system-i386 -drive format=raw,file=manux,index=0,if=floppy -m 64M

C’est d’ailleurs la commande que lance

   make runfloppy

Une autre façon de démarrer ManuX est la suivante

   qemu-system-i386 -kernel noyau.elf -m 64M

C’est ce que fait la commande

   make run

Mais on préférera probablement passer par une image ISO :

   qemu-system-i386 -m 64M -cdrom manux.iso

C’est ce que fait la commande

   make runiso

De nombreuses options sont disponibles, mais ce n’est pas le moment d’en parler !

L’émulateur Bochs

J’utilisais initialement cet outil, mais les fichiers de configuration ne semblent plus convenir aux dernières versions de Bochs[3] il faudrait les mettre à jour !

Le débugage de ManuX

Le débugage “interne”

Ce que j’appelle ici le débugage interne, c’est tous les outils que l’on peut utiliser dans le code lui-même pour essayer de comprendre ce qu’il s’y passe.

Nous avons pour le moment quelques outils de base, copieusement bugués eux-mêmes (!).

La fonction printk_debug

Elle permet de produire des messages d’erreur qui seront envoyés de façon sélective en fonction de la valeur de masqueDebugageConsole et de masqueDebugageFichier

Elle utilise pour cela les fonctionalités de la fonction printk() et du journal, ce qui permet d’envoyer les messages de debug sur la console et/ou dans un fichier.

Les masques de débogages

Deux masques de débogage sont définis : masqueDebugageConsole et masqueDebugageFichier. Ils sont définies dans

include/manux/debug.h (dox) .

Si le paramètre de compilation MANUX_DEBUG_VAR n’est pas défini, ces deux masques sont définis comme des macros. On ne peut donc pas trop jouer avec en cours d’exécution du noyau, …

En revanche, si MANUX_DEBUG_VAR est définie, c’est un nouveau monde qui s’ouvre à nous ! Ces variables sont en effet déclarées et initialisées dans noyau/debug.c (dox) , et elles peuvent alors être modifiées durant l’exécution du noyau.

Une utilisation particulièrement intéressante pourra en être faite au travers du registre (voir la doc) et des paramètres de boot, car il est alors possible de définir la valeur de ces masque via une option du bootloader.

La macro assert

Elle est définie dans le fichier include/manux/debug.h (dox)

et elle permet de vérifier lors de l’exécution qu’une propostion est vérifiée ou non. Si elle ne l’est pas, un message est affiché.

Le handler handlerPanique

La fonction handlerPanique est définie dans le fichier

include/manux/debug.h (dox) et permet d’afficher un état du système qui peut être utile à des fin de débugage.

Le fichier noyau/symbols

Il va surtout nous servir pour aider au débugage externe, mais il est généré à la compilation du noyau.

Le débugage “externe”

Une technique pour débuguer est d’utiliser qemu couplé avec gdb. Pour cela, je lance par exemple qemu de la façon suivante

   qemu-system-i386 -drive format=raw,file=manux,index=0,if=floppy  -rtc base\=localtime -m 64M -gdb tcp::1234  -S

Je lance ensuite gdb dans un autre terminal avec un .gdbinit qui contient :

  target remote :1234
  dir noyau
  dir lib
  file noyau/noyau.elf

La commande dir permet de dire à gdb où trouver les fichiers sources et la command file lui permet de consulter le binaire du noyau pour y trouver l’adresse des fonctions et variables (ici en fait, c’est une autre compilation du noyau au format elf !).

Exécution

Quelques commandes de base pour l’exécution de code dans gdb

  • run permet de lancer l’exécution d’un programme (inutilisable ici étant donné notre fonctionnement : on débogue à distance) ;

  • continue permet de poursuivre l’exécution jusqu’au prochain point d’arrêt/bug/fin/…;

  • step avance d’une instruction dans le code ;

  • next

  • breakpoint fonction

  • breakpoint fichier:ligne

Affichage des registres

Il peut être intéressant de consulter le contenu des registres

  • info registers donne le contenu des principaux registres.

Affichage des variables

Gdb permet également de consulter le contenu des variables :

  • p toto affiche le contenu de la variable toto.

Désassemblage du code

Une fonctionnalité intéressante est de pouvoir observer le code assembleur, ce qui peut permettre de comprendre certains comportements étranges, tels que les conséquences de choix d’optimisation du compilateur :

  • disassemble fonc affiche le code assembleur de la fonction fonc.

Références

“Qemu home page,” 2022. Available: https://www.qemu.org/.

The Bochs Project. “Bochs home page,” 2021. Available: https://bochs.sourceforge.io/.

The NASM development team. “NASM home page,” 2015. Available: https://www.nasm.us/.

[1] The NASM development team, “NASM home page,” 2015, Available: https://www.nasm.us/.

[2] “Qemu home page,” 2022, Available: https://www.qemu.org/.

[3] The Bochs Project, “Bochs home page,” 2021, Available: https://bochs.sourceforge.io/.