< Écrire un noyau en C | TutoOS | Gérer les interruptions - la théorie >
Le package contenant les sources est téléchargeable ici : kernel_ReloadGDT.tgz
Pour naviguer dans l'arborescence : ReloadGDT
Au boot, le programme du MBR commute le PC en mode protégé afin de pouvoir charger et exécuter un noyau 32 bits. Le problème est que la GDT initialisée par le secteur de boot ne correspond pas forcément è celle que l'on souhaite pour le noyau. Par exemple, si on démarre notre kernel à l'aide de LILO ou d'un autre boot loader, on ne sait pas a l'avance où sera la GDT ni comment elle sera constituée. Un noyau doit donc initialiser et charger sa propre GDT.
Il affiche un message, initialise la nouvelle GDT et la charge en mémoire. Après avoir réinitialisé la pile, le noyau passe la main à la fonction main()
qui affiche un message et boucle indéfiniment :
Les structures ci-dessous, définies dans le fichier gdt.h
, servent à créer les descripteurs de segment et le registre GDTR :
La directive __attribute__ ((packed))
indique à gcc
que la structure en question doit occuper le moins de place possible en mémoire. Sans cette directive, le compilateur insère des octets entre les champs de la structure afin de les aligner pour optimiser la vitesse d'accès. Or dans notre cas, nous voulons que la structure décrive exactement l'occupation en mémoire des données.
Pour ces mêmes raisons, il faut définir de nouveaux types de données afin de maîtriser les allocations en mémoire. Ces types sont définis dans le fichier types.h.
init_gdt()
Le fichier gdt.c contient la fonction init_gdt()
qui initialise les descripteurs de segments et charge la nouvelle GDT :
Les descripteurs sont initialisés et copiés dans le tableau kgdt[]
:
0xFFFFF + 1 = 0x100000
pages de 4ko, soient 4 Go. Autrement dit, ils adressent l'ensemble de la mémoire.
Une fois le tableau rempli, il est recopié à l'endroit en mémoire où la GDT doit résider :
Ensuite, la structure kgdtr
est initialisé puis chargée dans le registre GDTR. A ce moment là, le changement de GDT est effectif :
Une fois la nouvelle GDT chargée, il faut mettre à jour les selecteurs de segments de données (ds
, es
, fs
, gs
et ss
). Un long jump permet de mettre à jour le selecteur du segment de code :
Vous avez sans doute remarqué que le pointeur de pile est initialisé après l'appel à la fonction init_gdt()
. Pourquoi n'est-il pas initialisé dans init_gdt()
comme tous les autres ? Parceque l'instruction assembleur leave
, en fin de fonction, écrase le registre esp
avec la valeur de ebp
. Tout serait alors à refaire ! Une solution serait de forcer la valeur de ebp
de façon à ce qu'elle coïncide avec celle de esp
, mais cela ne ferait que repousser le problème : n'oubliez pas qu'en changeant la pile, on perd l'adresse sauvegardée du compteur ordinal (eip
) qui permet le retour.
main()
pour déjouer les pièges de gcc
Après avoir initialisé la GDT, la pile est initialisée pour pointer en 0x20000
. Cette adresse est arbitraire, j'aurais pu choisir autre chose... mais attention à prendre une valeur où la pile ne risque pas de corrompre le code ou des données !
Après ces initialisations, une fonction main()
est appelée. La création d'une nouvelle fonction peut sembler luxueuse quand on voit ce qu'elle réalise : juste afficher un messager et boucler indéfiniment. N'aurait-on pas pu placer l'intégralité du code dans la fonction _start()
? Non, car l'appel à la fonction print
est réalisé par gcc
de cette façon :
On remarque que le passage d'argument ne se fait pas par un push
mais par un mov
qui écrit l'adresse de la chaîne de caractère à afficher directement en 0x20000
, ce qui est au delà du sommet de la pile ! En principe, pour passer un paramètre à une fonction, un compilateur doit utiliser push
qui décrémente d'abord la valeur de esp
avant d'écrire sur la pile, mais gcc
procède autrement en réservant au début de la fonction suffisament de place sur la pile. Mais comme nous avons modifié entre temps la structure de la pile, et donc de la frame associée à la fonction _start
, cela ne peut plus fonctionner ! La fonction main()
permet de repartir sur une frame propre.
$ tar xfz kernel_ReloadGDT.tgz $ cd ReloadGDT $ make
< Écrire un noyau en C | TutoOS | Gérer les interruptions - la théorie >