< Réaliser un secteur de boot qui charge et exécute un noyau | TutoOS | Écrire un noyau en C >
Le package contenant les sources est téléchargeable ici : bootsect_PMode.tgz.
Pour naviguer dans l'arborescence : BootSector_PMode
Le microprocesseur d'un PC possède trois modes de fonctionnement :
Notre objectif étant de réaliser un noyau multi-utilisateurs, multi-tâches et pouvant adresser toute la mémoire, il nous faudra basculer le microprocesseur en mode protégé. Mais cela a d'importantes conséquences pour le programmeur :
Passer du mode réel au mode protégé est très simple, il suffit juste de mettre le bit 0 du registre CR0
à 1 :
Mais si cela suffit pour changer de mode, cela ne suffit pas à ce qu'un programme continue de fonctionner une fois le mode protégé établi. Pourquoi ? L'adressage en mode protégé, qui diffère de l'adressage en mode réel, s'appuie sur des structures qui doivent etre correctement initialisées lors du changement de mode pour que le processeur continue d'adresser correctement la mémoire (et notamment le segment de données, la pile et le pointeur d'instruction).
En mode protégé, il existe pour le programmeur trois types d'adresses :
Le schéma ci-dessous résume le principe de l'adressage en mode protégé :
Dans un premier temps, nous allons utiliser uniquement le mécanisme de segmentation sans le mécanisme de pagination, plus délicat à mettre en oeuvre.
Une adresse logique est constituée par un sélecteur de segment et un offset. Le sélecteur sélectionne un bloc mémoire d'une certaine taille, appelé segment, qui définit en quelque sorte l'espace de travail du programme. L'offset est un déplacement par rapport au début de ce bloc.
Un segment est décrit par une structure de 64 bits appelée descripteur de segment qui précise :
Les descripteurs sont stockés dans la Global Descriptor Table (GDT). Cette table peut résider n'importe où en mémoire. Son adresse en mémoire physique est renseignée au processeur grâce à un registre particulier : le GDTR :
Le sélecteur de segment est un registre de 16 bits directement manipulé par le programmeur qui pointe sur un descripteur de segment dans la GDT et indique de ce fait dans quel segment on se situe. Ces registres sont bien connus de ceux qui ont déjà programmé en mode réel :
cs
est le sélecteur de segment de code
ds
est le sélecteur de segment de données
es
, fs
et gs
sont des sélecteurs de segments généraux
ss
est le sélecteur de segment de pile
Le sélecteur pointe sur un descripteur qui donne l'adresse où commence le segment. En ajoutant l'offset à cette base, on obtient une adresse linéaire sur 32 bits :
Le schéma ci-dessous décrit la structure générale d'un descripteur de segment :
0x0
et sa limite doit être à 0xFFFFF
avec le bit de granularité à 1.
Ce flag est complexe à manipuler. Il est en lien avec les différents niveaux de privilèges et de protection mis en oeuvre par les microprocesseurs de type i386. La plupart des segments de code sont non conformant, ce qui signifie qu'ils peuvent transférer le contrôle (via un call
ou un jmp
) seulement à des segments de même privilège. La gestion des niveaux de protection sur architecture i386 est rendue très complexe par un foisonnement de mécanismes impossible à résumer ici. Pour une étude approfondie, l'étude de la documentation de référence est indispensable...
Selon que le bit E est à 0 ou à 1, la limite s'interprète différement. Pour un segment sur 32 bits, si le bit E est à 1, la limite supérieure de la plage de données est en 0xFFFFFFFF
et la limite inférieure est à l'adresse indiquée par le champ limite. Note : à confirmer, mais il semble que la base soit dans ce cas purement ignorée.
Il est possible de passer en mode protégé à plusieurs moments : lors de l'exécution du secteur de boot ou du noyau. Notre noyau va utiliser un jeu d'instructions sur 32 bits, seulement utilisable en mode protégé. Le plus simple est donc de passer en mode protégé pendant le boot, avant que le noyau ne s'exécute. Il est cependant possible que ce soit le noyau qui effectue la commutation mais celà complique inutilement son écriture car le code du noyau doit alors être en partie sur 16 bits et en partie sur 32 bits.
bootsect.asm <- cliquer pour afficher le code
Ce programme est identique à celui du chapitre précédent avec en plus des instructions qui basculent le microprocesseur en mode protégé. Pour résumer, le numéro de périphérique de boot est placé dans une variable, les registres relatifs aux segments de code et de données sont initialisés, puis le noyau est chargé en mémoire à l'adresse 0x1000
(on note que la variable KSIZE
a été augmentée afin de charger un noyau plus volumineux). Ensuite, la GDT est initialisée et chargée en mémoire, puis on bascule en mode protégé. Enfin, le noyau est exécuté.
Ce secteur de boot peut charger seulement des noyaux d'une taille limitée et bien inférieure à la capacité maximale d'une disquette. Nous verrons plus tard comment charger notre noyau à l'aide de GRUB.
Avant de passer en mode protégé, il faut initialiser la GDT de façon à ce qu'il n'y ait pas de problème d'adressage après le changement de mode. La GDT doit contenir des descripteurs pour les segments de code, de données et de pile. Les directives ci-dessous déclarent et initialisent la GDT :
L'étiquette gdt:
est un pointeur sur le début du tableau qui contient trois descripteurs :
gdt_cs:
, décrit le segment de code.
gdt_ds:
, décrit le segment de données.
Chaque descripteur est initialié de façon à pouvoir adresser l'ensemble de la RAM. La base de ces segments est à 0x0
avec une limite de 0xFFFFF
pages (le bit G est à 1).
Les schémas ci-dessous résument la façon dont sont initialisés les descripteurs :
Descripteur du segment de code
Descripteur du segment de données
La GDT est directement initialisée, mais avant de basculer en mode protégé, il faut renseigner le processeur pour qu'il prenne en compte la GDT. Celà se fait en mettant à jour le registre GDTR, de 6 octets, qui contient l'adresse de la GDT et sa taille (on parle aussi de limite). On charge ce registre spécial avec l'instruction lgdt
.
Dans le programme, gdtptr
est un pointeur sur une structure qui contient les informations à charger dans le registre GDTR. La structure gdtptr
est d'abord declarée et initialisée à zéro (comme une variable classique en C) :
Ensuite, on calcule les valeurs pour mettre dans cette structure. Le code suivant calcule la taille de la GDT et stocke la valeur dans le premier champs de gdptr
:
Ce code calcule l'adresse physique de la GDT en se basant sur les valeur du segment de données ds
et de l'adresse de l'étiquette gdt
. Le résultat de ce calcul est stocké dans le second champ de gdptr
:
Notre structure est donc maintenant correctement initialisée. Nous sommes maintenant presque prèt à passer en mode protégé. Avant celà, il faut inhiber les interruptions car comme le système d'adressage va changer, les routines appelées par les interruptions ne seront plus valides après la bascule (il faudra les reprogrammer) :
Le registre GDTR est chargé avec l'instruction lgdt
pour indiquer au microprocesseur où se trouve la GDT :
On peut maintenant passer en mode protégé :
Enfin ! :-)
Notre tâche semble terminée. Mais au fait... il reste encore à réinitialiser les sélecteurs de segment de code et de données ! La commande qui suit doit impérativement être la suivante afin de vider les caches internes du processeur :
En principe, il faudrait faire un far jump à la place du near jump ci-dessus pour réinitialiser le sélecteur de segment de code. Oui mais voilà, le manuel spécifie : "When the processor is switched into protected mode, the original code segment base-address value of FFFF0000H (located in the hidden part of the CS register) is retained and execution continues from the current offset in the EIP register. The processor will thus continue to execute code in the EPROM until a far jump or call is made to a new code segment, at which time, the base address in the CS register will be changed."
Ensuite, on réinitialise les sélecteurs de données :
Puis le segment de pile :
L'instruction suivante réinitialise le sélecteur de code et exécute le noyau situé à l'adresse physique 0x1000
. Cette instruction est essentielle car elle permet, outre l'exécution du code du noyau, la réinitialisation correcte du sélecteur de code sur le bon descripteur (offset 0x8
dans la GDT) :
Ensuite, le code du noyau s'exécute...
Ce noyau affiche juste un message de bienvenue et boucle ensuite indéfiniment. A ce stade, les routines du BIOS permettant d'afficher des caractères à l'ecran ne sont plus utilisables, il faut donc que nous gérions nous même l'affichage.
La mémoire video est mappée en mémoire à l'adresse physique 0xB8000
. On peut donc afficher des informations en manipulant directement les octets débutant à cette adresse :
Les attributs sont codés de la façon suivante :
Par exemple, le code suivant affiche le caractère 'H' en blanc sur fond magenta en haut à gauche de l'écran :
On compile le boot loader et le noyau séparement puis on crée la disquette :
$ nasm -f bin -o bootsect bootsect.asm $ nasm -f bin -o kernel kernel.asm $ cat bootsect kernel /dev/zero | dd of=floppyA bs=512 count=2880
< Réaliser un secteur de boot qui charge et exécute un noyau | TutoOS | Écrire un noyau en C >