You are on page 1of 32

Rapport projet tutoré

Réalisation d’un OS 32 bits pour PC(x86)

Maxime Chéramy <mcheramy@etud.insa-toulouse.fr>


Nicolas Floquet <nfloquet@etud.insa-toulouse.fr>
Benjamin Hautbois <bhautboi@etud.insa-toulouse.fr>
Ludovic Rigal <lrigal@etud.insa-toulouse.fr>

4ème année Informatique

Résumé du projet

L’architecture Intel x86 est probablement la plus répandue dans les ordinateurs de type PC. Elle est
cependant progressivement remplacée depuis quelques années par l’architecture amd-64. Néanmoins, du
fait de la rétro compatibilité entre les deux, la majorité des systèmes d’exploitation modernes est encore
conçue pour tourner sur x86. L’objectif de ce projet, était de réaliser un système d’exploitation basé sur
un noyau de type micro-noyau pour l’architecture x86. En dehors de l’aspect purement pédagogique, nous
espérons que ce système d’exploitation puisse servir de base pour de futurs projets de bas niveau (écriture
de pilotes matériel, recherche sur des algorithmes d’ordonnancement, ajout de fonctionnalités...).

Ce projet nous a permis de découvrir de manière concrète les mécanismes de bas niveau mis en
œuvre dans nos ordinateurs, et en particulier les spécificités de cette architecture. Nous avons pu faire
la distinction entre ce qui est pris en charge par le processeur et ce qui est du domaine du système
d’exploitation. Les notions développées en cours de Système d’exploitation en 3ème année ont pris une
toute autre dimension lorsqu’il a s’agit de les mettre en œuvre. Il est beaucoup plus simple de comprendre
le fonctionnement des différents mécanismes que de les mettre réellement en pratique puisqu’un infime
bug peut mettre en péril la stabilité du système tout entier. De plus, de par le caractère bas niveau
de notre projet nous avons été confrontés à des problèmes auxquels nous n’étions pas habitués et le
débuggage s’est avéré être bien plus compliqué.

Nous avons été encadré dans ce travail par Pierre-Emmanuel Hladik et Sébastien Di Mercurio que nous
tenons à remercier, en particulier pour avoir proposé un sujet si intéressant. Dans ce présent rapport,
nous vous proposons un tour d’horizon des différents éléments que nous avons mis en place dans notre
système d’exploitation mais aussi une présentation du déroulement du projet et des problèmes rencontrés.
Enfin, nous finirons par quelques pistes pour la suite de ce projet.
Table des matières
1 Conduite de projet 1
1.1 Organisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Réalisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.3 Avancement / Répartition du travail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

2 Technique 3
2.1 Mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.1.1 Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.1.2 Malloc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.2 Gestion des interruptions et Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.2.1 Mise en place . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.2.2 Rôle du wrapper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2.3 Mapping des exceptions et interruptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2.4 Liste des interruptions matérielles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3 Gestion de l’horloge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3.1 Fonctionnement de l’horloge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3.2 Conversions de dates et calendriers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3.3 Planifications d’évènements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.4 Gestion des processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.4.1 Changement de contexte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.4.2 Ordonnancement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.4.3 Appels systèmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.5 Pilotes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.5.1 Disquette . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.5.2 Gestion du clavier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.5.3 Pilote souris . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.6 Système de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.6.1 Système de fichier FAT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.6.2 Formatage de la Partition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.6.3 Fragmentation : Clusters/Chainage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.6.4 Lecture de l’arborescence et des fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.7 Entrées / Sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.8 IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.8.1 Sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

3 Problèmes 22
3.1 Logiciels de virtualisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.2 Makefile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.3 Préemptibilité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.4 Malloc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

4 Perspectives pour la suite 24


4.1 Architecture à micro-noyau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.2 Affichage graphique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3 IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.4 Prise en charge du réseau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

5 Annexes 26
5.1 Arborescence des fichiers de compilation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5.2 Diagramme de Gantt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
5.3 Compilation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
5.4 Déboguage avec gdb . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1 Conduite de projet

1.1 Organisation
Afin de travailler sur ce projet, de se répartir le travail, et de décider des choix techniques, nous avons fait des
réunions de travail d’environ une heure toutes les deux semaines en moyenne.
Lors de ces réunions, nous commencions systématiquement par un bilan de l’avancement du système d’exploita-
tion. C’était l’occasion de faire remonter les difficultés rencontrées et de réévaluer le temps nécessaire à la réalisation
des tâches que nous avions attribués à chacun. Nous avons aussi profité de ces réunions pour donner des explications
sur le fonctionnement du système d’exploitation, par exemple, comment fonctionne la pagination, les interruptions
ou encore les appels systèmes. Enfin, nous décidions ensemble de ce qu’il serait intéressant de développer et qui s’en
occuperait.
En dehors de ces réunions, nous parlions énormément du projet, que ce soit lors des pauses entre les cours ou
sur internet par messagerie instantanée (salons de discussion Jabber).

1.2 Réalisation
Pour pouvoir travailler ensemble sur ce projet, nous avons commencé par créer un dépôt Subversion accessible
sur Internet. Subversion est un outil de gestion de version qui nous permet de travailler ensemble mais chacun de
notre côté. Cela évite d’avoir un dossier partagé et de gérer à la main la fusion de nos travaux.
Nous avons essayé de commenter chacun de nos commits afin de savoir sur quoi porte chacune des modifications
et nous avons fait en sorte que la dernière version commitée n’entraine pas trop de régressions. L’utilisation de cet
outil nous a permit de déterminer à quel moment nous avons introduit certains bugs et ainsi de mieux les localiser.
De part la complexité de ce projet, ceci nous a été très précieux.
Pour développer ce projet, nous avons eu recours à plusieurs logiciels dont en particulier :
– GCC : Le compilateur C.
– GDB : Le debugger, interfaçable avec qemu et bochs.
– QEMU : Une machine virtuelle pour exécuter notre système d’exploitation.
– Bochs : Une autre machine virtuelle.

1.3 Avancement / Répartition du travail


Nous avons travaillé de manière régulière sur le projet comme le montre la figure 1. On peut cependant remarquer
deux plateaux qui correspondent en fait aux vacances.
Nous nous sommes répartis le travail pour essayer d’avancer le plus possible en parallèle. Nous avons tous chacun
une partie que nous maitrisons bien mieux que les autres. Ainsi par exemple, si on a besoin d’intervenir sur la gestion
des processus on va demander à Benjamin, si c’est au sujet de la mémoire on va demander à Maxime, si c’est au
sujet des appels systèmes on va demander à Nicolas et enfin, s’il s’agit de la lecture d’un fichier, on va demander
à Ludovic. Nous fonctionnons donc sur un système composé d’experts dont le rôle est d’expliquer aux autres le
résultat de ses travaux avec pour objectif de fournir les connaissances suffisantes à son utilisation et pour pouvoir
diagnostiquer les problèmes. Ce choix nous a permis d’avancer assez vite et apporte une certaine responsabilité
envers le reste de l’équipe.
Voici un historique un peu simplifié des différents éléments mis en place au cours de ce projet (se référer à la
figure 1) :

1. Prise en main de grub et exécution du Hello World fourni dans la documentation.


2. Mise en place de la GDT, segmentation, du découpage de la mémoire en cadres de page, des exceptions et des
interruptions. Nous réussissons aussi à détecter les périphériques PCI. Nous pouvons aussi lire des caractères
saisis au clavier ! (La pente sur la courbe provient du fait que le travail sur la mémoire et les interruptions a
été fait sur quelques jours sur une autre branche puis fusionné au trunk)
3. Vacances scolaires.
4. Début de la mise en place du changement de contexte et de la pagination.

Rapport projet tutoré Page 1


Lignes

9
17000
8
16000
15000 7

14000 6
13000
12000
11000
5
10000
9000 4
8000 3
7000 2

6000
5000
4000
3000 1

2000
1000

9
Février Mars Avril Mai

Figure 1 – Avancement du projet en lignes de code ajoutées

5. Pagination activée en identity mapping. Mise en place de la lecture sur disquette. Gestion d’évènements
programmés à l’aide du timer. Début kmalloc.
6. Création des appels systèmes. Premier ordonnancement entre deux processus. La souris est gérée. Mise en
place des entrées/sorties avec les streams stdin, stdout et stderr. Le scanf permet maintenant de taper du
texte et de supprimer en cas d’erreur. La prise en charge du FAT permet de parcourir les fichiers et dossiers
à l’intérieur de la disquette.
7. Vacances scolaires.
8. Amélioration des Makefile (makedepend). Correction de bugs principalement.
9. Correction de bugs. Ajout d’une API pour créer une IHM en console. La sortie écran est améliorée pour mieux
gérer le changement entre applications avec Alt+Tab.

Rapport projet tutoré Page 2


2 Technique
2.1 Mémoire
2.1.1 Pagination

Nous avons décidé d’activer la pagination dans notre système d’exploitation pour gérer la mémoire. En effet,
suite à nos recherches bibliographiques, nous avons jugé que la pagination était préférable à la segmentation et c’est
le système utilisé par les principaux systèmes d’exploitation du marché.
Pour rappel, la pagination consiste à découper la mémoire en pages de taille fixe et les placer dans des cadres
de page. Puisque nous travaillons sur une architecture 32 bits, l’espace d’adressage est limité à 4 Gio.
La première étape consiste à découper la mémoire physique en cadres de page de taille 4 Kio. Nous avons eu le
choix entre une taille de 4 Kio, 2 Mio et 4 Mio mais nous avons préféré utiliser la taille la plus fine pour augmenter
la granularité.
Afin de découper la mémoire physique et d’obtenir une liste de cadres utilisés et libres, nous avons simplement
fait une boucle qui itérait sur l’ensemble de la RAM et qui tous les 4 Kio vérifiait si le cadre courant est réservé ou
non. Typiquement, les cadres réservés sont ceux de la mémoire BIOS et du noyau.
Les cadres sont stockés dans deux listes chaı̂nées : used frame pages et free frame pages et remplis initialement
par la fonction memory setup().
Deux fonctions permettent de réserver et de libérer un cadre de page pour y stocker une page. D’autres fonctions
servent à pouvoir itérer sur les cadres de la mémoire physique mais elles sont moins importantes.
Une fois la mémoire physique découpée en cadres, nous avons pu activer la pagination. Comme la segmentation,
c’est la MMU qui s’occupe de la traduction des adresses mais il faut pour cela lui donner l’adresse du répertoire de
pages. Comme nous pouvons le voir sur la figure 2, la MMU du x86 travaille sur plusieurs niveaux de traduction.
Il y a tout d’abord un répertoire contenant les adresses des tables de pages et c’est dans ces dernières que nous
retrouvons les adresses des pages. On peut remarquer que le répertoire et les tables sont des tableaux de 1024 entrées
de chacune 4 octets ce qui fait des blocs de 4 Kio, soit la taille d’une page. Ces tableaux doivent être alignés sur un
cadre de page ce qui permet de les adresser en seulement 20 bits (4 Gio c’est 1024 × 1024 × 4 Kio soit 220 × 4 Kio).
Pour activer la pagination, il faut faire deux choses :
– Charger l’adresse du répertoire dans le registre CR3
– Passer le bit ”paging enable” du registre CR0 à 1.

