PSE Seance 1

De Ensiwiki
Aller à : navigation, rechercher
AttentionCette page est maintenue uniquement par les enseignants. Afin de ne pas perturber le déroulement des cours, elle n'a pas vocation à être modifiée par les élèves. Mais si vous avez des modifications à proposer, merci d'en discuter ou d'envoyer un e-mail aux auteurs de la page (cf. historique)

Mycomputer.png  Deuxième Année  CDROM.png  Informatique 

Note (CR) : version obsolète de la séance 1, on la garde temporairement pour récupérer les infos

Le but de cette première séance de Pratique du Système est de programmer la gestion de l'écran dans un noyau de système d'exploitation. On va se limiter à un mode d'affichage très simple géré par toutes les cartes graphiques depuis le début des années 80 (norme CGA).

Mais avant de commencer, il faut mettre en place l'environnement de travail que l'on va utiliser pendant 3 séances.

Configuration et prise en main de l'environnement de travail

Création d'une machine virtuelle

Un système d'exploitation s'exécute sur machine nue. Mais pour simplifier le développement et la mise au point des prototypes, nous allons utiliser la machine virtuelle VirtualBox que vous avez peut-être déjà utilisée pour exécuter l'image EnsiLinux sous Windows ou MacOS par exemple. L'intérêt de cet environnement d'exécution est qu'il est portable (vous pouvez l'installer sur vos machines personnelles et travailler en dehors des salles systèmes) et parfaitement transparent pour votre prototype : il n'y aurait rien à changer dans votre code pour que votre système s'exécute sur une machine nue. Pour ceux que ça intéresse, il est possible de créer une clé USB amorçable pour tester votre prototype sur une machine nue : la procédure est décrite sur cette page.

Attention : si vous installez VirtualBox sur votre machine personnelle sous Linux, vous devez télécharger la version binaire, pas la version Open Source Edition (OSE) qui n'est pas compatible avec le chargeur PXE utilisé.

Pour créer la machine virtuelle, il faut suivre pas à pas les étapes suivantes :

  1. lancer VirtualBox : sur les PC de la salle système, vous le trouverez sous Applications -> Outils système -> VirtualBox
  2. dans l'interface de VirtualBox, créer une nouvelle machine (icône « Nouveau » puis « Suivant »)
  3. donner un nom à votre machine, par exemple « PSE », puis dans le menu « Système d'exploitation » choisissez « Autre » puis également » Autre / inconnu » dans le menu « Version » et passer à la fenêtre suivante
  4. régler la taille de la mémoire sur 64 MiO et décocher l'option « Disque dur d'amorçage » puis terminer la création de la machine.

Une fois la machine créée, il faut configurer quelques paramètres :

  1. sélectionner votre machine et cliquer sur l'icône « Configuration », aller dans l'onglet « Système » et dans le menu « Ordre d'amorçage », cocher « Réseau » et décocher tout le reste
  2. dans l'onglet « Ports séries » sélectionner « Port 1 » et cocher « activer le port série », puis entrer les paramètres suivants : Numéro de port : « COM1 », Mode du port : « tube hôte » , cocher « Créer le tube » et entrer comme chemin /tmp/pse_com1. Valider.

Il faut maintenant télécharger un petit binaire qui va servir à démarrer le système :

  1. créer un répertoire TFTP dans le répertoire de base de VirtualBox : par exemple, sous Linux mkdir ~/.VirtualBox/TFTP ou sous MacOS mkdir ~/Library/VirtualBox/TFTP
  2. récupérer cette archive et la décompresser (en utilisant par exemple unzip)
  3. copier le fichier PSE.pxe extrait dans le répertoire TFTP que vous avez créé (attention, ce fichier doit porter le même nom que votre machine virtuelle et l'extension .pxe).

Vous devez enfin récupérer un ensemble de sources de départ que l'on vous fournit et qui sont disponibles dans cette archive que vous devez décompresser dans votre répertoire de travail. Les sources distribuées sont dans le répertoire kernel_base, qui contient lui-même deux sous-répertoires : kernel contient les sources du noyau et shared une mini-bibliothèque C qui vous aidera à développer votre prototype.

La compilation d'un noyau se fait simplement en se plaçant dans le répertoire kernel_base et en tapant la commande make : si tout se passe bien, le binaire kernel.bin est produit dans le sous-répertoire kernel : il faut alors le recopier dans le répertoire TFTP où vous avez copié PSE.pxe pour pouvoir démarrer le noyau sous VirtualBox (le Makefile le copie automatiquement au bon endroit si vous êtes sous Linux avec une installation classique : dans le cas contraire, vous pouvez facilement modifier la ligne en question dans kernel/Makefile pour l'adapter à votre système de développement).

Prise en main de l'environnement de développement

Lorsque vous lancez l'exécution du noyau dans VirtualBox, un certain nombre d'opérations d'initialisation sont effectuées puis la fonction kernel_start localisée dans le fichier kernel/start.c s'exécute : il s'agit du point d'entrée de votre noyau (comme la fonction main dans un programme C classique). Dans les sources fournies, cette fonction commence par un appel à la fonction call_debugger qui provoque l'affichage d'une page d'information utiles pour mettre au point votre système.

Compiler les sources fournies et lancer l'exécution du noyau dans VirtualBox : le système doit s'arrêter sur une page d'information comme celle ci-dessous :

Lorsqu'on arrive sur cette page, on peut utiliser les commandes suivantes :

  • espace permet de basculer entre l'affichage de la page d'information et de l'écran normal du système, pour voir d'éventuelles traces
  • i permet d'ignorer la page d'information et de poursuivre l'exécution du système
  • d active la connexion avec GDB : c'est la commande que l'on va utiliser pour mettre au point le système

Une fois qu'on a appuyé sur d, on doit utiliser la commande socat pour connecter VirtualBox à GDB : dans un nouveau terminal, taper la commande :

socat UNIX-CONNECT:/tmp/pse_com1 PTY,link=/tmp/pse_gdb

(socat permet de rediriger un flux mono- ou bi-directionnel vers un autre flux mono- ou bi-directionnel. Ceci inclue les tubes (pipes), les fichiers, les descripteurs de fichiers ouverts, les terminaux, les socket ipv4, ipv6, tcp, udp, unix ...). Si tout se passe bien, il ne doit y avoir aucun affichage et la commande se bloque (elle rend la main lorsque la connexion est interrompue).

La mise au point peut être faite avec GDB ou sa version graphique DDD que certains trouvent plus simple à utiliser. On détaille ci-dessous l'utilisation de DDD mais les manipulations sont exactement les mêmes avec GDB :

  • aller dans le sous-répertoire kernel et lancer ddd kernel.bin &
  • dans la console en bas de DDD, taper la commande target remote /tmp/pse_gdb pour connecter DDD au noyau en cours d'exécution (vous pouvez créer un bouton qui exécutera cette commande en allant dans Commands puis Define command...)
  • vous devez voir apparaitre dans la fenêtre source le contenu du fichier start.c, qui exécute simplement ici une fonction bien connue.

Si vous n'êtes pas à l'aise avec l'utilisation de DDD, vous pouvez vous entrainer à l'aide des manipulations suivantes (mais n'y passez pas trop de temps car l'important est le reste du TP) :

  • modifier le fichier start.c en ajoutant une variable locale dans la fonction fact servant à stocker le résultat de la fonction avant de le retourner, puis recompiler et reprendre le contrôle du noyau dans DDD
  • mettre des points d'arrêt dans le code et s'entrainer à utiliser la commande cont pour avancer de point en point
  • comprendre la différence entre la commande step et la commande next lors d'un passage sur une fonction
  • afficher le contenu d'une variable et suivre ses évolutions grâce à la commande display
  • afficher le code assembleur correspondant au code C en cours d'exécution, puis avancer l'exécution instruction par instruction
  • afficher les 20 premiers octets de la pile d'appel d'une fonction
  • afficher le contenu d'une plage de mémoire à partir de l'adresse 0x1000000

Spécification de l'affichage à l'écran

L'écran que nous considérons est le mode texte de base des cartes vidéo des PC dans lequel le noyau démarre. L'affichage fait 80 colonnes sur 25 lignes. L'affichage s'effectue en écrivant directement dans la mémoire vidéo pour y placer les caractères et leur couleur. Certaines opérations simples d'entrées-sorties sont nécessaires pour déplacer le curseur clignotant qui indique la position actuelle d'affichage.

Principe

L'écran est couplé à une zone de la mémoire vidéo commençant à une adresse dépendant du mode utilisé (ici l'adresse de début est 0xB8000) : tout ce qui est écrit dans cette zone mémoire est donc immédiatement affiché à l'écran. Dans le mode vidéo utilisé, l'écran peut être vu comme un tableau de 80x25 = 2000 cases. Chaque case représente un caractère affiché à l'écran, et est composée de 2 octets :

  • le premier octet contient le code ASCII du caractère ;
  • le deuxième octet contient le format du caractère, c'est à dire la couleur du texte, la couleur du fond et un bit indiquant si le texte doit clignoter :
