Logiciel de base

Mini-projet système


Introduction

Le cours de Logiciel de Base se termine par un mini-projet système : vous allez écrire un petit noyau de système d'exploitation en utilisant ce que vous avez appris en C et en assembleur. Ce mini-projet est en fait l'introduction au Projet système sur lequel vous travaillerez en fin de deuxième année.

On rappelle au passage quelques opérations binaires en C qui seront utiles tout au long de ce projet :

Le planning du projet est libre pour vous laisser avancer à votre rythme, mais on peut envisager une progression comme suit :

Supports pour le mini-projet

Prise en main de l'environnement de travail

Lisez la documentation sur l'environnement.

Téléchargez les sources de base.

Pour travailler sur votre machine personnelle

Vous pouvez si vous le souhaitez travailler sur votre portable personnel : tous les outils utilisés sont gratuits et disponibles en ligne.

Si vous êtes sous Linux, la plupart des outils nécessaires doivent déjà être installés ou peuvent l'être facilement via votre gestionnaire de paquets favori. Notez qu'il faut une version 32 bits de GCC pour compiler le noyau, si votre système ne dispose que d'outils 64 bits vous devrez l'installer avec votre gestionnaire de paquets favori.

Si vous êtes sous OSX, il vous faut un environnement de développement gérant le format ELF 32 bits, ce qui n'est pas le cas du compilateur CLANG fourni par Apple. Vous pouvez télécharger cette archive testée sous Yosemite et la décompresser (vous pouvez installer le logiciel gratuit The Unarchiver depuis l'AppStore) dans le répertoire /usr/local (pensez à ajouter /usr/local/i386-pc-elf/bin à votre PATH ensuite). Il faut aussi modifier le fichier kernel/Makefile pour préfixer tous les appels à GCC et aux binutils par i386-pc-elf-. Sous OSX, vous n'avez pas besoin d'utiliser de client VNC car QEmu affiche directement sa console dans une fenêtre native du système.

Gestion de l'écran

Lisez la documentation sur la gestion de l'écran.

Résumé du travail demandé :

Le but final est d'écrire une fonction void console_putbytes(char *chaine, int32_t taille) qui affiche une chaine de caractères à la position courante du curseur. Attention, vous devez respecter le nom et la spécification de cette fonction car elle est appelée par d'autres fonctions du noyau, par exemple printf.

A part celles pour laquelle c'est noté explicitement, toutes les fonctions doivent être écrites en C. Pour les fonctions à écrire en assembleur, il est recommandé de procéder en deux temps :

Pour tester en C les fonctions accédant aux ports d'entrée-sortie, vous pouvez utiliser les pseudo-fonctions C : uint8_t inb(uint16_t port) et void outb(uint8_t val, uint16_t port) (qui ne font en fait qu'appeler les instructions assembleur équivalentes).

Les fonctions en assembleur doivent être écrites dans des fichiers fct_xxxx.S (n'essayez pas d'inclure du code assembleur directement dans du code C : écrire du code assembleur inline implique de respecter des contraintes complexes et complique grandement la mise au point des fonctions). Notez que les fichiers doivent avoir une extension en majuscules : la différence entre un fichier .s et .S est que GCC fait passer le pré-processeur sur les fichiers .S avant d'appeler l'assembleur, ce qui permet d'inclure des fichiers d'en-tête .h et donc d'utiliser des constantes.

Pour arriver au but final vous pouvez par exemple implanter dans cet ordre :

  1. une fonction uint16_t *ptr_mem(uint32_t lig, uint32_t col) qui renvoie un pointeur sur la case mémoire correspondant aux coordonnées fournies : cette fonction doit être traduite en assembleur ;
  2. une fonction void ecrit_car(uint32_t lig, uint32_t col, char c, uint32_t coul_texte, uint32_t coul_fond) qui écrit le caractère c aux coordonnées spécifiées : cette fonction doit être traduite en assembleur ;
  3. une fonction void efface_ecran(void) qui doit parcourir les lignes et les colonnes de l'écran pour écrire dans chaque case un espace en blanc sur fond noir (afin d'initialiser les formats dans la mémoire) ;
  4. une fonction void place_curseur(uint32_t lig, uint32_t col) qui place le curseur à la position donnée : cette fonction doit être traduite en assembleur ;
  5. une fonction void traite_car(char c) qui traite un caractère donné (c'est à dire qui l'affiche si c'est un caractère normal ou qui implante l'effet voulu si c'est un caractère de contrôle) ;
  6. une fonction void defilement(void) qui fait remonter d'une ligne l'affichage à l'écran (il pourra être judicieux d'utiliser memmove définie dans string.h pour cela) ;
  7. la fonction console_putbytes demandée, qui va sûrement utiliser les fonctions précédentes.

