< É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 gccAprè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 >