Structure de l'octet de format
bit 7 6 5 4 3 2 1 0
clignote couleur du fond couleur du texte

Attention : le clignotement n'est pas géré correctement par VirtualBox (changement de la couleur de fond au lieu de clignoter), vous pouvez forcer le bit 7 toujours à 0.

Les couleurs disponibles sont listées dans le tableau ci-dessous :

Les 16 couleurs de la palette CGA
valeur couleur valeur couleur valeur couleur valeur couleur
0 noir
4 rouge
8 gris foncé
12 rouge clair
1 bleu
5 magenta
9 bleu clair
13 magenta clair
2 vert
6 marron
10 vert clair
14 jaune
3 cyan
7 gris
11 cyan clair
15 blanc

Les 16 couleurs sont possibles pour le texte, par contre seules les 8 premières peuvent être sélectionnées pour le fond.

Pour afficher un caractère à la ligne lig et à la colonne col de l'écran, on doit donc écrire dans le mot de 2 octets (short en C) dont l'adresse en mémoire peut être calculé à partir de la formule suivante : 0xB8000 + 2 * (lig * 80 + col).

Une fois cette adresse calculée, on peut écrire le caractère c (de couleur ct, sur un fond cf et avec un bit de clignotement cl) en utilisant les opérateurs sur les bits du langage C : le premier octet est égal à la valeur du caractère c et le 2ème octet est calculé comme suit : (cl << 7) | (cf << 4) | (ct).

