{{{#!protected **[https://www-soc.lip6.fr/trac/archi-l3s5/wiki/AS5-TME9?action=edit EDIT]** }}} [[PageOutline]] {{{#!html

Boot et premier programme en mode kernel

}}} Cette page décrit la séance complète : TD et TME. Elle commence par des exercices à faire sur papier et puis elle continue et se termine par des questions sur le code et quelques exercices de codage simples à écrire et tester sur le prototype. La partie pratique est découpé en 5 étapes. Pour chaque étape, nous donnons une brève description, suivie d'une liste des objectifs principaux et d'une liste des fichiers présents. Un bref commentaire est ajouté pour les fichiers. Vous avez une liste de questions simple et l'exercice de codage. 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/AS5-9-2p.pdf Cours de démarrage présentant l'architecture matérielle et logicielle que vous allez manipuler] ''obligatoire'' * [htdocs:cours/doc_MIPS32.pdf Document sur l'assembleur du MIPS et la convention d'appel des fonctions] : ''recommandé'' * [wiki:Howto-TP Configuration de l'environnement des TP] : ''obligatoire'' * [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 commencer par récupérer [htdocs:files/tp1.tgz l'archive code du tp1] * Assurez vous que vous avez déjà sourcé le fichier `Source-me.sh` (il doit être dans votre `.bashrc`). * Placez cette archive dans le répertoire AS5 et décompressez-la avec **`tar xvzf tp1.tgz`** * Après décompression, avec la commande **`tree -L 1 tp1`**, vous devriez obtenir ceci: {{{#!bash tp1 ├── 1_hello_boot ├── 2_init_asm ├── 3_init_c ├── 4_nttys ├── 5_driver └── Makefile }}} = A. Travaux dirigés == A1. Analyse de l'architecture Les trois figures ci-dessous donnent des informations sur l'architecture du prototype **almo1** sur lequel vous allez travailler. * À droite, vous avez un schéma de connexion simplifié. * Au centre, vous avez la représentation des 4 registres internes du contrôleur de terminal `TTY` nécessaires pour commander un couple écran-clavier. * À gauche, vous avez la représentation de l'espace d'adressage implémenté pour le prototype. [[Image(htdocs:img/almo1.png,nolink,height=300)]] [[Image(htdocs:img/TTY.png,nolink,height=200,top)]] [[Image(htdocs:img/espace_adresse.png,nolink,height=300)]] **Questions** {{{#!protected ------------------------------------------------------------------------------------ ''Les réponses sont dans les transparents du cours }}} 1. Il y a deux mémoires dans **almo1** : RAM et ROM. Qu'est-ce qui les distinguent et que contiennent-elles ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' * La ROM est une mémoire morte, c'est-à-dire en lecture seule. Elle contient le code de démarrage du prototype. * La RAM est une mémoire vive, c'est-à-dire pouvant être lue et écrite. Elle contient le code et les données. ''''''''''''''' }}} 1. Qu'est-ce l'espace d'adressage du MIPS ? Quel taille fait-il ?\\Quelles sont les instructions du MIPS permettant d'utiliser ces adresses ? Est-ce synonyme de mémoire ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' * L'espace d'adressage du MIPS est l'ensemble des adresses que peut former le MIPS. * Les adresses sont sur 32 bits qui désigne chacune un octet, il y a donc 2^32^ octets. * On accède à l'espace d'adressage avec les instructions load/store (`lw`, `lh`, `lb`, `lhu`, `lbu`, `sw`, `sh`, `sb`). * Non, les mémoires sont des composants contenant des cases de mémoire adressable. Les mémoires sont placées (on dit aussi « ''mappées'' » dans l'espace d'adressage). ''''''''''''''' }}} 1. Dans quel composant matériel se trouve le code de démarrage et à quel adresse est-il placé dans l'espace d'adressage et pourquoi à cette adresse ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' * Le code de boot est dans la mémoire ROM. * Il commence à l'adresse `0xBFC0000` parce que c'est l'adresse qu'envoie le MIPS au démarrage. ''''''''''''''' }}} 1. Quel composant permet de faire des entrées-sorties dans almo1 ?\\Citez d'autres composants qui pourraient être présents dans un autre SoC ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' * Ici, c'est le composant `TTY` qui permet de sortir des caractères sur un écran et de lire des caractères depuis un clavier. * Dans un autre SoC, on pourrait avoir un contrôleur de disque, un contrôleur vidéo, un port réseau Ethernet, un port USB, des entrées analogiques (pour mesurer des tensions), etc. ''''''''''''''' }}} 1. Il y a 4 registres dans le contrôleur de `TTY`, a quelles adresses sont-il placés dans l'espace d'adressage ?\\Comme ce sont des registres, est-ce que le MIPS peut les utiliser comme opérandes pour ses instructions (comme add, or, etc.) ?\\Dans quel registre faut-il écrire pour envoyer un caractère sur l'écran du terminal (implicitement à la position du curseur) ?\\Que contiennent les registres `TTY_STATUS` et `TTY_READ` ?\\Quelle est l'adresse de `TTY_WRITE` dans l'espace d'adressage ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' * Le composant `TTY` est placé à partir de l'adresse `0xD0200000`. * Non, ce sont des registres de périphériques placés dans l'espace d'adressage et donc accessibles par des instructions load/store uniquement. * Pour écrire un caractère sur l'écran, il faut écrire le code ASCII du caractère dans le registre `TTY_WRITE` * `TTY_STATUS` contient 1 s'il y a au moins un caractère en attente d'être lu, `TTY_READ` contient le code ASCII du caractère tapé au clavier si `TTY_STATUS==1` ''''''''''''''' }}} 1. Le contrôleur de `TTY` peut contrôler de 1 à 4 terminaux. Chaque terminal dispose d'un ensemble de 4 registres (on appelle ça une carte de registres, ou en anglais une ``register map``). Ces ensembles de 4 registres sont placés à des adresses contiguës. S'il y a 2 terminaux (`TTY0` et `TTY1`), A quelle adresse est le registre `TTY_READ` de `TTY1` ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' * Si les adresses utilisées par `TTY0` commencent à `0xd0200000` alors celles de `TTY1` commencent à l'adresse `0xd0200010` et donc `TTY_READ` est à l'adresse `0xd0200018`. ''''''''''''''' }}} 1. Que représentent les flèches bleues sur le schéma ? Pourquoi ne vont-elles que dans une seule direction ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' * Ces flèches représentent les requêtes d'accès à la mémoire, c'est-à-dire les ''loads'' et les ''stores'' qui sont émis par le MIPS lors de l'exécution des instructions `lw`, `sw`, etc. Les requêtes sont émises par le MIPS et reçues par les composants mémoires ou périphériques. * On ne représente pas les données qui circulent, mais juste les requêtes, pour ne pas alourdir inutilement le schéma. Implicitement, si le MIPS envoie une requête de lecture alors il y aura une donnée qui va revenir, c'est obligatoire, alors on ne la dessine pas, car ce n'est pas intéressant. En revanche, le fait que le MIPS soit le seul composant à émettre des requêtes est une information intéressante. ''''''''''''''' }}} == A2. Programmation assembleur L'usage du code assembleur est réduit au minimum. Il est utilisé uniquement où c'est indispensable. C'est le cas du code de démarrage. Ce code ne peut pas être écrit en C au moins une raison importante. Le compilateur C suppose la présence d'une pile et d'un registre du processeur contenant le pointeur de pile, or au démarrage les registres sont vides (leur contenu n'est pas significatif). Dans cette partie, nous allons nous intéresser à quelques éléments de l'assembleur qui vous permettront de comprendre le code en TP. **Questions** 1. Nous savons que l'adresse du premier registre du `TTY` est `0xd0200000` est qu'à cette adresse se trouve le registre `TTY_WRITE` du `TTY0`. Écrivez le code permettant d'écrire le code ASCII `'x'` sur le terminal 0. Vous avez droit à tous les registres du MIPS. {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' {{{#!asm lui $4, 0xD020 ori $4, $4, 0x0000 // cette instruction ne sert a rien puisqu on ajoute 0, mais je la met pour le cas general ori $5, 'x' sb $5, ($4) // Notez que le 0 devant ($4) n est pas obligatoire }}} ''''''''''''''' }}} 1. Le problème est que l'adresse du `TTY` est un choix de l'architecte du prototype et s'il décide de placer le `TTY` ailleurs dans l'espace d'adressage, il faudra réécrire le code précédent. Nous allons utiliser une étiquette, supposons que l'adresse du premier registre du `TTY` se nomme `__tty_regs_map`. Le code assembleur ne connait pas l'adresse, il ne connait que le symbole. Si nous voulons toujours écrire `'x'` sur le terminal 0. Nous allons utiliser la macro `la $r, label` qui est remplacée par les deux instructions `lui` et `ori`. Pour être plus précis {{{#!asm la $r, label }}} est remplacé par {{{#!asm lui $r, label>>16 ori $r, $r, label & 0xFFFF }}} Réécrivez le code précédent en utilisant `la` {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' {{{#!asm la $4, __tty_regs_map ori $5, 'x' sb $5, ($4) }}} ''''''''''''''' }}} == A3. Chaîne de compilation == A4. Programmation en C * fonction write buffer en assembleur et en C * Makefile et make recurs * Les struct et les tableaux de structs * fonction read buffer en C * les usages de extern/globl et de volatile * compilation conditionnelle = Travaux Pratiques == 1. Premier programme en assembleur dans la seule section de boot**= **ajouter des analyses des fichiers objets et des traces** 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 ├── hcpu.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**\\ ''Les réponse sont dans le cours ou dans les fichiers sources''\\\\ - 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 variable 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. ''''''''''''''' }}} - Dans quel fichier se trouve le code de boot et pourquoi avoir nommé ce fichier ainsi ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Le code de boot est dans le fichier `hcpu.S`. Il a a été nommé ainsi parce que c'est du code qui dépend du hardware et qu'il concerne le cpu. ''''''''''''''' }}} - A 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 mémoires, dont une région commençant à cette adresse-là, et c'est dans cette région que l'on met le code de boot. ''''''''''''''' }}} - 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` ''''''''''''''' }}} - 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`) ''''''''''''''' }}} - 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`) ''''''''''''''' }}} - 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`. ''''''''''''''' }}} - Dans quelle section se trouve le message hello pour le compilateur ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Le message est aussi la section `.text`. ''''''''''''''' }}} - Dans quelle section se trouve le code de boot dans le code exécutable ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Dans le programme exécutable, le code de boot est mis dans la section `.boot`. ''''''''''''''' }}} - Dans quelle région de la mémoire le code de boot est-il placé ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Le code de boot est placé dans la région `boot_region` ''''''''''''''' }}} - Comment connaît-on l'adresse du registre de sortie du contrôleur de terminal `TTY` ? {{{#!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`. ''''''''''''''' }}} - 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 ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - C'est quand la boucle d'affichage détecte le `0` terminateur de la chaîne de caractères. ''''''''''''''' }}} - Pourquoi terminer le programme par un `dead: j dead` ? {{{#!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. ''''''''''''''' }}} * **Exercice - Modifiez le code de `hcpu.S` afin d'afficher le message "Au revoir\n" (''Hommage VGE'') après le message "Hello".\\ Vous devez avoir deux messages, et pas seulement étendre le premier. {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' * Ils doivent dupliquer la boucle d'affichage et le message. Il faut juste faire attention aux labels en en créant des nouveaux. et ils ne peuvent pas utiliser des fonctions parce qu'ils n'ont pas de pile. ''''''''''''''' }}} == 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/ ├── hcpu.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**\\ ''Les réponses sont dans le cours ou dans les fichiers sources''\\\\ - Regarder dans le fichier `hcpu.S`, dans quelle section est désormais le code de boot ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Réponse ''''''''''''''' }}} - Le code de boot ne fait que sauter dans la fonction kinit avec l'instruction `j`, il n'y a pas de retour, ce n'est donc pas un `jal`, mais pourquoi ne pas avoir utilisé `j init` et donc pourquoi passer par un registre ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - On ne sait pas le code de boot est en `0xBFC00000`. L'instruction `j imm26` permet de sauter à n'importe quelle adresse d'instruction `0xB.......` (dans le memento MIPS `j imm26` : `PC <- PC[31:28] || imm26*4` ''''''''''''''' }}} - Dans `kernel.ld`, la définition de la mémoire est plus complète, elle contient 3 régions : pour le code de boot `boot_region` pour le code du noyau `ktext_region` et pour les données globales du noyau `kdata_region`. Ces régions ne contiennent qu'une section de sorties (resp. `.boot`, `.ktext` et `.kdata`) remplies avec les sections d'entrées produites par le compilateur.\\ 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 le compilateur. ''''''''''''''' }}} - Quelle est la valeur de `__kdata_end` ? Pourquoi, selon vous, mettre 2 «`_`» au début des variables ? {{{#!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. ''''''''''''''' }}} - {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} - {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} - {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} - {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} == 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/ ├── hcpu.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**\\ ''Les réponses sont dans le cours ou dans les fichiers sources''\\\\ - Question ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Réponse ''''''''''''''' }}} == 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 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/ ├── hcpu.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**\\ ''Les réponses sont dans le cours ou dans les fichiers sources''\\\\ - Question ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Réponse ''''''''''''''' }}} == 5. Premier petit pilote pour le terminal Dans l'étape 4, nous accédons au registre 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 **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.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**\\ ''Les réponses sont dans le cours ou dans les fichiers sources''\\\\ - Question ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Réponse ''''''''''''''' }}} - Quel est le nom de la directive assembleur permettant de déclarer une section