Afin de vérifier le bon fonctionnement de vos différentes fonctions, le plus simple est de faire un affichage avec printf (définie dans stdio.h), car printf utilise console_putbytes pour l'affichage à l'écran.

Le module de gestion de l'écran doit garder en interne la position courante du curseur, ainsi que les différents attributs (couleur du texte, du fond), dans des variables globales.

Le bout de bibliothèque C fourni comprend de nombreuses fonctions utiles : il faut s'en servir pour ne pas ré-inventer (et perdre du temps à mettre au point) du code redondant ! Vous trouverez la documentation des fonctions C dans les pages man habituelles : par exemple, man memmove ou man sprintf.

Gestion du temps

Lisez la documentation sur la gestion du temps.

Résumé du travail demandé :

Le travail demandé peut être découpé de la façon suivante :

  1. écrire une fonction qui prend en paramètre une chaine de caractères et l'affiche en haut à droite de l'écran : c'est cette fonction qui sera appelée par le traitant d'interruption quand on devra mettre à jour l'affichage de l'heure (fonction C à rajouter logiquement dans votre module de gestion de l'écran) ;
  2. écrire le traitant de l'interruption 32 qui affiche à l'écran le temps écoulé depuis le démarrage du système : ce traitant commence par une partie en assembleur pour sauvegarder les registres et acquitter l'interruption, mais la partie gérant l'affichage doit être faite dans une fonction en C qu'on appelera par exemple void tic_PIT(void) (on attire au passage votre attention sur l'existence dans la mini-libc fournie d'une fonction sprintf qui vous sera vraisemblablement utile) ;
  3. initialiser l'entrée 32 dans la table des vecteurs d'interruptions, grace à une fonction void init_traitant_IT32(void (*traitant)(void)) à écrire en C ;
  4. régler la fréquence de l'horloge programmable : la fréquence d'émission des signaux par l'horloge doit être une constante globale de votre système, afin de permettre facilement de la changer : cette fonction doit être traduite en assembleur ;
  5. démasquer l'IRQ0 pour autoriser les signaux en provenance de l'horloge : cette fonction doit être traduite en assembleur ;
  6. démasquer les interruptions externes grâce à un appel à la fonction sti() comme expliqué dans le squelette de code donné plus haut.

Gestion de la date

Lisez la documentation sur la gestion de l'horloge calendaire.

Résumé du travail demandé :

On conseille de commencer par implanter les fonctions de lecture des informations du CMOS, c'est à dire :

  1. une fonction uint8_t lit_CMOS(uint8_t reg) qui renvoie en décimal la valeur lue dans le registre de numéro reg : cette fonction doit être traduite en assembleur (vous implanterez le décodage BCD → décimal dans cette fonction) ;
  2. une fonction void affiche_date(void) qui lit les différentes valeurs intéressantes du CMOS, les mets en forme dans une chaine de caractères, et appelle une fonction ecrit_date pour réaliser l'affichage ;
  3. une fonction void ecrit_date(char *chaine) à ajouter dans votre module de gestion de l'écran et qui écrit la chaine en haut à gauche de l'écran, de façon très similaire à la fonction ecrit_temps écrite précédemment pour gérer l'affichage de l'uptime.

Ensuite, il faudra gérer l'interruption 40, ce qui peut être décomposé en écrivant :

  1. une fonction void regle_frequence_RTC(void) qui règle la fréquence et le type de signal de la RTC, cette fonction doit être traduite en assembleur ;
  2. modifier ou étendre les fonctions que vous avez déjà écrites pour la gestion de l'interruption 32, pour qu'elles puissent aussi gérer l'interruption 40 ;
  3. écrire la partie assembleur du traitant de l'interruption 40, comme expliqué plus haut (remarque : vous devez écrire un traitant séparé de celui de l'interruption 32, il n'est pas recommandé de chercher à écrire un traitant générique, mais vous pouvez fortement vous inspirer du traitant existant).

Si vous avez fini en avance

Vous pouvez ajouter la gestion du clavier à votre noyau. Pour cela, il suffit d'écrire un traitant pour l'interruption 33, qui doit :

(et bien sûr également démasquer l'IRQ1 et enregistrer l'adresse du traitant de l'interruption 33 dans la table des vecteurs d'interruption)

La valeur lue correspond au code de la touche (scancode) : décoder ce code pour obtenir le code ASCII de la touche est un processus plus compliqué car il dépend du modèle de clavier utilisé.

Si vous vous ennuyez vraiment

Vous pouvez ajouter la gestion des processus à votre noyau : attention, il s'agit d'un travail conséquent et complexe, donc assurez-vous d'avoir fini correctement les parties précédentes avant de vous lancer dedans.

Pour réaliser cette partie, vous aurez besoin de :