< Créer et exécuter une tâche | TutoOS | Gérer la mémoire - utiliser la pagination >
Le package contenant les sources est téléchargeable ici : kernel_MonoTask_Syscall.tgz
Pour naviguer dans l'arborescence : MonoTask_Syscall
Le mécanisme de segmentation empèche le code utilisateur d'accéder librement aux périphériques ou au noyau. Pour accéder à ces ressources, par exemple pour écrire dans la mémoire vidéo ou sur disque, le code utilisateur utilise des services implémentés au niveau du noyau : les appels systèmes. Le noyau illustré ici implémente un appel système permettant d'afficher un message à l'écran.
Nous avons vu aux chapitres précédent que des interruptions peuvent êtres déclenchées par les périphériques ou directement par le processeur en cas d'exception. Un troisième type d'interruption existe, ce sont les interruptions logicielles, déclenchées volontairement par le code noyau ou utilisateur à l'aide de l'instruction int
. Ce sont ces interruptions qui vont nous servir pour implémenter les appels système.
Nous avons déjà détaillé la façon dont fonctionnent les interruptions matérielles. Les interruptions logicielles fonctionnent exactement de la même façon à deux différences prèt :
Le DPL (Descriptor Privilege Level) est utilisé pour contrôler l'accès au segment selon le niveau de privilège du code appelant. La valeur 3 indique que toutes les applications, même les moins privilégiées, peuvent utiliser le trap gate tandis que la valeur 0 restreint son utilisation au seul code noyau. Comme nous voulons que le trap gate soit utilisable par les applications utilisateur, il faut un DPL de 3 :
Il est initialisé à l'aide du code suivant :
Grâce à ce descripteur, suite à une interruption de type int 0x30
, le processeur va exécuter la routine de service _asm_syscalls
.
Nous avons vu ci-dessus qu'un appel système se fait très simplement grâce aux interruptions logicielles. Le noyau peut ainsi fournir des services aux applications utilisateur, mais comment passer des paramètres à ces services ? Deux solutions sont très courantes :
Nous allons voir comment implémenter la deuxième solution. Elle a le mérite d'être simple, mais le nombre réduit de registres sur les architectures i386 limite le nombre de paramètres que l'on peut passer par cette méthode (eax, ebx, ecx, edx, edi et esi).
Dans notre implémentation d'un appel système qui affiche une chaîne de caractère à l'écran, les registres utilisés sont :
eax
, qui contient le numéro d'appel système
ebx
, qui contient l'adresse de la chaîne de caractères à afficher
Le code qui réalise l'appel :
La routine de traitement de l'interruption est semblable à celles déjà vu pour les interruptions matérielles. Elle commence par sauvegarder les registres utilisateur et par initialiser le registre DS pour qu'il pointe sur le segment de données du noyau. Ensuite, elle pousse sur la pile le registre eax
, qui contient le numéro de l'appel système, pour le transmettre à la fonction do_syscalls()
appelée juste après :
La fonction do_syscalls()
effectue le vrai travail de gestion de l'interruption. Le principe de cette fonction est simple :
La ligne ci-dessus permet de récupérer le numéro d'appel système passé en paramètre à la fonction (par le biais de l'instruction push eax
). Dans notre implémentation, il n'y a pour le moment qu'un appel système, mais un noyau en a généralement plusieurs dizaines ou centaines.
Le bloc traite l'appel système numéro 1
.
Dans notre implémentation, les paramètres sont passés à l'appel système à l'aide des registres. Nous savons que l'adresse de la chaîne à afficher a été placée par le programme utilisateur dans le registre EBX. Mais à cet endroit de la fonction do_syscalls
, rien ne nous garanti que ce registre n'ait pas été modifié. Heureusement, ce registre a été sauvegardé sur la pile noyau, ce qui permet de récupérer sa valeur.
Une fois récupérée la valeur de EBX, on devrait pouvoir afficher la chaîne sans problème ? Et bien non !
La chaîne de caractères contenant le message à afficher n'est toujours pas accessible car EBX
contient seulement un déplacement et il faut récupérer l'adresse de la base du segment de données utilisateur pour connaître l'adresse physique exacte où se situe la chaîne. On obtient cette adresse à partir de la valeur sauvegardée du registre DS qui pointe sur le bon descripteur de segment dans la GDT.
Note : l'annexe sur les Stack Frame explique dans le détail comment récupérer des paramètres sur la pile.
A partir de la valeur ds_select
, on retrouve l'emplacement du descripteur de segment de données dans la GDT. La valeur du registre DS n'est pas utilisée telle qu'elle car le champ RPL a été positionné. Pour obtenir l'offset du descripteur dans la GDT, il faut appliquer le masque 0xF8
. L'adresse de base est ensuite reconstituée à partir de ses différents champs.
Enfin, la fonction print()
est appelée avec l'adresse physique de la chaîne à afficher.
Pour les autres appels systèmes, on affiche seulement un petit message à l'écran et la fonction se termine.
Le code applicatif est assez simple et affiche simplement un message avant de boucler indéfiniment.
Le modèle d'organisation de la mémoire utilisé est décrit au chapitre précédent. Il spécifie que le code applicatif, défini dans par la fonction task1()
, doit être en 0x30000
. Comme ce code fait au maximum 100 octets, le noyau le recopie directement à l'adresse voulue :
Les données utilisateur, ici le message à afficher, doivent être stockées dans le segment entre 0x30000
et 0x31000
. En principe, nous aimerions avoir une fonction utilisateur ressemblant à ceci :
Hélas, à ce stade du développement de notre noyau, ça n'est pas possible car la fonction task1()
fait partie du noyau et n'a pas été compilée pour tenir compte du fait qu'elle serait relogée ailleurs.
Nous sommes donc obligés de ruser un peu pour stocker la chaîne de caractère quelque part entre 0x30000
et 0x31000
. Arbitrairement (enfin pas tout à fait, nous avons pris soin de prendre une valeur qui n'écrase pas le code de la fonction), la chaîne est placée en 0x30100
:
Une fois la chaîne placée en 0x30100
, le code peut exécuter l'appel système permettant de l'afficher à l'écran :
La tâche en mode utilisateur fait un appel système au service d'affichage, en mode noyau, qui écrit dans la mémoire vidéo :
< Créer et exécuter une tâche | TutoOS | Gérer la mémoire - utiliser la pagination >