** DOCS [__[wiki: Start]__][__[wiki:Howto-TP Config]__][__[htdocs:cours/doc_MIPS32.pdf User]__][__[wiki:Doc-MIPS-Archi-Asm-kernel Kernel]__] — COURS [__[htdocs:cours/Archi-1-C9-4p.pdf 9]__] [__[htdocs:cours/Archi-1-C10-4p.pdf 10]__] [__[htdocs:cours/Archi-1-C11-4p.pdf 11]__] — TD [__[https://www-soc.lip6.fr/trac/archi-l3s5/wiki/Archi-1-TD9 9]__][__[https://www-soc.lip6.fr/trac/archi-l3s5/wiki/Archi-1-TD10 10]__][__[https://www-soc.lip6.fr/trac/archi-l3s5/wiki/Archi-1-TD11 11]__] — TP [''9''][__[https://www-soc.lip6.fr/trac/archi-l3s5/wiki/Archi-1-TP10 10]__][__[https://www-soc.lip6.fr/trac/archi-l3s5/wiki/Archi-1-TP11 11]__] — ZIP [__[htdocs:files/kO6bin.tgz gcc...]__][__[htdocs:files/tp1.tgz 9]__][__[htdocs:files/tp2.tgz 10]__][__[htdocs:files/tp3.tgz 11]__] [[PageOutline]] ** {{{#!html

Boot et premier programme en mode kernel

}}} La partie pratique est découpée en 5 étapes. Pour chaque étape, nous donnons (1) une brève description, (2) une liste des objectifs principaux de l'étape, (3) une liste des fichiers avec un bref commentaire sur chaque fichier, (4) une liste de questions simples dont les réponses sont dans le code, le cours ou le TD et enfin (5) un exercice de codage. **IMPORTANT\\Avant de faire cette séance, vous devez avoir lu les documents suivants** : * [wiki: Description des objectifs de cette séance et des suivantes] : ''obligatoire'' * [htdocs:cours/Archi-1-C9-4p.pdf Cours de démarrage présentant l'architecture matérielle et logicielle que vous allez manipuler] ''obligatoire'' * [htdocs:cours/Archi-1-C9-annexe-4p.pdf Éléments d'information sur les outils de la chaîne de compilation] ''recommandé'' * [wiki:Howto-TP Configuration de l'environnement des TP] : ''obligatoire'' * [htdocs:cours/doc_MIPS32.pdf Document sur l'assembleur du MIPS et la convention d'appel des fonctions] : ''recommandé, normalement déjà lu'' * [wiki:Doc-MIPS-Archi-Asm-kernel Documentation sur le mode kernel du MIPS32] : ''optionnel pour cette séance'' **Récupération du code du TP** * **Vous devez avoir installé le simulateur du prototype almo1 et la chaine de cross-compilation MIPS ([wiki:Howto-TP Config sections 2.2 et 3.2])** * Téléchargez **[htdocs:files/tp1.tgz l'archive code du tp1]** et placez là dans le répertoire **`~/kO6`** (ou dans le répertoire que vous avez choisi, relisez la page sur la configuration si ce n'est pas clair). * Ouvrez un `terminal` * Allez dans le répertoire `kO6` : **`cd ~/kO6`** * Décompressez l'archive du tp1 (dans le répertoire **`kO6`**) : **`tar xvzf tp1.tgz`** * Exécutez la commande **`cd ; tree -L 1 kO6/tp1/`**.\\ ''(si vous n'avez pas `tree` sur votre Linux, vous pouvez l'installer, c'est un outil utile, mais pas indispensable pour ces TP)''\\ Vous devrez obtenir ceci: {{{#!bash kO6/tp1 ├── 1_hello_boot ├── 2_init_asm ├── 3_init_c ├── 4_nttys ├── 5_driver └── Makefile }}} **Avant de commencer** * Pour les travaux pratiques, vous devez d'abord répondre aux questions, elles ont pour but de vous faire lire le code et revoir les points du cours. Les réponses sont dans le cours ou dans les fichiers sources. Certaines ont déjà été traitées en TD, c'est normal. Ensuite, vous passez aux exercices pratiques. * Vous devez avoir récupérer l'archive tp1.tgz pour pouvoir faire cette partie, si ce n'est pas la cas, retournez lire la section `Récupération du code du TP` en haut de cette page. La variable `shell` `$kO6` doit être définie dans votre environnement si vous avez suivi les consignes de la page [wiki:Howto-TP Config sections 2.2]. * Si vous avez bien suivi les étapes de configuration de l'environnement et de récupération du code alors le code se trouve dans `~/kO6/tp1/`, et ouvrez un terminal et allez-y. Dans le répertoire `~/kO6/tp1/` vous avez 5 sous-répertoires et un Makefile. Le fichier `~/kO6/tp1/Makefile` permet de faire le ménage en appelant les Makefiles des sous-répertoires avec la cible `clean`, il est simple, mais c'est un Makefile hiérarchique. Ouvrez-le par curiosité. = 1. Premier programme en assembleur dans la seule section de boot Nous commençons par un petit programme de quelques lignes en assembleur, placé entièrement dans la région mémoire du boot, qui réalise l'affichage du message "Hello World". C'est un tout tout petit programme, mais pour obtenir l'exécutable, vous devrez utiliser tous les outils de la chaîne de cross-compilation MIPS et pour l'exécuter vous devrez exécuter le simulateur du prototype. C'est simple, mais c'est nouveau pour beaucoup d'entre vous **Objectifs** - produire un exécutable à partir d'un code en assembleur. - savoir comment afficher un caractère sur un terminal. - analyse d'une trace d'exécution **Fichiers** {{{ 1_hello_boot ├── hcpua.S : code dépendant du cpu matériel en assembleur ├── kernel.ld : ldscript décrivant l'espace d'adressage pour l'éditeur de lien └── Makefile : description des actions possibles sur le code : compilation, exécution, nettoyage, etc. }}} **Questions** 1. Dans quel fichier se trouve la description de l'espace d'adressage du MIPS ? Que trouve-t-on dans ce fichier ? {{{#!protected ------------------------------------------------------------------------------------ '' - C'est dans le fichier kernel.ld. \\On y trouve: - la définition de variables du ldscript. Ce sont essentiellement des adresses dans l'espace d'adressage, mais pas seulement, il y a aussi la taille des régions. - On trouve ensuite la déclaration des régions mémoires. - et enfin la définition des sections de sortie qui seront mises dans le fichier binaire produit et dans quelle région elles sont placées. '' }}} 1. Dans quel fichier se trouve le code de boot et pourquoi, selon vous, avoir nommé ce fichier ainsi ? {{{#!protected ------------------------------------------------------------------------------------ '' - Le code de boot est dans le fichier `hcpua.S`. Il a a été nommé ainsi parce que c'est du code qui dépend du hardware, qu'il concerne le cpu et que c'est du code en assembleur. Nous verrons plus tard qu'il y aura un fichier `hcpuc.c` '' }}} 1. À quelle adresse démarre le MIPS ? Où peut-on le vérifier ? {{{#!protected ------------------------------------------------------------------------------------ '' - L'adresse de démarrage est `0xBFC00000`. - On peut le vérifier dans le fichier `kernel.ld`. Il y a une définition des régions de la mémoire, dont une région commençant à cette adresse-là, et c'est dans cette région que l'on met le code de boot. '' }}} 1. Que produit `gcc` quand on utilise l'option `-c` ? {{{#!protected ------------------------------------------------------------------------------------ '' - L'option `-c` demande à `gcc` de s'arrêter après avoir produit le fichier objet. - Il produit donc un fichier au format `.o` '' }}} 1. Que fait l'éditeur de liens ? Comment est-il invoqué ? {{{#!protected ------------------------------------------------------------------------------------ '' - L'éditeur de liens rassemble toutes les sections produites par le compilateur, et donc présentes dans les fichiers objet `.o`, et il les place dans de nouvelles sections, elles-mêmes placées dans les régions de la mémoire, conformément au fichier ldscript (ici `kernel.ld`). - L'éditeur de liens est appelé par `gcc` si on n'a pas l'option `-c`ou directement par `ld` (ici `mipsel_unknown_ld`) '' }}} 1. De quels fichiers a besoin l'éditeur de liens pour fonctionner ? {{{#!protected ------------------------------------------------------------------------------------ '' - L'éditeur de liens a besoin des fichiers objets `.o` et du fichier `ldscript` (ici, `kernel.ld`) '' }}} 1. Dans quelle section se trouve le code de boot pour le compilateur ? ''(la réponse est dans le code assembleur)'' {{{#!protected ------------------------------------------------------------------------------------ '' - Le code de boot a été mis dans une section `.text`. '' }}} 1. Dans quelle section se trouve le message "hello" pour le compilateur ? Ce choix est particulier, mais ce message est en lecture seule. {{{#!protected ------------------------------------------------------------------------------------ '' - Le message est aussi la section `.text`. '' }}} 1. Dans quelle section se trouve le code de boot dans le code exécutable ? (la réponse est dans `hcpua.S`) {{{#!protected ------------------------------------------------------------------------------------ '' - Dans le programme exécutable, le code de boot est mis dans la section `.boot`. '' }}} 1. Dans quelle région de la mémoire le code de boot est-il placé ? (la réponse est dans `kernel.ld`) {{{#!protected ------------------------------------------------------------------------------------ '' - Le code de boot est placé dans la région `boot_region` '' }}} 1. Comment connaît-on l'adresse du registre de sortie du contrôleur de terminal `TTY` ? (la réponse est dans `kernel.ld' et sur cette page) {{{#!protected ------------------------------------------------------------------------------------ '' - Le fichier `kernel.ld` déclare une variable `__tty_regs_map` initialisée avec l'adresse de où sont placés les registres de contrôles du `TTY`. Le premier registre à l'adresse `__tty_regs_map` est l'adresse du registre de sortie `TTY_WRITE`. '' }}} 1. Le code de boot se contente d'afficher un message, comment sait-on que le message est fini et que le programme doit s'arrêter ? (ou quel est le caractère de fin de chaîne ?) {{{#!protected ------------------------------------------------------------------------------------ '' - C'est quand la boucle d'affichage détecte le `0` terminateur de la chaîne de caractères. '' }}} 1. Pourquoi terminer le programme par un `dead: j dead` ? Notez qu'on ne peut pas encore faire un ''`syscall exit`'' parce qu'il n'y a pas de gestionnaire de syscall et surtout parce `syscall` est une instruction appeler par une application utilisateur, et qu'il n'y en a pas encore. {{{#!protected ------------------------------------------------------------------------------------ '' - If faut arrêter le programme, car il n'y a plus de code, mais on ne sait pas arrêter le processeur, alors on le faire tourner en rond. '' }}} 1. Quelle est la différence entre `#include "file.h"` et `#include ` ? Quelle option du compilateur C permet de spécifier les répertoires lesquels se trouvent les fichiers include ? Si vous ne savez pas interrogez Internet ! {{{#!protected ------------------------------------------------------------------------------------ '' Cours 9 / slides 18 et 58\\\\ Ce sont toujours des connaissances connues en principe, mais comme c'est utilisé dans le code, ce n'est pas inutile d'en parler rapidement. * Avec `#include "file.h"`, le préprocesseur recherche le fichier dans le répertoire local. * Avec `#include `, le préprocesseur recherche le fichier dans les répertoires standards tel que `/usr/include` et dans les répertoires spécifiés par l'option `-I` du préprocesseur. Il peut y avoir plusieurs fois `-I` dans la commande, par exemple `-Idir1 -Idir2 -Idir3`. * C'est donc l'option `-I` qui permet de définir les répertoires de recherche. '' }}} 1. Comment définir une macro-instruction C uniquement si elle n'est pas déjà définie ? Écrivez un exemple. Si vous ne savez pas regardez l'usage de `#ifndef` {{{#!protected ------------------------------------------------------------------------------------ '' Cours 9 / slides 9 et 58\\\\ Cette déclaration est présente à plusieurs endroits dans le code. Elle permet de définir des valeurs de paramètres par défaut, s'ils ne sont pas déjà définis ailleurs, soit plus haut dans le code, ou dans un fichier inclu, ou encore passé en paramètre du compilateur par l'option `-D`. * En utilisant, une directive `#ifndef` : {{{#!c #ifndef MACRO #define MACRO #endif }}} '' }}} 1. Comment être certain de ne pas inclure plusieurs fois le même fichier `.h` ? Vous devriez avoir trouvé l'explication en répondant à la question précédente. {{{#!protected ------------------------------------------------------------------------------------ '' Cours 9 / slides 9 et 58\\\\ C'est un usage de ce qui a été vu dans la question précédente. C'est utilisé dans tous les fichiers .h (sauf oubli). * En utilisant ce que nous venons de voir dans la question précédente : on peut définir une macro instruction différente au début de chaque fichier `.h` (en utilisant le nom du fichier comme nom de macro pour éviter les collisions de nom). On peut alors tester l'existence de cette macro comme condition d'inclusion du fichier. {{{#!c ————————————————————— debut du fichier filename.h #ifndef _FILENAME_H_ #define _FILENAME_H_ [... contenu du fichier ...] #endif ————————————————————— fichier de fichier filename.h }}} '' }}} **Exercices** - Exécutez le programme en lançant le simulateur avec `make exec`, qu'observez-vous ? {{{#!protected ------------------------------------------------------------------------------------ '' - On voit une fenêtre `xterm` qui affiche un message et c'est tout. Dans le terminal de lancement de `make exec`, on voit le compteur de cycles avancer. '' }}} - Exécutez le programme en lançant le simulateur avec `make debug`.\\Cela exécute le programme pour une courte durée et cela produit deux fichiers `trace0.s` et `label0.s`.\\`trace0.s` contient la trace des instructions assembleur exécutées par le processeur. \\Ouvrez `trace.0.s` et repérez ce qui est cité ici - On voit la séquence des instructions exécutées - La première colonne nous informe que les adresses lues sont dans l'espace Kernel - La seconde colonne sont les numéros de cycles - La troisième sont les adresses - La quatrième le code binaire des instructions - Le reste de la ligne contient l'instruction désassemblée - Lorsque les adresses ont un nom, c'est à dire qu'une étiquette leur a été attribuée, celle-ci est indiquée. `label0.s` contient la séquence des appels de fonctions de l'exécutions. C'est en fait un extrait de la trace.\\Ouvrez le fichier `label0.s` et interprétez ce que vous voyez. {{{#!protected ------------------------------------------------------------------------------------ '' {{{#!asm K 12: :-------------------------------------------------------------------------------- K 12: 0xbfc00000 0x3c04bfc0 lui a0,0xbfc0 K 13: 0xbfc00004 0x24840028 addiu a0,a0,40 K 14: 0xbfc00008 0x3c05d020 lui a1,0xd020 K 15: 0xbfc0000c 0x24a50000 addiu a1,a1,0 K 26: :------------------------------------------------------------------------------- K 26: 0xbfc00010 0x80880000 lb t0,0(a0) K 27: 0xbfc00014 0xa0a80000 sb t0,0(a1) K 37: --> READ MEMORY @ 0xbfc00028 BE=---1 --> 0x6c6c6548 K 39: <-- WRITE MEMORY @ 0xd0200000 BE=---1 <-- 0x48 }}} '' }}} - Modifiez le code de `hcpua.S` afin d'afficher le message "Au revoir\n" après le message "Hello".\\ Vous devez avoir deux messages, et pas seulement étendre le premier. {{{#!protected ------------------------------------------------------------------------------------ '' * Il faut dupliquer la boucle d'affichage et le message. Il faut juste faire attention aux labels en en créant des nouveaux. On ne peut pas utiliser des fonctions parce qu'il n'y a pas encore de pile d'exécution. '' }}} = 2. Saut dans le code du noyau en assembleur Dans le deuxième programme, nous restons en assembleur, mais nous avons deux fichiers source : (1) le fichier contenant le code de boot et (2) le fichier contenant le code du noyau. Ici, le code du noyau c'est juste une ''fonction'' `kinit()`. Ce n'est pas vraiment une fonction car on n'utilise pas la pile. **Objectifs** - Savoir comment le programme de boot fait pour sauter à l'adresse de la routine kinit. - Avoir un fichier kernel.ld un peu plus complet. **Fichiers** {{{ 2_init_asm/ ├── hcpua.S : code dépendant du cpu matériel en assembleur ├── kernel.ld : ldscript décrivant l'espace d'adressage pour l'éditeur de lien ├── kinit.S : fichier contenant le code de démarrage du noyau, ici c'est une routine kinit. └── Makefile : description des actions possibles sur le code : compilation, exécution, nettoyage, etc. }}} **Questions** 1. Regarder dans le fichier `hcpua.S`, dans quelle section est désormais le code de boot ? {{{#!protected ------------------------------------------------------------------------------------ '' - Il a été placé dans la section de `.boot` '' }}} 2. Le code de boot ne fait que sauter à l'adresse `kinit` avec l'instruction `jr`, il n'y a pas de retour, ce n'est donc pas un `jal`. Où est défini `kinit` ? Comment le code de boot connait-il cette adresse ? Pourquoi ne pas avoir utilisé `j init` et donc pourquoi passer par un registre ? {{{#!protected ------------------------------------------------------------------------------------ '' - `kinit` est défini dans la `kinit.S`. - `hcpua.S` ne connait pas cette adresse, mais grâce au `.globl kinit`, l'éditeur de lien saura compléter `hcpua.o`, dans l'exécutable. - Le code de boot est en `0xBFC00000`, `kinit` est en `0x80000000`, ces deux adresses ne partagent pas les 4 bits de poids fort, c'est trop loin pour un simple `j`. '' }}} 1. Dans `kernel.ld`, que signifie `*(.*data*)` ? {{{#!protected ------------------------------------------------------------------------------------ '' - C'est une manière de désigner toutes les sections nommées `.*data*` (avec `*` = n'importe quoi) présentes dans n'importe quel fichier objets reçu par l'éditeur de liens. '' }}} 1. Quelle est la valeur de `__kdata_end` ? Pourquoi mettre 2 «`_`» au début des variables du `ldscript` ? ([https://www.gnu.org/software/libc/manual/html_node/Reserved-Names.html réponse]) {{{#!protected ------------------------------------------------------------------------------------ '' - `__kdata_end` est l'adresse du premier octet placé juste après la région data. - les 2 «`_`» permettent d'éviter les conflits avec les noms des symboles (fonction, variable, type, etc.) présents dans le programme. '' }}} **Exercices** - Exécutez le programme sur le simulateur. Est-ce différent de l'étape 1 ? {{{#!protected ------------------------------------------------------------------------------------ '' * Non, c'est le même comportement. '' }}} - Modifiez le code, comme pour l'étape 1, afin d'afficher un second message ? {{{#!protected ------------------------------------------------------------------------------------ '' * C'est très semblable, c'est même identique à l'étape 1, l'idée est d'ouvrir le code... '' }}} = 3. Saut dans la fonction kinit() du noyau en langage C Dans ce troisième programme, nous faisons la même chose que pour le deuxième mais `kinit()` est désormais écrit en langage C. Cela change peu de choses, sauf une chose importante `kinit()` est une fonction et donc il faut absolument une pile d'exécution. **Objectifs** - Savoir comment et où déclarer la pile d'exécution du noyau. - Savoir comment afficher un caractère sur un terminal depuis un programme C. **Fichiers** {{{ 3_init_c/ ├── hcpua.S : code dépendant du cpu matériel en assembleur ├── kernel.ld : ldscript décrivant l'espace d'adressage pour l'éditeur de lien ├── kinit.c : fichier en C contenant le code de démarrage du noyau, ici c'est la fonction kinit(). └── Makefile : description des actions possibles sur le code : compilation, exécution, nettoyage, etc. }}} **Questions** 1. Quand faut-il initialiser la pile ? Dans quel fichier est-ce ? Quelle est la valeur du pointeur initial ? {{{#!protected ------------------------------------------------------------------------------------ '' - Il faut initialiser le pointeur avant d'appeler `kinit()` - C'est dans le fichier `hcpua.S` - `$29` ← `__kdata_end`, c'est-à-dire `0x80400000` '' }}} 1. Dans quel fichier le mot clé `volatile` est-il utilisé ? Rappeler son rôle. {{{#!protected ------------------------------------------------------------------------------------ '' * Il est utilisé dans `kinit.c` pour informer le compilateur que la variable `__tty_regs_map` doit toujours être lue en mémoire et ne peut jamais être "optimisée" dans un registre. Les écritures dans la doivent aussi toujours avoir lieu. Cette variable désigne les registres du contrôleur de terminal. Quand le programme accède en lecture ou écriture à cette variable, il veut accéder au terminal, il faut vraiment qu'il y ait des load/store dans le programme assembleur correspondant au programme source. '' }}} **Exercices** - Exécutez le programme sur le simulateur. Est-ce différent de l'étape 1 ? {{{#!protected ------------------------------------------------------------------------------------ '' * Non, c'est le même comportement :-) '' }}} - Ouvrez les fichiers `kinit.o.s` et `kernel.x.s`, le premier fichier est le désassemblage de `kinit.o` et le second est le désassemblage de `kernel.x`. Dans ces fichiers, vous avez plusieurs sections. Les sections `.MIPS.abiflags`, `.reginfo` et `.pdr` ne nous sont pas utiles (elles servent au chargeur d'application, elles contiennent des informations sur le contenu du fichier et cela ne nous intéresse pas).\\Notez l'adresse de `kinit` dans les deux fichiers, sont-ce les mêmes ? Sont-elles dans les mêmes sections ? Expliquez pourquoi. {{{#!protected ------------------------------------------------------------------------------------ '' * Dans kinit.o.s, l'adresse de `kinit` est `0` alors que `kernel.x.s` l'adresse est `0x80000000`. * Dans kinit.o.s, `kinit` est dans la section `.text` alors que dans `kernel.x.s` `kinit` est dans la section `.ktext`. * La raison est que * dans `kinit.o`, `kinit` n'a pas encore été placé, le compilateur commence toutes ses sections à 0, donc `kinit` est dans la section `.text` et elle commence à 0. * dans `kernel.x.s` `kinit` est placé et mis dans la section `.ktext` comme le fichier `kernel.ld` le demande. '' }}} - Modifiez le code de `kinit.c`, et comme pour l'étape 1, afficher un second message ? {{{#!protected ------------------------------------------------------------------------------------ '' * Hormis, qu'il s'agit de code C, il n'y a pas de différence de principe, c'est toujours du copier-coller, l'important c'est d'ouvrir le code. '' }}} = 4. Accès aux registres de contrôle des terminaux `TTY` Le prototype de SoC que nous utilisons pour les TP est configurable. Il est possible par exemple de choisir le nombre de terminaux texte (TTY). Par défaut, il y en a un mais, nous pouvons en avoir jusqu'à 4. Nous allons modifier le code du noyau pour s'adapter à cette variabilité. En outre, pour le moment, nous ne faisions qu'écrire sur le terminal, maintenant, nous allons aussi lire le clavier. **Objectifs** - Savoir comment compiler un programme C avec du code conditionnel. - Savoir comment décrire en C l'ensemble des registres d'un contrôleur de périphérique et y accéder. **Fichiers** {{{ 4_nttys/ ├── hcpua.S : code dépendant du cpu matériel en assembleur ├── kernel.ld : ldscript décrivant l'espace d'adressage pour l'éditeur de lien ├── kinit.c : fichier en C contenant le code de démarrage du noyau, ici c'est la fonction kinit(). └── Makefile : description des actions possibles sur le code : compilation, exécution, nettoyage, etc. }}} **Questions** 1. Dans le fichier `kinit.c`, il est question d'un loopback, à quoi cela sert-il ? {{{#!protected ------------------------------------------------------------------------------------ '' - Par défaut les caractères tapés au clavier ne sont pas affichés par le matériel. Or, l'utilisateur s'attend au fait que s'il tape sur une touche, celle-ci s'affiche. Comme ce n'est pas le matériel qui le fait c'est au logiciel de le faire. - Le fait que ce soit le logiciel qui fait cette opération de loopback permet de savoir que le programme tourne. Si un caractère s'affiche quand on tape au clavier, c'est qu'un morceau du programme a lu le registre `read` et réécrit cette valeur dans le registre `write`. '' }}} 1. Dans le fichier `kinit.c`, on trouve `__tty_regs_map[ tty%NTTYS ].write = *s`, expliquez le modulo. {{{#!protected ------------------------------------------------------------------------------------ '' * C'est une sécurité, un peu inutile ici, qui permet de ne pas écrire en dehors de la zone des registres du contrôleur de tty '' }}} 1. Exécutez le programme sur le simulateur. Qu'observez-vous ? Est-ce que les deux fenêtres ont le même comportement vis-à-vis du clavier ? {{{#!protected ------------------------------------------------------------------------------------ '' - Deux fenêtres sont apparues avec un message uniquement dans la fenêtre `proc0_term0`. - Non. Quand on tape dans la fenêtre `proc0_term0`, les touches tapées s'affichent alors que rien ne se passe dans la fenêtre `proc0_term1`. '' }}} **Exercices** - Modifiez le code pour afficher un message sur le second terminal, il y a toujours une attente sur le premier terminal. {{{#!protected ------------------------------------------------------------------------------------ '' - C'est juste deux copier-coller (message et boucle d'affichage). '' }}} - Modifiez le code pour que le programme affiche les touches tapées au clavier sur les deux terminaux. C'est-à-dire, ce que vous tapez sur le terminal `proc0_term0` s'affiche sur ce même terminal, et pareil pour `proc0_term1`. L'idée est de ne plus faire d'attente bloquante sur le registre `TTY_STATUS` de chaque terminal. Pour que cela soit plus amusant, changez la casse (minuscule ←→ majuscule) sur le terminal `proc1_term1` (si vous tapez `bonjour 123`, il affiche `BONJOUR 123` et inversement. {{{#!protected ------------------------------------------------------------------------------------ '' {{{#!c void kinit (void) { char *s; char c; for (s = hello_0; *s; s++) { // for all char in hello string __tty_regs_map[ 0%NTTYS ].write = *s; // write it to the tty output register } for (s = hello_1; *s; s++) { // for all char in hello string __tty_regs_map[ 1%NTTYS ].write = *s; // write it to the tty output register } while (1) { if (__tty_regs_map[0].status != 0) { // wait for a char typed on keyboard 0 c = __tty_regs_map[0].read; // read the char __tty_regs_map[0].write = c; // and print it (that is a software loopback) } if (__tty_regs_map[1].status != 0) { // wait for a char typed on keyboard 1 c = __tty_regs_map[1].read; // read the char if ((c >= 'a') && (c <= 'z')) c += 'A' - 'a'; else if ((c >= 'A') && (c <= 'Z')) c += 'a' - 'A'; __tty_regs_map[1].write = c; // and print it (that is a software loopback) } } while (1); }}} '' }}} == B5. Premier petit pilote pour le terminal Dans l'étape précédente, nous accédons aux registres de périphérique directement dans la fonction `kinit()`, ce n'est pas très simple. C'est pourquoi nous allons ajouter un niveau d'abstraction qui représente un début de pilote de périphérique (device driver). Ce pilote, même tout petit constitue une couche logicielle avec une API. **Objectifs** - Savoir comment créer un début de pilote pour le terminal `TTY`. - Savoir comment décrire une API en C - Savoir appeler une fonction en assembleur depuis le C **Fichiers** {{{ 5_driver/ ├── harch.c : code dépendant de l'architecture du SoC, pour le moment c'est juste le pilote du TTY ├── harch.h : API du code dépendant de l'architecture ├── hcpu.h : prototype de la fonction clock() ├── hcpua.S : code dépendant du cpu matériel en assembleur ├── kernel.ld : ldscript décrivant l'espace d'adressage pour l'éditeur de lien ├── kinit.c : fichier en C contenant le code de démarrage du noyau, ici c'est la fonction kinit(). └── Makefile : description des actions possibles sur le code : compilation, exécution, nettoyage, etc. }}} **Questions** 1. Le code du driver du TTY est dans le fichier `harch.c` et les prototypes sont dans `harch.h`. Si vous ouvrez `harch.h` vous allez voir que seuls les prototypes des fonctions `tty_gets()` et `tty_puts()` sont présents. La structure décrivant la carte des registres du `TTY` est déclarée dans le `.c`. Pourquoi avoir fait ainsi ? {{{#!protected ------------------------------------------------------------------------------------ '' - Le noyau n'a pas besoin de savoir comment sont organisés les registres dans le TTY. Il a juste besoin de savoir comment écrire ou lire un message. Plus c'est cloisonné, moins il y a de risque de problèmes. En outre, cela simplifie un hypothétique portage sur une autre architecture. '' }}} 1. Le MIPS dispose d'un compteur de cycles internes. Ce compteur est dans un banc de registres accessibles uniquement quand le processeur fonctionne en mode `kernel`. Nous verrons ça au prochain cours, mais en attendant nous allons quand même exploiter ce compteur. Pourquoi avoir mis la fonction dans `hcpua.S` ? Rappeler, pourquoi avoir mis `.globl clock` {{{#!protected ------------------------------------------------------------------------------------ '' * La fonction qui lit ce registre (Attention ce `$9` ne désigne pas un registre GPR du processeur !) est nécessairement en assembleur car elle utilise des instructions particulières et dépend du matériel, elle est donc mise dans `hcpua.S`. * `.globl clock` permet de faire en sorte que la fonction soit visible par les autres fichiers C. '' }}} 1. Compilez et exécutez le code avec `make exec`. Observez. Ensuite ouvrez le fichier `kernel.x.s` et regardez où a été placée la fonction `clock()`.\\Est-ce un problème si `kinit()` n'est plus au début du segment `ktext` ? Posez-vous la question de qui a besoin de connaître l'adresse de `kinit()` {{{#!protected ------------------------------------------------------------------------------------ '' * Non, ce n'est pas un problème puisque ça fonctionne. Le code de boot a besoin de l'adresse de `kinit()` mais on l'obtient avec la macro `la` - c'est l'éditeur de lien qui fera en sorte que dans les codes binaire l'adresse de `kinit()` mise dans le registre `$26` soit la bonne. '' }}} **Exercices** - Afin de vous ''détendre un peu'', vous allez créer un petit jeu `guess` - `guess` tire un nombre entre '0' et '9' et vous devez le deviner en faisant des propositions.\\`guess` vous dit si c'est trop grand ou trop petit. Ce programme ne va révolutionner votre vie de programmeur(se), mais bon, c'est probablement le premier programme que vous allez écrire et faire tourner sur une machine sans système d'exploitation. __Étapes__ - Vous créez deux fichiers `guess.c` et `guess.h`. - `guess.c` contient le jeu il y au moins une fonction `guess()` - `guess.h` contient les déclarations externes de `guess.c` - `kinit()` doit lancer `guess()` - `guess()` - vous demande de taper une touche pour démarrer le jeu. - effectue un tirage d'un nombre en utilisant la fonction `clock()` et ne gardant que le chiffre de poids faible (ce n'est pas aléatoire, mais c'est mieux que rien) - exécute en boucle jusqu'à réussite - demande d'un chiffre - comparaison avec le tirage et affichage des messages `"trop grand"`, `"trop petit"` ou `"gagné"` - Vous devrez modifier le Makefile puisque vous avez un fichier à compiler en plus. - Si c'est trop facile, vous pouvez complexifier en utilisant des nombres à 2 chiffres ou plus. {{{#!protected [htdocs:files/6_guess.tgz 6_guess.tgz à décompresser dans tp1]\\\\ **kinit.h** {{{#!c #include #include char hello[] = "Hello World!\n\n"; char end[] = "\nend!\n"; void kinit (void) { tty_puts (0, hello); // print hello string guess(); tty_puts (0, end); // print end string on terminal 0 while (1); // infinite loop } }}} **guess.h** {{{#!c #ifndef _GUESS_H_ #define _GUESS_H_ extern void guess (void); #endif }}} **guess.c** {{{#!c #include #include #include void guess (void) { char c; int num; int random; do { random = clock() % 10; // only one digit do { do { tty_puts(0,"Donnez un nombre : "); c = tty_getc(0); } while ( (c < '0') && (c > '9') ); num = c - '0'; if (num < random) tty_puts(0," -> trop petit\n"); else if (num > random) tty_puts(0," -> trop grand\n"); } while (random != num); tty_puts(0,"\nGagné\n"); tty_puts(0,"\nOn recommence [Y]/N ? "); c = tty_getc(0); tty_puts(0,"\n\n"); } while (c != 'N'); } }}} }}}