Une fois la pagination activée, il n’est plus possible de manipuler des adresses physiques, il convient alors de
manipuler des adresses virtuelles. Cela peut avoir des conséquences inattendues, et c’est pour cette raison que nous
avons mis en place un système parfois appelé ”Identity mapping” et qui consiste à placer les pages dans les cadres
tel qu’une adresse physique soit identique à une adresse virtuelle. Ensuite il sera bien sûr possible de casser cette
égalité.
Une fois la mémoire mappée, nous donnons au répertoire des pages une adresse particulière afin d’y accéder et
pouvoir modifier les tables de pages facilement. L’idée est la suivante : nous avons choisi une entrée du répertoire
(arbitrairement la dernière) et nous l’avons fait pointer vers l’adresse du répertoire de page. Ainsi l’adresse virtuelle
0xFFFFF00 est l’adresse du répertoire et 0xFFC + 1024 * index page table est l’adresse d’une table de page.
Nous n’avons pas eu le temps de finir la mise en place de la pagination. Il nous reste seulement une dernière
étape qui consiste à créer un répertoire de page par processus et recharger le répertoire de page du processus à
chaque changement de contexte. Actuellement, tous les processus partagent le même répertoire ce qui réduit la
sécurité du système (possibilité pour un processus de taper dans la mémoire d’un autre).
Pour la suite, il est important de garder à l’esprit que c’est la MMU qui se charge de la traduction d’adresse.
Ainsi, des zones mémoires peuvent être contigües dans la mémoire virtuelle alors que dans la mémoire physique il
en est tout autrement.

2.1.2 Malloc

Après avoir mis en place la pagination, on peut maintenant se lancer dans l’écriture de fonctions permettant l’al-
location dynamique de la mémoire virtuelle. Cette allocation dynamique est découpée en deux parties. La première

Rapport projet tutoré Page 3


31 21 11 0

Répertoire de tables de pages Indice de Offset


la page dans la page Page (4 Kio)
(1024 entrées)
Indice de
la table Table de pages
(1024 entrées)

@phys table (20 bits)


+ flags (12 bits)

@phys page (20 bits)


+ flags (12 bits)

CR3

Figure 2 – Traduction d’une adresse linéaire en une adresse physique par la MMU

est le gestionnaires des pages libres et utilisées en mémoire virtuelle (c’est-à-dire dans le répertoire de page courant)
que l’on appellera la VMM (Virtual Memory Manager). La seconde partie (une seconde couche) s’occupe de gérer
les blocs de mémoire allouées ou libres (à l’octet près cette fois). On appellera cette seconde partie : kmalloc.

Virtual Memory Manager La VMM s’occupe donc de savoir quelles sont les pages libres de la mémoire virtuelle
(c’est-à-dire les pages non mappées à des pages physiques) et quelles sont celles déjà utilisées. Pour cela, on utilise
deux listes doublements chainées : une pour les pages libres, et une pour les pages utilisées. Chaque nœud de ces
listes décrit un bloc de page (en fait il contient simplement le nombre de pages du bloc). Chacun de ces nœuds se
trouve au début du bloc de page qu’il décrit. L’adresse (virtuelle) d’un nœud est donc aussi le début du bloc, il
suffit d’ajouter la taille d’un nœud (sizeof(struct noeud) = 16 octets) pour obtenir l’adresse du début du bloc. Les
deux listes sont ordonnées par adresses de blocs de pages croissantes.
Nous allons maintenant décrire le processus d’allocation de pages par la VMM. Lorsque la fonction allo-
cate new pages(nb pages) est appelée, la VMM parcourt la liste des pages libres pour voir s’il existe un bloc libre
de taille assez importante (c’est-à-dire possédant au moins nb pages libres). S’il n’existe pas de tel bloc, la VMM
va créer une nouvelle zone libre en haut du tas. Ensuite la VMM va réserver nb pages pages physique et mapper
chacune de ces pages physiques aux pages virtuelles libres du bloc en question. Par la suite, la VMM va créer un
nouveau nœud descripteur de bloc de pages et le placer au début du bloc (comme vu dans le paragraphe précèdent).
Elle va ensuite rajouter ce nœud dans la liste des blocs de pages utilisées (et éventuellement le retirer de la liste des
blocs de pages libres). Cet algorithme est appelé First Fit, c’est l’un des plus simples à mettre en place et il a pour
avantage d’être très rapide. Il est particulièrement adapté pour les systèmes qui disposent de plus de mémoire que
nécessaire car il ne parcourt pas de longues structures afin de trouver l’emplacement optimal.
Pour mettre cette gestion des pages un peu plus au clair, on peut voir un exemple de ce à quoi peut ressembler
la mémoire virtuelle sur la figure 3. En bleu, les structures décrivant les blocs de pages, en gris les blocs de pages
utilisées, et en blanc les blocs de pages libres. Sur ce schéma, on peut voir trois blocs de pages. La première (en
partant du bas de la mémoire) est donc un bloc de pages utilisées, la structure au début de ce bloc est donc le
premier nœud de la liste des blocs de pages utilisées. Ce nœud pointe donc vers le prochain bloc de pages utilisées.
Le bloc de pages libres au milieu est le seul de sa liste et ne pointe donc vers rien.
Dans cet exemple, si le noyau demande l’allocation de 3 pages, celles-ci seront placées dans le bloc libre qui sera
coupé en deux. Un nouveau bloc de pages utilisées (contenant 3 pages) sera créé et le bloc libre sera remplacé par

Rapport projet tutoré Page 4


Fin de la zone gérée
par vmm.c (vmm top)

Bloc de
2 pages

Zone où les pages virtuelles


ne sont pas mappées à
des pages physiques, Bloc de
c’est donc des zones ”libres” 4 pages

Zone où les pages virtuelles


sont mappées à
des pages physiques (contigües), Bloc de
c’est donc des zones ”utilisées” 3 pages = 3 * 4096
= 12288 octets
Structure décrivant le bloc (16 o)

Début de la zone
gérée par vmm.c
Noyau - Identity Mapped

Figure 3 – Mémoire Virtuelle

un nouveau bloc de taille 1. Si le noyau demande l’allocation de 5 pages, la VMM va devoir agrandir le tas, en
créant une nouvelle zone libre en haut du tas.

kmalloc Il n’y a pas lieu de s’étendre beaucoup sur kmalloc, vu qu’il marche sur le même principe que la VMM :
deux listes doublements chainées représentant les zones de mémoires libres et utilisées. Des structures décrivant les
zones de mémoires au début de ces zones mémoires.
Les deux différences entre kmalloc et la VMM sont que kmalloc travaille au niveau de l’octet (au lieu des pages)
et que kmalloc n’a pas besoin de mapper les pages virtuelles aux pages physiques.

2.2 Gestion des interruptions et Exceptions


2.2.1 Mise en place

Pour gérer les interruptions et les exceptions, nous avons eu besoin de configurer l’Interrupt Descriptor Table
(IDT). L’IDT est un tableau qui contient en particulier l’adresse des Interrupt Service Routines (ISR). Ces routines
sont simplement les fonctions à exécuter lorsqu’une interruption est levée.
Avant toute chose, pour pouvoir supporter les interruptions matérielles, il est aussi nécessaire de configurer le
Programmable Interrupt Controller (PIC). Sur l’architecture x86, il s’agit du i8259 et ils sont au nombre de deux.
L’un est le maı̂tre et l’autre est l’esclave et ils sont mis en cascade. Chacun d’eux possède 8 entrées ce qui fait 15
IRQ (et non 16 puisqu’ils sont en cascade).
Le rôle du PIC est de gérer les interruptions matérielles, et l’un des points important de la configuration est le
mapping des interruptions. C’est en effet lors de cette configuration que l’on va lui dire d’utiliser les interruptions
processeur entre 32 et 47.

Rapport projet tutoré Page 5


Ensuite nous pouvons configurer la gestion des interruptions et des exceptions. Pour cela nous avons découpé
en trois partie la mise en place d’une interruption. Sur la couche la plus basse se trouve la configuration de l’IDT
qui est commune aux interruptions et exceptions. Sur cette couche repose les fonctions d’activation et désactivation
des interruptions et exceptions (dans deux fichiers distincts). Et enfin, nous avons à côté la définition de wrappers
qui permettent d’encapsuler les appels des handlers en sauvegardant le contexte et en le rétablissant comme il faut
après. Nous configurerons donc les entrées de l’IDT de sorte à appeler les wrappers et non directement les handlers.

2.2.2 Rôle du wrapper

Le wrapper va commencer par faire une copie des registres avant d’exécuter le handler. Une fois que le handler
a terminé son exécution, le wrapper va restaurer l’état des registres et pouvoir sortir de l’interruption. Selon le type
d’interruption, le wrapper va faire d’autres actions. Dans le cas des exceptions, il faut appeler la handler avec le
code d’erreur. Ce code a été placé dans la pile juste avant que le wrapper soit executé. Dans le cas des interruptions
matérielles, il faut envoyer le signal End Of Interrupt au PIC.

2.2.3 Mapping des exceptions et interruptions