Gestion du curseur

Lorsqu'on écrit dans un terminal en mode texte, on voit s'afficher un curseur clignotant qui indique la prochaine case dans laquelle on va écrire. Dans le mode vidéo que l'on utilise, ce curseur est géré directement par la carte vidéo : il suffit de lui indiquer à quelles coordonnées elle doit l'afficher.

On communique pour cela via des ports d'entrée-sorties : il s'agit de canaux de communication reliant les périphériques et dont les adresses sont fixées. Il existe deux types de ports :

  • les ports de commandes qui servent à indiquer au périphérique l'opération que l'on souhaite effectuer ;
  • les ports de données qui permettent de communiquer effectivement avec le périphérique, en lisant ou en envoyant des données.

Dans l'architecture Pentium, les ports sont couplés à une plage d'adresses de 64 KiO : l'adresse d'un port est donc une valeur sur 16 bits (unsigned short en C). Cependant, on ne peut pas accéder à cette zone mémoire directement via des pointeurs : on doit utiliser des instructions particulières.

Il existe des instructions assembleur dédiées pour la communication via les ports : sur le Pentium, il s'agit de l'instruction in (pour lire une donnée en provenance d'un port et la stocker dans un registre du processeur) et de l'instruction out (pour envoyer une donnée à un port). Pour faciliter l'utilisation de ces instructions, on fourni des fonctions C réalisant les mêmes opérations :

  • unsigned char inb(unsigned short port) renvoie l'octet (unsigned char) lu sur le port dont l'adresse est passée en paramètre
  • void outb(unsigned char value, unsigned short port) envoie l'octet value sur le port d'adresse port
  • ainsi que l'équivalent de ces fonctions pour des valeurs sur 16 bits (inw, outw) et sur 32 bits (inl, outl)).

Dans les cartes vidéos VGA que l'on utilise dans ce TP, le port de commande gérant la position du curseur est le 0x3D4 et le port de données associé est le 0x3D5. La position du curseur est un entier sur 16 bits calculé via la formule suivante : pos = col + lig * 80. Cette position doit être envoyée en deux temps à la carte vidéo : on envoie d'abord l'octet de poids faible puis l'octet de poids fort de la position. La succession d'opérations à effecter est donc la suivante :

  1. envoyer la commande 0x0F sur le port de commande pour indiquer à la carte que l'on va envoyer la partie basse de la position du curseur
  2. envoyer cette partie basse sur le port de données
  3. envoyer la commande 0x0E sur le port de commande pour signaler qu'on envoie maintenant la partie haute
  4. envoyer la partie haute de la position sur le port de données.

Les caractères à afficher

On considère dans ce TP les caractères de la table ASCII (man ascii), qui sont numérotés de 0 à 127 inclus. Les caractères dont le code est supérieur à 127 (accents, etc.) seront ignorés.

Les caractères de code ASCII 32 à 126 doivent être affichés en les plaçant à la position actuelle du curseur clignotant et en déplaçant ce curseur sur la position suivante : à droite, ou au début de la ligne suivante si le curseur était sur la dernière colonne.

Les caractères de 0 à 31, ainsi que le caractère 127 sont des caractères de contrôle. Le tableau ci-dessous décrit ceux devant être gérés. Tous les autres caractères de contrôle doivent être ignorés.

Caractères de contrôle à gérer
Code de contrôle Mnémonique Effet
8 BS Recule le curseur d'une colonne s'il n'est pas sur la première colonne
9 HT Avance à la prochaine tabulation (colonnes 1, 9, 17, ..., 65, 73, 80)
10 LF Déplace le curseur sur la ligne suivante, colonne 1
12 FF Efface l'écran et place le curseur sur la colonne 1 de la ligne 1
13 CR Déplace le curseur sur la ligne actuelle, colonne 1

Travail demandé

Le but final est d'écrire une fonction void console_putbytes(char *chaine, int 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.

Pour cela, on recommande d'implanter progressivement la spécification en la découpant en fonctions.

Par exemple, vous pouvez implanter dans cet ordre :

  1. une fonction short *ptr_mem(int lig, int col) qui renvoie un pointeur sur la case mémoire correspondant aux coordonnées fournies (cette fonction peut aussi s'écrire efficacement sous la forme d'une macro)
  2. une fonction void ecrit_car(int lig, int col, char c) qui écrit le caractère c aux coordonnées spécifiées (vous pouvez aussi ajouter des paramètres pour permettre de préciser la couleur du caractère, celle du fond ou le bit de clignotement)
  3. une fonction void place_curseur(int lig, int col) qui place le curseur à la position donnée
  4. une fonction void efface_ecran(void) dont on vous laisse deviner le but
  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ées, 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 garde en interne la position courante du curseur, ainsi que les différents attributs (couleur du texte, du fond, clignotement), 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 !