Le processeur x86 permet de gérer jusqu’à 256 interruptions. Nous avons décidé de réserver les 32 premières
entrées pour les exceptions et les 32 suivantes pour les interruptions dont les 16 premières sont des interruptions
matérielles. Le mapping des interruptions est présenté dans la figure 4.

0 3132 4748 63 255

Exceptions Interruptions matérielles Interruptions logicielles Non utilisé

Figure 4 – Mapping des interruptions dans l’IDT.

2.2.4 Liste des interruptions matérielles

Sur le PIC maı̂tre :


– IRQ 0 : Horloge système
– IRQ 1 : Clavier
– IRQ 2 : Signal cascadé du PIC esclave (IRQs 8 à 15)
– IRQ 3 : Port série 2 ou 4
– IRQ 4 : Port série 1 ou 3
– IRQ 5 : Port LPT 2 ou carte son
– IRQ 6 : Lecteur de disquette
– IRQ 7 : Port LPT 1
PIC Esclave :
– IRQ 8 : Horloge temps réel
– IRQ 9:
– IRQ 10 :
– IRQ 11 :
– IRQ 12 : Souris sur connecteur PS/2
– IRQ 13 : Coprocesseur math
– IRQ 14 : Disque dur primaire
– IRQ 15 : Disque dur secondaire

Rapport projet tutoré Page 6


2.3 Gestion de l’horloge
2.3.1 Fonctionnement de l’horloge

Principe de fonctionnement : La gestion du temps s’appuie sur une horloge périphérique appelée RTC (Real
Time Clock) ainsi que sur le compteur programmable PIT (Programmable Interval Timer) I8254. À l’initialisation
nous allons interroger l’horloge afin d’obtenir la date et l’heure actuelle, à partir de ce moment nous n’aurons plus
besoin de consulter la RTC. En effet, une fois l’heure et la date obtenues, la nouvelle heure est extrapolée grâce au
PIT. Pour cela nous paramétrons le timer pour lancer une interruption au bout d’un temps T correspondant à un
tick système, à l’issue de cette interruption le temps système est incrémenté et le PIT est reconfiguré pour un autre
tick. Le temps écoulé en secondes est calculé à partir de la durée d’un tick.

Lecture de l’horloge : La communication avec la RTC se fait en écrivant une requête sur le port 0x70 et en
lisant la réponse sur le port 0x71, les codes de requête sont décrits dans la table 1
La requête ”jour de la semaine” demande une réponse du type Lundi, Mardi..., Samedi, Dimanche. Tandis que la

signification alarme code


seconde 0x00
seconde X 0x01
minute 0x02
minute X 0x03
heure 0x04
heure X 0x05
jour de la semaine 0x06
jour du mois 0x07
mois 0x08
année 0x08

Table 1 – Codes de requête de la RTC

requête ”jour du mois” demande une réponse du type 1,2,...,30,31.


La RTC répond non pas dans un format binaire pur mais dans un format dit décimal codé en binaire (BCD). Dans
ce format les décimales qui constituent le mot en base 10 sont encodées unes à unes sur 4 bits.
Exemple : 27 encodé en binaire donne 16+8+2+1 soit 0001 1011, 27 encodé en BCD donne 2 soit 0010 puis 7 soit
0111 donc 0010 0111.
Nous avons donc dû coder une fonction de conversion pour traduire les valeurs obtenues grâce à la RTC.

Le champ alarme dans la table indique les requêtes concernant, non pas la valeur de l’horloge mais la fonction
alarme de la RTC, en effet l’horloge peut être configurée pour allumer automatiquement l’ordinateur à une heure
donnée.

Configuration du compteur : Le fonctionnement d’un PIT est le suivant, un PIT contient généralement un
registre incrémenté à intervalle régulier. Lorsque la valeur du registre provoque un overflow, le PIT génère une
interruption (ici IRQ0). Configurer un compteur consiste donc à mettre en place un handler sur l’interruption qui
sera levée puis à paramétrer le registre de manière à ce que l’interruption arrive au moment voulu. La configuration
de l’I8254 se fait à travers le port 0x40 pour positionner le registre de comparaison et le port 0x43 pour régler plus
précisément le fonctionnement du compteur.

2.3.2 Conversions de dates et calendriers

Au niveau du système il existe trois échelles de temps.


La première s’exprime en ticks depuis le démarrage du système. Le tick représente l’unité de temps la plus
fine manipulable sur le système, la durée d’un tick est ajustable, néanmoins, afin d’éviter de saturer le processeur
avec les interruptions d’horloge, on évite de spécifier un tick trop court, ici le tick est de 1 milliseconde, ceci est

Rapport projet tutoré Page 7


suffisamment précis pour notre système d’exploitation. Cette échelle est principalement utilisée pour une gestion
du temps de bas niveau, par exemple pour l’ordonnancement.
L’échelle suivante s’exprime en secondes à partir d’une date dite Epoch, dans notre système il s’agit du 1er
Janvier de l’an 2000. Cette échelle est utilisée par les programmes pour garder le compte des dates.
La dernière échelle est celle que nous utilisons tous, elle s’exprime en secondes, minutes, heures, date. La dif-
ficulté repose principalement dans le calcul des années bissextiles et du jour de la semaine, cette échelle s’adresse
généralement à l’utilisateur.

2.3.3 Planifications d’évènements

Afin d’implémenter la fonction sleep et l’ordonnanceur de la manière la moins scrutative possible nous avons
inclus un gestionnaire de planification d’évènements dans l’horloge. Le fonctionnement repose sur l’utilisation d’un
tas, celui-ci stocke les différents évènements planifiés. A chaque tick d’horloge il suffit alors de vérifier s’il y a un
évènement à déclencher et le déclencher le cas échéant. La gestion des ticks s’exécutant en ring 0, le code à exécuter
doit pour des mesures de sécurité être du code kernel, il est donc proscrit de permettre à une application en user
space de planifier directement l’appel d’une de ses fonctions, la planification d’événements se doit donc d’être utilisée
par le kernel ou utilisée indirectement au travers des appels systèmes. Une autre exigence à prendre en compte est
la rapidité d’exécution du handler, en effet, celui-ci s’exécute dans le cadre d’une interruption d’horloge.

Une fois la planification d’évènements en place, il devient dès lors aisé d’implémenter la fonction sleep, cela revient
à planifier une fonction qui va réveiller le programme puis d’endormir le programme en le passant en statut idle.

2.4 Gestion des processus


La gestion des processus encadre tout ce qui touche de près ou de loin aux processus dans le système d’exploitation.
Ceci comprend la manière dont sont créés, stockés et détruits les processus, mais aussi comment l’ordonnancement est
géré (algorithme d’ordonnancement, mécanisme de changement de contexte) et comment les processus interagissent
avec le système (appels systèmes).

Dans notre système d’exploitation, chaque processus est représenté à travers une structure contenant un certain
nombre d’informations :
– Des identifiants (Pid, Nom )
– État du processus (Idle, running, terminated...)
– État des registres (permettant le changement de contexte)
– Différents descripteurs pour les entrées/sorties
– Informations diverses (temps d’exécution par exemple).
La création d’un processus revient donc à initialiser une telle structure, et à l’ajouter à la liste des processus. Il
faut également, durant cette initialisation, réserver la mémoire pour les piles utilisateur et noyau du processus.

Une fois qu’au moins un processus a été créé, il est possible d’activer l’ordonnanceur afin d’exécuter les processus
contenus dans la liste, nous allons donc voir les différents mécanismes liés à cette exécution.

2.4.1 Changement de contexte

Le changement de contexte est un mécanisme clé dans l’exécution et l’ordonnancement des tâches. Il permet d’une
part, à partir de l’état d’exécution d’un processus (c’est à dire des valeurs de ces registres), rétablir son exécution,
tout en sauvegardant l’état d’exécution actuel. Il doit permettre également de changer le niveau de privilèges. En
effet, l’ordonnanceur étant exécuté avec les privilèges du noyau (ring 0), le changement de contexte doit permettre
de repasser en privilège utilisateur (ring 3).

Rapport projet tutoré Page 8


La première approche pour résoudre ce problème a été de chercher dans la documentation Intel. Il existe en
effet un mécanisme permettant d’effectuer le changement de contexte grâce à des mécanismes du processeur. Cette
méthode, appelée ”Changement de contexte Hardware” souffre malheureusement de certains défauts :
– À chaque processus doit être associé une entrée dans la GDT. Le nombre de processus maximum est donc
fortement restreint.
– Bien que les mécanismes soit gérés par le matériel, ils sont très laborieux à mettre en œuvre.
Cette technique ne correspondant pas à nos besoins, nous avons opté pour un changement de contexte dit ”software”,
qui est utilisé dans la plupart des systèmes d’exploitation.

Principe du changement de contexte logiciel :


L’idée principale du changement de contexte logiciel est de détourner l’utilisation de l’instruction de retour
d’interruption ”iret”. Afin de mieux comprendre comment cela fonctionne, nous allons d’abord regarder en détail
ce que fait le processeur lors d’une interruption, et lorsqu’il rencontre un ”iret”.
Lors d’une interruption, le processeur sauvegarde automatiquement sur la pile noyau différents registres nécessaires
au retour dans le contexte utilisateur :
– SS (Stack Segment)
– ESP (Stack Pointer)
– EFLAGS
– CS (Code Segment)
– EIP (Instruction Pointer)
Puis passe en ring 0.

La figure 5 illustre l’état de la pile après l’interruption.

SS

ESP

EFLAGS

CS

EIP
ESP

Figure 5 – Pile système après une interruption

En partant de là, la seule chose que l’instruction iret a à faire est :


– Dépiler EIP et CS.
– Dépiler EFLAGS.
– En cas de changement de privilèges, dépiler ESP et SS et changer le niveau privilèges (le changement de
privilèges est détecté en comparant le CS actuel et le nouveau CS)

Imaginons maintenant que nous sommes en ring 0, et que nous voulons exécuter un processus en ring 3. Il suffit
simplement de mettre les registres eax, ebx, ecx, et edx au valeurs correspondantes pour le processus, puis d’empiler,
de manière logicielle, SS, ESP, EFLAGS, CS et EIP correspondant au processus, pour imiter une interruption. Ceci
fait, il ne reste plus qu’à exécuter une instruction iret, qui dépilera les valeurs comme si une interruption avait eu
lieu, et changera le privilège en ring 3.

Rapport projet tutoré Page 9


Nous avons maintenant à disposition un mécanisme permettant à partir d’un état d’exécution quelconque, et
de privilège quelconque, de reprendre l’exécution d’un processus. Nous allons maintenant voir comment mettre ce
mécanisme en œuvre pour ordonnancer des taches.

2.4.2 Ordonnancement

Afin de pouvoir exécuter plusieurs tâches en parallèle il faut ce que l’on appelle un ordonnanceur, l’ordonnanceur
est chargé de distribuer le temps d’exécution sur le processeur des différents processus. Il s’agit d’un des éléments
les plus importants d’un système d’exploitation, à terme, un mauvais ordonnanceur implique systématiquement de
mauvaises performances.

Nous n’avons pas eu le temps d’implémenter un ordonnanceur complexe, nous avons simplement opté pour
un ordonnancement de type tourniquet. Cet ordonnanceur ne gère pas les priorités, néanmoins il est capable de
n’exécuter que les processus actifs et non les processus en état d’attente. Le principe de fonctionnement (Voir Figure
6) est relativement simple : les processus étant placés dans une liste, à chaque exécution de l’ordonnanceur on prend
le prochain élément de la liste en ignorant les processus non exécutables (en pause, terminé...). Avant d’exécuter
le processus sélectionné, l’ordonnanceur prend soin de faire en sorte qu’il sera bien rappelé au bout du quantum
de temps alloué au processus. Le mécanisme assurant l’appel régulier de l’ordonnanceur utilise le gestionnaire de
planification d’événements vu précédemment.

Début

Obtenir processus courant

non
Courant à
l’etat running

oui

Sauvegarde du contexte

Mise à jour de la durée de la tâche

Recherche du
prochain processus à
executer

non
Aucune tâche
à executer

oui Mise en place de la pile


Programmation de la prochaine
exécution du scheduler
Programmation de la prochaine
exécution du scheduler
Blocage
Réaffectation de stdin,
stdout, stderr

Changement de contexte

Figure 6 – Déroulement de l’ordonnancement

Rapport projet tutoré Page 10


Fonctionnement de l’ordonnanceur L’ordonnanceur est appelé suite à une interruption, il s’exécute donc en
ring 0. À l’appel de l’ordonnanceur, le contexte du process en cours d’exécution à été sauvegardé dans la pile noyau,
registres ss, esp, eflags, cs et eip, ainsi que l’état de des registres eax, ebx, ecx, edx, ebp, esi, edi, ds, es, fs et gs.
Voir Figure 7 pour une illustration de l’état de la pile à l’appel de l’ordonnanceur.

ss
esp

eflags Empilé par le mécanisme d’interruption

cs

eip
eax
ecx

edx

ebx
esp kernel

ebp

esi Empilé aprés l’interruption (instructions pusha et push)

edi

ds
es

fs
gs

fs
eip kernel
Empilé à l’appel de l’ordonnanceur
ebp kernel
EBP

Figure 7 – Illustration de l’état de la pile noyau à l’appel de l’ordonnanceur

L’ordonnanceur va alors s’exécuter de la manière suivante :


1. Récupère dans la pile noyau la valeur des registres ss, esp, eflags, cs, eip, eax, ebx, ecx, edx, ebp, esi, edi, ds,
es, fs et gs et les sauvegarde dans la structure du process.
2. Cherche un processus à exécuter dans la liste des processus.
3. Positionne le pointeur de pile sur la pile du nouveau process.
4. Empile le contexte du nouveau process (valeurs des registres ss, esp, eflags, cs, eip).
5. Met à jour les registres eax, ebx, ecx, edx, ebp, esi, edi, ds, es, fs et gs avec les nouvelles valeurs.
6. Exécute l’instruction iret.

2.4.3 Appels systèmes

Comme nous l’avons expliqué plus haut, les processus sont exécutés avec un privilège utilisateur, grâce au
mécanisme de changement de contexte logiciel que nous avons mis en œuvre. Cependant il peut être utile par
moment d’exécuter certaines portions de code avec un privilège système, dans le cas par exemple de certains accès
bas niveau. Pour réaliser cela, nous avons mis en place ce que l’on nomme des appels systèmes.
Pour faire cela, nous utilisons les interruptions logicielles. A priori, on pourrait créer une interruption par appel
système. Ceci pose cependant un gros problème de scalabilité, car ceci nous limite directement dans le nombre
d’appels systèmes possibles.

Rapport projet tutoré Page 11


La solution qui se propose est donc de n’utiliser qu’une interruption. Le handler de cette interruption aurait
alors pour rôle d’exécuter la fonction demandée. Pour cela, on mobilise les registres à usage général pour informer
le handler de ce qu’il doit faire :

Registre Usage Remarque


EAX Numero de la fonction à exécuter Obligatoire
EBX Paramètre 1 Optionnel
ECX Paramètre 2 Optionnel
EDX Paramètre 3 Optionnel

Table 2 – Registres utilisés par les appels systèmes

Dans la pratique, on maintient coté noyau une liste des différents appels systèmes. Quand le handler reçoit un
appel système, il vérifie si celui-ci est dans la liste : si c’est le cas, il l’exécute, sinon il renvoit une erreur.
Coté espace utilisateur, il est conseillé de fournir une interface pour chaque appel système, afin d’en simplifier
l’utilisation.

2.5 Pilotes
2.5.1 Disquette

Notre système d’exploitation est, pour le moment, stocké sur un support de type disquette. Le pilote disquette
est donc primordial pour pouvoir accéder depuis l’OS aux fichiers présents sur ce type de support si l’on veut par
la suite, par exemple, lancer des exécutables. Nous allons donc détailler ici le fonctionnement du pilote permettant
d’utiliser le lecteur de disquette.

Adressage sur les supports de stockage Nous allons tout d’abord commencer par un peu de théorie sur la
manière dont sont adressées les données dans les supports de stockage de type disquette ou disque dur.

Le secteur de donnée et la plus petite unité de données transférable (en lecture ou en écriture) par un média de
stockage. La taille d’un secteur est le plus souvent de 512 octets mais certains supports, comme les disques optiques,
emploient d’autres valeurs comme 1024 octets ou 2048 octets. Un adressage permet de désigner de façon unique
un secteur du média de stockage. Il existe plusieurs types d’adressage de secteur de données dont notamment ceux
décrits si après.

Cylinder/Head/Sector CHS L’adressage en CHS est un moyen d’adresser les secteurs de données stockés
sur une disquette ou un disque dur. Il est basé en fait sur le fonctionnement général de ce type de support et dépend
de la géométrie physique du support en question.
Les données sont physiquement stockées sur un ensemble de plateaux en rotation (un seul pour la disquette).
Les têtes de lecture sont au nombre de 2 par plateau (une de chaque coté), toutes les têtes se déplacent en même
temps suivant le même axe de rotation. Une piste correspond à l’ensemble des secteurs parcourus par une tête à une
position donnée. Un cylindre correspond en fait à l’ensemble des pistes lues par les têtes pour une position donnée.
Voir Figure 8 pour une illustration.

Rapport projet tutoré Page 12


Piste

Secteur
Tête

Cylindre

Figure 8 – Illustration adressage CHS

Un secteur est donc adressé de la manière suivante : le numéro du cylindre sur lequel il se trouve C, le numéro
de la tête de lecture qui permet d’accéder à la piste sur laquelle il se trouve H et le numéro du secteur sur cette
piste S.
Ce type d’adressage dépend donc des caractéristiques du support à savoir le nombre de têtes, de cylindres et de
secteurs par piste.

Logical Block Addressing LBA Ce type d’adressage ne dépend pas du média de stockage, en particulier
de sa géométrie physique. L’adresse LBA d’un secteur de données est simplement un numéro unique pris dans
l’intervalle [0..N [, où N est le nombre total de secteurs du support.

Conversion LBA/CHS Comme dit précédemment l’adressage CHS est peu pratique lorsqu’on veut adresser
un secteur de manière générale sur un support sans avoir les informations sur ses caractéristiques. Le LBA est
l’adressage qui est utilisé notamment dans les systèmes de fichiers pour désigner un secteur de donnée sur le
support. Il est donc nécessaire ensuite d’effectuer une conversion (voir Figure 9) pour pouvoir communiquer avec le
média de stockage si c’est un disque dur ou une disquette.

Cylinder = sector_LBA / (nb_sectors_per_track*nb_head);


Head = (sector_LBA % (nb_sectors_per_track*nb_head)) / (nb_sectors_per_track);
Sector = (sector_LBA % (nb_sectors_per_track*nb_head)) % (nb_sectors_per_track);

Figure 9 – Conversion adresse LBA vers CHS

Communication avec le contrôleur

Nous allons maintenant voir de quelle manière le pilote communique avec le contrôleur disquette.
L’intégralité des communications avec le contrôleur se fait en écrivant et en lisant dans des registres, accessibles via
le port I/O.
Voici les registres mis à disposition par le contrôleur :

Les adresses de ces registres sont exprimées relativement à une adresse de base qui est est 03f0h pour le contrôleur
principal.
Nous n’utiliserons en fait que quelques un de ces registres, en particulier :
– Data FIFO : permet d’envoyer des commandes au contrôleur.
– Main status register : permet de récupérer des informations générales sur le status du contrôleur
– Digital output register : contrôle du moteur, activation du DMA et de l’IRQ.
– Configuration control register : définir le débit de transfert.

Rapport projet tutoré Page 13


Registre Adresse R/W
Status register A 0h R
Status register B 1h R
Digital output register 2h W
Tape drive register 3h W
Main status register 4h R
Datarate select register 4h W
Data FIFO 5h R/W
Digital input register 7h R
Configuration control register 7h W

Table 3 – Registres du contrôleur disquette

File de commandes :
Alors que la plupart des registres ne permettent que de configurer ou lire des flags de contrôle, le registre Data
FIFO a un rôle beaucoup plus important dans le fonctionnement du contrôleur. Il permet en effet de transmettre
au contrôleur un certain nombre de commandes, également des paramètre pour ces commandes, mais également de
recevoir des réponses à ces commandes. La documentation fournit un grand nombre de commandes. Notre pilote
étant plutôt minimaliste, nous n’avons utilisé que les commandes suivantes :

Commande Numéro Description


Specify 3h Donner au contrôleur des informations sur le lecteur auquel il est connecté
Write data 5h Écrit un ou plusieurs secteurs sur la disquette
Read data 6h Lit un ou plusieurs secteurs de la disquette
Recalibrate 7h Positionne la tête de lecture/écriture en butée sur le cylindre 0
Sense interrupt 8h Récupère des information après qu’une interruption ait été levée par le contrôleur
Seek Fh Déplacer la tête de lecture/écriture à un cylindre donné
Version 10h Déterminer la version du contrôleur

Table 4 – Liste des principales commandes utilisées dans le pilote

La majorité des commandes envoyées au contrôleur mettent un certain délai avant de s’exécuter (à cause
des contraintes matérielles intrinsèques). Afin de savoir quand une commande a bien été traitée, et donc savoir
quand il est possible de récupérer les données dans la FIFO, le contrôleur possède une interruption dédiée (IRQ 6),
qu’il lève dès qu’une commande est exécutée. Quand cette interruption est levée, il est possible de récupérer des
informations supplémentaires via la commande Sense Interrupt, pour savoir par exemple à quelle commande répond
l’interruption levée.
Maintenant que nous savons communiquer avec le contrôleur, nous allons maintenant voir les différentes phases
dans l’utilisation du lecteur disquette.

Initialisation du lecteur :
L’initialisation du lecteur doit réaliser les opérations suivantes :
– Activer l’IRQ.
– Régler les caractéristiques du lecteur via le Configuration Control Register et la commande Specify.
– Calibrer le lecteur (activation du moteur + commande Recalibrate)
Ceci permet d’initialiser le lecteur lui même, mais ce n’est pas encore suffisant dans notre cas. En effet, pour pouvoir
transfert des données vers et depuis la disquette avec un débit respectable, nous allons utiliser un DMA (Direct
Memory Access) dédié au lecteur disquette, appelé ISA DMA. L’initialisation de ce DMA sortant un peu du cadre
de la description du pilote pour lecteur disquette, nous ne la détaillerons pas ici.

Lecture et écriture des données :


Le lecteur est désormais utilisable, nous allons maintenant voir comment nous procédons pour lire ou écrire des
données sur la disquette. La méthode que nous avons utilisé consiste à ne lire que des cylindres au lieu de lire

Rapport projet tutoré Page 14


secteur par secteur. Grâce à un mode de fonctionnement du lecteur, il nous est possible de lire deux cylindres à la
fois (grâce aux deux têtes). Ainsi nous gardons toujours en RAM le contenu de ces deux cylindres, et l’utilisateur
peut lire les secteurs se trouvant dans ces cylindres.
L’algorithme est le suivant :

lire_secteur( secteur )
si secteur est dans cylindre_actuel
retourner valeur du secteur se trouvant dans le tampon en RAM
sinon
lire_cylindre(cylindre contenant secteur)
retourner valeur du secteur se trouvant dans le tampon en RAM
fin si
fin lire_secteur

ecrire_secteur( secteur, data )


si secteur n’est pas dans cylindre_actuel
lire_cylindre(cylindre contenant secteur)
fin si

copier data dans l’image du cylindre actuel en RAM


ecrire_cylindre(cylindre_actuel)
fin ecrire_secteur

Ainsi la lecture de secteurs adjacents en mémoire devient très rapide, puisque cela revient à faire un accès disque,
puis que des accès mémoire.

2.5.2 Gestion du clavier

Lors de l’appui d’une touche sur le clavier, l’IRQ 1 est déclenchée. La routine d’interruption se charge de lire
le code de la touche tapée sur le port 0x60. Le code la touche est alors interprétée en un caractère qui est ensuite
empilé dans le buffer du processus au premier plan.
Le code des touches est appelé ”Scan Code” et il existe trois modes possibles (appelés Set 1, Set 2 et Set 3). Le
set 1 est utilisé par les IBM PC XT et les plus anciens, et le set 2 est utilisé par les IBM PC AT et plus récent. Le
set 3 n’est que rarement utilisé. Pour des raisons de compatibilité, sur tous les PC avec des claviers IBM PC AT
ou PS/2, par défaut l’ordinateur traduit le code set 2 en set 1 sauf si on lui dit de se comporter autrement. Nous
avons donc décidé d’utiliser le set 1.
Lors de l’appui d’une touche, de un à trois octets sont générés. Les scancodes de deux octets commencent par
0xE0 et les scancodes de trois octets commencent par 0xE1. Ceci nous permet donc de savoir s’il y a d’autres octets
à lire avant d’interpréter le code. Lorsque la touche est relâchée, un code similaire est envoyé qui a pour particularité
d’avoir un certain bit à 1 ce qui ajoute la valeur 0x80 au code.
Pour décoder une touche, nous utilisons une table de correspondance pour les caractères qui doivent être placés
dans le buffer du stream stdin du processus au premier plan. Pour les touches modificatrices, nous tenons à jour des
variables pour connaitre l’état actuel du clavier. Cela permet, par exemple de savoir si nous sommes en majuscule
ou en minuscule.

2.5.3 Pilote souris

Le fonctionnement de la souris est globalement très proche de celui du clavier, lorsqu’un nouveau paquet est
disponible sur le port 0x60, l’IRQ 12 est lancée. Il s’agit du même port que le clavier, néanmoins, grâce au numéro
d’interruption il est possible de savoir que les données proviennent bien de la souris.
Afin de faire fonctionner la souris il faut la régler, pour cela nous envoyons sur le port 0x64 les codes 0xF6
(Utiliser les options par défaut) et 0xF4 (Activer l’envoi des informations). La souris va donc commencer à envoyer
régulièrement des paquets contenant des informations sur son état.

Rapport projet tutoré Page 15


La souris envoie ainsi à tour de rôle 3 types de paquets. Il est possible de savoir quel type de paquet on s’apprête
à lire en incrémentant un compteur à chaque paquet reçu.
Le premier paquet contient principalement des informations sur les boutons de la souris, il contient également
les bits de signes des axes et les champs indiquant qu’un overflow a eu lieu sur le mouvement de la souris. Un
overflow a peu de chance d’arriver et si il arrive cela signifie que la souris a décroché, on peut dans ce cas ignorer
ce paquet ainsi que les 2 paquets suivants puisque les informations de mouvement n’ont plus aucun sens.

7 6 5 4 3 2 1 0
overflow overflow bit de bit de Toujours 1 Bouton Bouton Bouton
Y X signe Y signe X milieu Droit Gauche

Table 5 – Paquet 1

Les deux paquets suivants donnent le mouvement effectué par la souris selon les 2 axes depuis la dernière mesure.
Grâce au bit de signe envoyé dans le premier paquet on obtient une valeur allant de -255 à +255.

7-0 7-0
Mouvement X Mouvement Y

Table 6 – Paquets 2 et 3

Parmi les améliorations que l’on pourrait apporter à ce driver souris on peut citer entre autres le support USB.
Actuellement nous ne pouvons utiliser que des souris branchées sur le port PS2, le support des souris USB nécessite
une gestion préalable de la pile USB, or nous n’avons pas implémenté cette pile. Néanmoins, en supposant que nous
ayons cette pile il serait facile de supporter les souris USB. En effet celles-ci émulent le fonctionnement des souris
PS2, des lors, la seule différence entre le driver PS2 et le driver USB est la ligne d’IRQ utilisée, les souris USB
déclenchant une IRQ depuis le bus USB au lieu de déclencher l’IRQ 12. Il serait également bienvenu de supporter
les boutons additionnels ainsi que la molette.

Problèmes rencontrés Après initialisation, le premier paquet envoyé par la souris ne correspond pas à la doc-
umentation que nous avons trouvé, de plus nous n’avons trouvé mention de ce problème nul part. Étant donné que
nous pouvons faire fonctionner la souris sans ce paquet nous avons décidé de l’ignorer tout simplement.

2.6 Système de fichier


2.6.1 Système de fichier FAT

FAT, acronyme anglais de File Allocation Table (table d’allocation de fichiers), est un système de fichiers conçu
par Microsoft. Il fut utilisé notamment sous MS-DOS puis dans la branche 9x de Windows. Les systèmes Windows
actuels, dérivés de la branche NT, utilisent généralement le NTFS pour les disques durs, mais reconnaissent toujours
le FAT car les cartes mémoire des appareils photos numériques ou des baladeurs, de même que leurs mémoires
internes, sont presque toujours au format FAT ainsi que certains supports externes (disques durs USB, clef USB ..).
L’espace mémoire couvert par la partition et divisé en clusters de taille fixe. La taille d’un cluster pour une
partition et choisie au formatage en fonction de la taille de la partition et du type de FAT (FAT12, FAT16 ou
FAT32) la taille minimum étant la taille d’un secteur soit généralement 512 octets.
L’intérêt de diviser la mémoire en clusters et de pouvoir fragmenter l’espace occupé par un fichier ou dossier.
Ainsi un fichier de taille supérieure à un cluster (supérieure à 512o par ex) ne va pas être écrit sur une zone mémoire
continue mais sur plusieurs clusters à différents endroits de la partition. Voir figure 13 pour un exemple.
Cela permet d’optimiser l’utilisation de l’espace mémoire lors d’écritures et d’effacements de fichiers de tailles
différentes. En effet il n’est pas nécessaire de chercher un espace mémoire continue suffisant pour écrire les données
mais il suffit d’utiliser un ensemble de clusters libres, sans se soucier de leur emplacement sur la partition.
L’inconvénient d’un tel système de fichier, mis à part le fait qu’il ne soit pas optimisé pour les petits fichiers
(inférieurs à 512octets), est que les données peuvent devenir très fragmentées. L’inconvénient de la fragmentation
est que cela entraı̂ne des accès successifs à des clusters situés à des endroits éloignés les uns des autres sur le disque
entraı̂nant de nombreux déplacements de têtes qui ralentissent considérablement la lecture ou l’écriture.

Rapport projet tutoré Page 16


2.6.2 Formatage de la Partition

Partition FAT Une partition FAT est divisée en plusieurs zones comme illustré en Figure 10. Après le Secteur de
Boot se trouve la File Allocation Table (FAT) dont l’utilisation est décrite à la section suivante, ensuite une copie de
cette même FAT pour une question de sécurité en cas d’altération de la première, puis une zone réservée au répertoire
racine (de taille fixe en FAT12 et FAT16) et enfin une zone de données avec des fichiers et des sous-répertoires.

Partition FAT

Secteur de Boot

File Allocation Table

File Allocation Table (copie)

Répertoire Racine

Fichiers et Sous-répertoires

Zone réservée
Zone des données

Figure 10 – Partition FAT

Secteur de Boot Au montage de la partition il est nécessaire de récupérer toutes les informations permettant
de déduire les adresses (LBA du premier secteur) de ces différentes zones. Ces informations sont contenues dans Le
Secteur de Boot décrit Figure 11.

Rapport projet tutoré Page 17


Secteur de Boot
Position (octet) Taille (octet) Description
0 3 Saut vers un programme qui va charger le système d’exploitation
3 8 Nom du programme qui a formaté le disque
11 2 Nombre d’octets par secteur (512, 1 024, 2 048 ou 4 096)
13 1 Nombre de secteurs par cluster (1, 2, 4, 8, 16, 32, 64 ou 128)
14 2 Nombre de secteurs réservés
16 1 Nombre de FATs sur le disque (2 par défaut)
17 2 Taille du répertoire racine en nombre d’entrées
19 2 Nombre total de secteurs 16-bit
21 1 Type de disque (0xF8 pour les disques durs, 0xF0 pour les disquettes)
22 2 Taille d’une FAT en secteurs
24 2 Nombre de secteurs par piste
26 2 Nombre de têtes
28 4 Secteurs cachés (Secteurs non visibles dans cette partition)
32 4 Nombre total de secteurs 32-bit
36 1 Identifiant du disque
37 1 Réservé pour usage ultérieur
38 1 Signature (0x29 par défaut)
39 4 Numéro de série du disque
43 11 Nom du disque sur 11 caractères
54 8 Type de système de fichiers (FAT12, FAT16, FAT32).
62 448 Code de Boot pour l’OS
510 2 Signature du Secteur de Boot (0x55 0xAA)

Figure 11 – Contenu du Secteur de Boot - 512 octets

2.6.3 Fragmentation : Clusters/Chainage

Chaı̂nage des clusters Pour reconstituer un fichier il est nécessaire de lire dans le bon ordre l’ensemble des
clusters sur lesquels il a été écrit. La technique utilisée est un ”chaı̂nage” des clusters qui est obtenu grâce à la File
Allocation Table (FAT) située en début de partition après le secteur de Boot.
La FAT est un tableau de 1 colonne, N lignes où N est le nombre de clusters disponibles. Le numéro de la ligne
où on lit le tableau correspond au numéro du cluster dont on cherche des informations. Par exemple :

Cluster_Suivant = File_Allocation_Table[Cluster_Courant];

Le tableau Figure 12 décrit comment interpréter une valeur contenue dans la FAT. Suivant la valeur on obtient
une information sur l’état du cluster, s’il est vide, utilisé ou inutilisable (réservé ou défectueux). Dans le cas où le
secteur est utilisé, la valeur correspond en fait directement au prochain cluster du fichier. Dans le cas où il n’y a pas
de secteur suivant, des valeurs spéciales sont réservées pour indiquer que le secteur est le dernier du fichier. Voir
Figure 13 pour un exemple de chaı̂nage de clusters.

FAT12 FAT16 FAT32 Description


0x000 0x0000 0x0000000 Cluster vide
0x001 0x0001 0x0000001 Cluster réservé
0x002 - 0xFEF 0x0002 - 0xFFEF 0x0000002 - 0xFFFFFEF Pointeur vers le cluster suivant du fichier
0xFF0 - 0xFF6 0xFFF0 - 0xFFF6 0xFFFFFF0 - 0xFFFFFF6 Valeurs réservées
0xFF7 0xFFF7 0xFFFFFF7 Cluster défectueux
0xFF8 - 0xFFF 0xFFF8 - 0xFFFF 0xFFFFFF8 - 0xFFFFFFF Dernier cluster d’un fichier

Figure 12 – Interprétation des valeurs de la FAT

Rapport projet tutoré Page 18


Fichier 1 Partition FAT

Fichier 2

Zone réservée
File Allocation Table

1 0xFF0

2 0x003 Cluster 2

3 0x005 Cluster 3

4 0x009 Cluster 4

5 0x006 Cluster 5

6 0xFFF Cluster 6

7 0x000 Cluster 7

8 0x000 Cluster 8

9 0xFFF Cluster 9

Figure 13 – Exemple de chaı̂nage de Cluster

Différences entre FAT12/16/32 Ce qui différencie les systèmes de fichiers FAT12, FAT16 et FAT32 est en fait
le nombre de bits sur lesquels sont codées ces valeurs. Cela a un impact direct sur le nombre de clusters que l’on
peut considérer.
– FAT12 : 212 − 18 = 4078 clusters possibles
– FAT16 : 216 − 18 = 65, 518 clusters possibles
– FAT16 : 232 − 18 = 268, 435, 438 clusters possibles
Plus le support de stockage est grand, plus on a intérêt d’utiliser un nombre de clusters important pour réduire
leur taille. Dans le cas de la disquette, le FAT12 permet un nombre suffisant de clusters pour que leur taille soit
ramenée à celle d’un secteur.

LBA d’un cluster On remarque que c’est la zone des données où sont stockés les fichiers et sous répertoires
qui est divisée en clusters et référencée dans la FAT. De plus le premier cluster de cette zone est le cluster No 2.
Car comme indiqué dans la Figure 12 les valeurs 0x0 et 0x1 sont des valeurs réservées dans la FAT. Le LBA de ce
cluster est en fait le LBA de la zone des données qui est déduit des informations situées dans le Secteur de Boot.
Pour retrouver l’adresse (LBA) du cluster sur la partition à partir de son numéro, on effectue l’opération suivante :

LBA_Cluster = (No_Cluster - 2) * Taille_Cluster + LBA_Cluster_No_2;

2.6.4 Lecture de l’arborescence et des fichiers

En FAT, tout est considéré comme étant un fichier. Ainsi un répertoire n’est rien d’autre qu’un fichier, il peut
être de taille variable et peut être contenu sur plusieurs clusters, mais va être interprété différemment. Un répertoire
contient des blocs de 32 octets appelés entrées. Chacune de ces entrées décrit un fichier contenu dans ce répertoire,
voir Figure 14 pour un descriptif du codage de ces entrées.
Une entrée donne des informations sur le type de fichier (Attributs du fichiers), notamment pour savoir si c’est
un fichier ou un sous-dossier, si c’est un fichier caché, s’il est en lecture seule etc.. Elle donne aussi des informations
sur les dates et heures de création ainsi que du dernier accès. Et enfin, chaque entrée contient le numéro du premier
cluster du fichier, ce qui va permettre d’y accéder, ainsi que sa taille en nombre de clusters.

Rapport projet tutoré Page 19


Pour lire l’arborescence on doit d’abord accéder au répertoire racine. Un espace, de taille fixe pour FAT12 et
FAT16, lui est réservé (Voir Figure 10), et sont emplacement est connu par les informations récupérées sur le secteur
de Boot. Une fois qu’on a parcouru tout le répertoire racine et qu’on a récupéré les informations concernant tout
les fichiers et sous-dossiers qu’il contient, on peut accéder à l’ensemble des fichiers et dossiers de l’arborescence.

Position (octet) Taille (octet) Description


0 8 Nom du fichier
8 3 Extension
11 1 Attributs du fichier
12 1 Réservé, utilisé par NT
13 1 Heure de création : par unité de 10 ms (0 à 199)
14 2 Heure de création
16 2 Date de création
18 2 Date du dernier accès
20 2 Numéro du premier cluster pour FAT32 (2 octets de poids fort)
22 2 Heure de dernière modification
24 2 Date de dernière modification
26 2 Numéro du premier cluster du fichier
28 4 Taille du fichier

Attributs Bit : 0 Lecture seule, 1 Fichier caché, 2 Fichier système, 3 Nom du volume, 4 Sous-répertoire, 5 Archive, 6 Device, 7 Inutilisé
Date Bits : 15-9 Année - 1980 (0 = 1980, 127 = 2107), 8-5 Mois (1 = Janvier, 12 = Décembre), 4-0 Jour (1 - 31)
Heure Bits : 15-11 Heures (0-23), 10-5 Minutes (0-59), 4-0 Secondes/2 (0-29)

Figure 14 – Décodage d’une entrée d’un répertoire - 32 octets

2.7 Entrées / Sorties


La lecture et l’écriture dans des fichiers et même au clavier et à l’écran se fait à travers différentes couches
largement inspirées de POSIX. La figure 15 montre le lien entre ces couches.

Process
flags flags
read pointer buffer
read base current octet buf
used ofd
read end current cluster
write pointer current octet
Adresse
write base first cluster
Indice
write end dans file size
buffer base la table extra data
buffer end write()
chain read()
fileno
open file descriptor
FILE

Table fd[FOPEN MAX]

Figure 15 – Liens entre les streams et les descripteurs de fichier

L’application utilise un stream de type FILE pour lire ou écrire. À la création du processus, les streams stdin,
stdout et stderr sont créés. Pour en créer d’autres, il faut utiliser fopen. Ces streams sont appelés ainsi puisqu’ils

Rapport projet tutoré Page 20


fournissent un premier niveau de buffer. Les fonctions de base les plus utilisés ont été implémentées : fprintf, printf,
getchar, scanf, fflush, etc. C’est grâce à l’utilisation du buffer dans le stream que nous pouvons taper du texte et
supprimer ce qui vient d’être tapé dans un scanf. Dans le cas de stdout, le stream n’est automatiquement flushé
que lors de l’écriture d’un retour chariot ou d’un caractère End Of File. Il est bien sûr possible d’appeler la fonction
fflush pour forcer l’écriture.
Chaque stream possède un identifiant l’associant à un descripteur de fichier local au processus. En effet, chaque
processus possède une table de descripteurs de fichiers dont la taille est limitée par FOPEN MAX. Les trois premières
entrées de cette tables sont définies à la création du processus, il s’agit des entrées 0, 1 et 2 qui correspondent
respectivement à stdin, stdout et stderr. Un descripteur de fichier ne possède que deux informations : est-ce que
l’entrée est utilisée et quelle est l’adresse du descripteur de fichier ouvert. Le rôle de fopen est de créer un nouveau
stream et de faire appel à la fonction open. La fonction open fait un appel système pour ouvrir le fichier et créer
un descripteur de fichier ouvert.
Un descripteur de fichier ouvert contient toutes les informations nécessaires pour la lecture et l’écriture dans un
fichier. Le rôle de la fonction open va être notamment de spécifier l’adresse des fonctions read et write permettant
d’accéder au média en lecture et en écriture. En effet ces fonctions sont spécifiques au système de fichier du support
sur lequel on veut accéder.
Pour le moment dans l’OS n’est implémenté que la lecture et l’écriture sur un système de fichiers FAT, c’est
pourquoi des informations relatives à ce système de fichiers sont directement inclues dans le descripteur de fichier.
A savoir le premier cluster du fichier ouvert (first cluster) et le cluster actuellement dans le buffer (current cluster).
Lors d’un accès au fichier la fonction read ou write va être appelée et va prendre en paramètre un pointeur vers
le descripteur de fichier. Si l’accès est en dehors du cluster bufferisé la fonction read ou write va déterminer le cluster
à lire ou écrire et va demander un accès aux secteurs de données concernés sur le média via les fonctions du driver.

2.8 IPC
2.8.1 Sémaphores

Les sémaphores permettent de gérer l’exécution de plusieurs processus en concurrence. Le fonctionnement d’un
sémaphore est le suivant : il s’agit d’une primitive manipulable par deux fonctions, P permet de prendre le sémaphore
et V permet de le relâcher. Un sémaphore contient une valeur entière, cette valeur est décrémentée dès que l’on
prend le sémaphore et incrémentée dès qu’on le relâche. Il est impossible pour un sémaphore de prendre une valeur
négative, la fonction P est par conséquent bloquante le temps que le sémaphore soit relâché.
Afin d’utiliser les sémaphores il faut préalablement les créer avec la fonction sem create, pour cela il faut une
clef qui désigne le sémaphore au niveau du système, le système renvoie alors au processus un identifiant semid afin
de manipuler le sémaphore. Une autre fonction, sem get permet de récupérer un sémaphore créé précédemment.
Enfin, il est possible de supprimer un sémaphore avec la fonction sem del.
L’implémentation des sémaphores repose sur une liste de sémaphores stockée dans le noyau. Lors de l’appel à
la fonction P on décrémente si possible le sémaphore, dans le cas ou c’est impossible la demande est stockée et le
processus est mis en pause. Lors de l’appel à la fonction V on incrémente le sémaphore, si cela débloque un processus
alors on le réveille. Afin de choisir quel processus sera débloqué lorsque le sémaphore est à nouveau disponible les
demandes de sémaphore sont stockées dans une file, les processus obtiennent donc le sémaphore dans l’ordre dans
lequel ils l’ont demandé.

Rapport projet tutoré Page 21


3 Problèmes
3.1 Logiciels de virtualisation
Lorsque nous avons commencé ce projet, nous savions que nous aurions besoin d’un logiciel de virtualisation afin
de faciliter le développement. Nous nous sommes arrêté principalement sur deux logiciels : Bochs et QEMU. Bochs
est seulement un émulateur d’architecture x86 alors que QEMU supporte davantage d’architectures mais dont nous
n’avons pas besoin.
Pendant le développement, nous nous sommes aperçu de quelques différences dans le comportement de Bochs
et de QEMU ce qui nous a permit de détecter quelques problèmes. C’est pour cette raison que nous avons continué
à utiliser les deux même si nous utilisons surtout QEMU qui a pour avantage d’être plus rapide.
Bochs possède un debugger intégré s’il est compilé avec l’option adéquate mais dans ce cas il n’est pas possible
d’utiliser gdb et inversement, il est possible d’activer le support de gdb mais cela désactive le debugger intégré.
En ce qui concerne QEMU, pour une raison qui nous échappe encore, notre système d’exploitation plante avec
la version 0.12 depuis quelques temps. Ce n’est pas le seul problème rencontré avec cette version, nous avons des
problèmes pour ajouter des points d’arrêts avec gdb (ce qui nous aiderait à déterminer ce qui fait planter). Nous
nous sommes résignés à utiliser la branche 0.11 de QEMU.
Certains d’entre nous ont eu un autre problème avec QEMU : ce dernier lançait un serveur VNC au lieu d’ouvrir
une fenêtre comme nous en avions l’habitude. Il semblerait qu’il faille certains paquets SDL lors de la compilation
pour que cela marche correctement.

3.2 Makefile
Pour gérer la compilation et l’édition des liens, nous avons utilisé gcc, ld et surtout make pour automatiser la
génération de l’image. Initialement, nous éditions les fichiers Makefile manuellement ce qui était un travail long et
parfois incomplet. Au début cela n’était pas un gros problème mais de temps en temps, nous avons eu des bugs
incompréhensibles qui nous ont fait perdre beaucoup de temps inutilement. Nous nous sommes rendu compte que
faire un make clean pouvait résoudre le problème, c’est alors que nous avons compris que le problème venait de
dépendances manquantes.
Nous avons alors réfléchi à comment nous pourrions rendre la compilation plus fiable. Nous avons tout d’abord
fait la liste des outils à notre disposition (Make, SCons, CMake, Automake...). Finalement Make avait pour avantage
d’être disponible sur beaucoup plus de machines que les autres outils et de plus il était déjà en place dans notre projet.
Alors nous avons décidé de voir comment pourrait-on améliorer nos Makefile. C’est là que nous avons découvert
makedepend qui est capable de déterminer les dépendances et de modifier le fichier Makefile en conséquent. Plus
tard nous avons aussi découvert que gcc est capable de faire exactement la même chose mais nous avions déjà
modifié nos Makefile pour utiliser makedepend.
Depuis, nous avons moins de problèmes liés à la compilation. Nous avons aussi profité de ces modifications pour
générer des sorties plus lisibles pour mieux voir les erreurs et avertissements lors de la compilation.

3.3 Préemptibilité
Vers la fin du projet, un fois l’ordonnancement mis en place et avec l’arrivée de l’utilisation du système de fichier
FAT, nous avons rencontré des problèmes assez étranges. Par exemple, certains accès à la disquette sont devenus
totalement bloquants (programme bloqué et plus d’ordonnancement). En analysant l’origine du bug en détail, voici
les étapes qui menaient à ce type de blocage :

1. Un appel système est fait pour lire un secteur sur le disque.

2. Le secteur à lire ne ce situe pas sur le cylindre courant, il faut donc lire un autre cylindre.

3. Le pilote du lecteur disquette envois une requète de type SEEK pour déplacer la tête de lecture.

4. Le pilote attend une interruption pour confirmer la bonne exécution de la commande - BLOCAGE.

Rapport projet tutoré Page 22


La découverte de ce bug nous a permis de mettre en avant une grande faiblesse de notre mécanisme d’appels
systèmes. En effet, nous utilisons des interruptions pour effectuer celles-ci, mais comme nous le savons, les inter-
ruption ne sont pas interruptibles. Ainsi, au sein d’une interruption nous tentions de recevoir une interruption,
mais celle-ci étant masquée, il n’y avait aucune chance de la recevoir. Ce problème se trouve également dans un
autre mécanisme du système, qui lui se serait avèré beaucoup plus compliqué à diagnostiquer : l’ordonnanceur étant
une interruption, les appels systèmes se trouvaient être non préemptibles, et faussaient donc toute la mécanique
d’ordonnancement.

La solution que nous avons trouvé pour cela conciste à ne pas utiliser des interruptions classiques pour effectuer
nos appels systèmes. Nous utilisons donc à la place ce que nous appelons une trap gate. La trap gate est un
mécanisme qui fonctionne de la même manière que l’interruption (son descripteur se trouve également dans l’IDT,
et n’est different de celui des interruption qu’à un flag près). Quand une trap gate est appelée, c’est donc le handler
associé qui reprend la main, mais au le flag d’interruption présent dans le registre EFLAGS reste inchangé, et il est
donc possible d’interromptre un handler de trap gate, et le problème se trouve ainsi réglé.

3.4 Malloc
Une chose est primordiale avant de se lancer dans la conception et l’implémentation d’un module de gestion
dynamique de la mémoire, c’est de bien comprendre les différents mécanismes de gestion de la mémoire au niveau
du processeur : différents types d’adresses (virtuelles, linéaires et physiques), les liens entre ces types et la façon
dont le processeur et la MMU décodent ces adresses (répertoire de pages, registre CR3), et enfin comment effectuer
un mapping mémoire entre adresse virtuelle et physique. Effectuer ce mapping mémoire fut le premier problème
rencontré lors de la création de la gestion dynamique de la mémoire.
Une fois un mapping effectué entre une page physique et une page virtuelle, il faut aussi garder cette information
en mémoire, savoir quelle page virtuelle est allouée, quelles sont celles qui ne sont pas encore mappées. Ici, nous
avons donc eu un second problème, qui est de savoir où stocker ces informations. Comme nous ne pouvons pas faire
(encore) d’allocation dynamique, il semble difficile d’avoir une liste permettant de connaı̂tre les pages utilisées. On
pourrait avoir un tableau statique pour l’ensemble des pages, mais ce système semble d’emblée peu efficace. Comme
vu précédemment dans la section mémoire, nous avons opté pour un système de listes doublements chaı̂nées, en
plaçant chaque nœud au début des blocs de pages.
Enfin, un dernier problème s’est posé. C’est qu’il est assez difficile de débugger des problèmes de mémoire
virtuelle puisque lorsque le processeur lance des exceptions de défaut de pages, ces défauts de pages se produisent
après l’allocation, pendant l’utilisation des zones mémoires fraı̂chement allouées. Il est donc difficile de savoir si
c’est le module d’allocation dynamique qui est en cause, ou s’il s’agit d’autre chose.

Rapport projet tutoré Page 23


4 Perspectives pour la suite

4.1 Architecture à micro-noyau


Pour faciliter le développement du système d’exploitation, nous avons fait un système entièrement monolithique.
Puis nous devions ensuite faire les modifications nécessaires pour en faire un système à base de micro-noyau.
Malheureusement nous n’avons pas eu le temps de terminer de mettre en place des mécanismes essentiels au
fonctionnement du système et nous avons préféré régler ces derniers détails plutôt que de nous lancer dans une
refonte profonde de l’architecture du système.
Nous pensons que cet objectif était trop ambitieux étant donné le peu de temps dont nous avons disposé. Nous
estimons qu’il nous serait nécessaire d’avoir au moins un ou deux mois supplémentaires pour pouvoir mettre en
place un système de micro-noyau. Sachant que certains points restent toutefois à corriger avant.

4.2 Affichage graphique


Nous aurions beaucoup avoir eu le temps de pouvoir développer une interface graphique. Malheureusement ceci
représente tout de même pas mal de travail et nous avons finalement abandonné l’idée pour nous concentrer sur
l’existant. Cependant nous avons commencé à y réfléchir et la première chose à faire est changer la résolution de
l’écran. Cela nous a paru assez simple au premier abord mais finalement nous avons perdu beaucoup de temps
dessus.
Pour changer de résolution, il faut normalement être en mode réel or nous sommes en mode protégé mais il
existe au moins 3 méthodes pour contourner cette limitation. La première solution est de booter grub avec une
résolution graphique, mais cela nous empêche de garder notre console actuelle. Une autre solution est de repasser en
mode réel (avec des techniques détournées) puis repasser après en mode protégé. Et enfin, la technique qui semble
la plus adaptée consiste à utiliser le mode virtuel 8086 (vm86).
Le mode virtuel 8086 permet au processeur de se comporter comme s’il était en mode réel sauf qu’il est possible de
repasser en mode protégé plus rapidement et facilement (automatiquement lors d’une interruption). Une particularité
très intéressante de ce mode est que la pagination du mode protégé est active même si les adresses mémoires ne
sont pas en 32 bits. En fait, cela permet de faire tourner plusieurs processus en vm86 qui manipuleront les même
adresses virtuelles mais des adresses réelles différentes. Il suffit de mapper la mémoire que l’on veut entre 0 et 1
Mio.

4.3 IPC
Outre les sémaphores, il existe de nombreux moyens de communication entre processus que nous n’avons pas eu
le temps d’implémenter, néanmoins, nous avons pris le temps de réfléchir à comment nous pourrions le faire.

Pipes Les pipes sont des canaux de communication bidirectionnels entre deux processus, ces canaux fonctionnent
comme une association de deux files, une pour chaque sens de communication, les processus étant libres de lire ou
d’écrire dans n’importe lequel de ces ”tuyaux”. Afin d’implémenter les pipes, nous pourrions faire appel aux flux
qui ont déjà été développés, en stockant deux flux dans l’espace noyau et en permettant d’y insérer/lire des données
au travers des appels systèmes. Une autre approche, utilisant indirectement les flux et plus proche de l’esprit Unix
est d’utiliser des fichiers pour implémenter les pipes.

Mémoire partagée Le fonctionnement de la mémoire partagée repose sur la possibilité d’autoriser plusieurs
processus à écrire dans la même zone mémoire, ceci est relativement facile à réaliser en modifiant le répertoire de
page des deux processus communicants. Le principe consiste à mapper une même zone mémoire physique dans les
repertoires de pages respectifs des processus. Cela n’implique pas d’avoir des adresses virtuelles identiques mais
seulement de pointer vers la même adresse physique. On peut aussi régler les droits pour faire en sorte qu’un seul
des deux processus puisse avoir le droit d’écriture. Actuellement il nous est impossible d’implémenter la mémoire
partagée pour la simple raison que nous n’avons pas fini d’activer la pagination.

Rapport projet tutoré Page 24


4.4 Prise en charge du réseau
Une avancée majeure pour notre système d’exploitation serait qu’il soit capable de dialoguer avec le monde
extérieur à l’aide du réseau. Cette fonction nécessite la mise en place de nombreuses couches. La première chose
à faire est de pouvoir dialoguer avec la carte réseau et être capable d’émettre et recevoir des octets. Nous avons
commencé à nous intéresser au bus PCI donc une partie du travail est déjà fait. Imaginons que cela se fasse sans
trop de difficultés, il faut tout de même mettre en place toutes les structures de données en interne pour permettre à
plusieurs applications d’utiliser la carte réseau. Une fois que ceci est fait, il faut implémenter les différentes couches
réseaux à savoir plus précisément la couche réseau (IP, ARP, ICMP...) et la couche de transport (TCP, UDP...).
Nous pensons que cela pourrait faire l’objet d’un très bon projet tutoré à destination des étudiants de Réseaux
et Télécommunications.

Rapport projet tutoré Page 25


5 Annexes

Rapport projet tutoré Page 26


Rapport projet tutoré

5.1 Arborescence des fichiers de compilation

Dossier contenant les fichiers d’implémentation (.c, .S)

Dossier contenant les fichiers de spécification (.h) locaux


visibles uniquement depuis les fichiers du dossier parent
tacos
Dossier contenant les fichiers de spécification (.h)
visibles depuis l’ensemble des fichiers

include libc apps utils kernel system clock pci filesystem drivers

beeper.h ctype.c fiinou.c heap.c boot.S sem.c clock.c pci.c fat.c


clock.h errno.c pres.c widget.c dummy process.c syscall.c events.c pci config.c
ctype.h fcntl.c shell.c exception.c i8254.c
debug.h libio.c shell utils.c exception wrappers.S beeper
errno.h stdlib.c tests.c fpu.c
events.h string.c gdt.c
exception.h time.c include i8259.c include
include beeper.c
fat.h unistd.c idt.c
fcntl.h include interrupts.c
floppy.h widget.h interrupts wrappers.S pci vendor.h
fopen.h kernel.c i8254.h keyboard
gui.h apps.h kmalloc.c
heap.h stdio fat test.h kpanic.c
interrupts.h shell utils.h ksem.c
ioports.h ksyscall.c keyboard.c
keyboard.h fmemopen.c mbr.c
kmalloc.h fopen.c memory.c
libio.h fprintf.c pagination.c
memory.h fwrite.c process.c mouse
mouse.h get.c scheduler.c
pci config.h printf.c vm86.c
pci.h put.c vmm.c mouse.c
pci types.h scanf.c
process.h sprintf.c
sem.h stdfiles.c
shell.h stdio.c include floppy
stdarg.h video.c
stdio.h
stdlib.h
string.h dummy process.h floppy.c
syscall.h fpu.h floppy dma.c
time.h gdt.h floppy interrupt.c
types.h i8259.h floppy motor.c
unistd.h idt.h floppy utils.c
video.h kpanic.h
ksem.h
ksyscall.h
mbr.h
msr.h include
multiboot.h
pagination.h
scheduler.h floppy dma.h
vm86.h floppy interrupt.h
vmm.h floppy motor.h
floppy utils.h

Figure 16 – Arborescence des fichiers de compilation


Page 27
Rapport projet tutoré

5.2 Diagramme de Gantt

Figure 17 – Diagramme de Gantt


Page 28
5.3 Compilation
Le projet est découpé en plusieurs parties et chaque partie possède son Makefile. À la racine du projet se trouve
un Makefile plus complet qui est capable d’appeler récursivement les autres Makefile.
Les principaux arguments pour make sont :
– kernel.bin : Compile tous les sous dossiers et fait le lien.
– img : Fabrique une image de disquette avec l’OS et Grub dessus.
– runqemu : Lance qemu avec une mémoire de 8 mo.
– runqemugdb : Lance qemu en mode gdbserver pour le debug.
– runbochs : Lance bochs en utilisant le fichier de configuration bochsrc
– doc : Génère la documentation avec Doxygen
– clean : Supprime les fichiers .o, .bin et l’image.
– depend : Génère automatiquement les dépendances pour make en utilisant makedepend.
Par défaut, c’est kernel.bin qui est exécuté.

Rapport projet tutoré Page 29


5.4 Déboguage avec gdb
Lancement de qemu en mode serveur gdb Pour lancer une session de déboguage il faut d’abord lancer qemu
en mode serveur gdb, pour cela depuis le dossier du projet on tape ”make runqemugdb”. Il faut ensuite lancer un
gdb qui va se rattacher sur ce serveur, pour cela on tape ”gdb”, toujours depuis le dossier du projet où se trouve le
fichier .gdbinit

Utilisation de gdb Voici une liste des commandes utiles pour manipuler gdb.
– c : reprend l’exécution
– s : avance d’un pas l’exécution
– si : avance d’une instruction assembleur
– b location : place un point d’arrêt (breakpoint), location peut être le nom d’une fonction, un numéro de ligne
ou fichier :numéro ligne
– bt : affiche la pile, attention étant donné que nous modifions la pile en fonction de nos besoins le résultat peut
parfois s’avérer surprenant.
– frame num : passe à la frame num, num est celui donné par la commande bt, ceci permet de changer le
contexte pour parcourir les différents niveaux d’appels de fonction
– layout [src-asm-split-cmd] : permet de diviser la fenêtre de gdb en deux ou trois parties pour afficher en plus
de la zone de commande, le code source, le code assembleur ou les deux
– focus [src-asm-cmd] : utile lorsque l’on utilise layout, cette commande permet de changer le focus des flèches
directionnelles.

La figure suivante illustre une session de déboguage en mode layout src, avec un point d’arrêt sur la fonction
cmain. On peut voir que la ligne courante est surlignée, une flèche indique également l’instruction actuelle. Le point
d’arrêt est indiqué par un B dans la marge, la zone de commandes est en bas de l’écran. Pour débuter une session
de débogage il est conseillé le placer un point d’arrêt sur cmain puis d’appuyer sur c pour lancer grub, en effet qemu
ne lance pas l’exécution automatiquement au démarrage.

Rapport projet tutoré Page 30

You might also like