}}}
Cette page décrit la séance complète : TD et TP. 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é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/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 être sur une **machine Linux** native ou virtualisée (sinon lisez [https://www-soc.lip6.fr/trac/archi-l3s5/wiki/Howto-TP Configuration de l'environnement des TP → Étape 1])
* Vous devez avoir le répertoire **`$HOME/AS5`** contenant le prototype **almo1** et le **compilateur MIPS** (sinon lisez [https://www-soc.lip6.fr/trac/archi-l3s5/wiki/Howto-TP Configuration de l'environnement des TP → Étape 2])
* Téléchargez **[htdocs:files/tp1.tgz l'archive code du tp1]** et placez là dans le répertoire `$HOME/AS5`
* Assurez-vous que vous avez déjà sourcé le fichier `Source-me.sh` (sinon lisez [https://www-soc.lip6.fr/trac/archi-l3s5/wiki/Howto-TP Configuration de l'environnement des TP → Étape 3])
* Ouvrez un `terminal`, allez dans le répertoire `AS5` (**`cd ~/AS5`**) et décompressez l'archive du tp1 avec **`tar xvzf tp1.tgz`**\\''Cette étape est peut-être inutile si vous avez déjà fait la décompression de l'archive au moment de son téléchargement.''
* Dans le `terminal`, exécutez la commande **`cd ; tree -L 2 AS5`**. Vous devriez obtenir ceci:\\''(si vous n'avez pas `tree` sur votre Linux, vous pouvez l'installer, c'est un outil utile, mais pas indispensable pour ces TP)''
{{{#!bash
/home/osboxes/AS5
├── bin
│ ├── almo1.x
│ ├── gcc
│ ├── Source-me.sh
│ ├── test
│ └── tracelog
└── 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**
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 ? Quelle 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ésignent 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`, à quelles adresses sont-ils 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 mets 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 connaît 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`. Il existe aussi la macro `li` pour initialiser des valeurs 32bits dans un registre. Pour être plus précis, les instructions
{{{#!asm
la $r, label
li $r, 0x87654321
}}}
sont remplacés par
{{{#!asm
lui $r, label>>16
ori $r, $r, label & 0xFFFF
lui $r, 0x8765
ori $r, $r, 0x4321
}}}
Réécrivez le code de la question précédente en utilisant `la` et `li`
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
{{{#!asm
la $4, __tty_regs_map
li $5, 'x'
sb $5, ($4)
}}}
'''''''''''''''
}}}
1. En assembleur pour sauter à une adresse de manière inconditionnelle, on utilise les instructions `j label` ou `jr $r`, peuvent-elles faire les choses ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* Presque, mais pas tout à fait
- `j label` effectue en fait un saut relatif au `PC` puisqu’elle fait `PC ← PC&0xF0000000 + (label<<2)&0x0FFFFFFF)`. Les 4 bits de poids forts du `PC` sont conservés.
- `jr $r` effectue un vrai saut absolu puisqu’elle fait `PC ← $r`
Autrement dit, si l’on veut aller n'importe où en mémoire, il faut utiliser `jr`.
'''''''''''''''
}}}
1. Vous avez utilisé les directives `.text` et `.data` pour définir les sections où placer les instructions et les variables globales, mais il existe la possibilité de demander la création de nouvelles sections dans le code objet produit par le compilateur avec la directive `.section name,"flags"`
- `name` est le nom de la nouvelle section on met souvent un `.name` pour montrer que c'est une section et
- `"flags"` informe du contenu `"ax"` pour des instructions, `"ad"` pour des données (ceux que ça intéresse pourront regarder là [https://frama.link/20UzK0FP])
Écrivez le code assembleur créant la section ".mytext" et suivi de l'addition des registres `$5` et `$6` dans `$4`
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
{{{#!asm
.section .mytext,"ax"
add $4,$5,$6
}}}
'''''''''''''''
}}}
1. À quoi sert la directive `.globl label` ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* `globl` signifie `glob`al `l`abel. Cette directive permet de dire que le `label` est visible en dehors de son fichier de définition. Ainsi il est utilisable dans d'autres programmes assembleur ou d'autres programmes C.
'''''''''''''''
}}}
1. Écrivez une séquence de code qui affiche la chaîne de caractère `"Hello"` sur `TTY0`. Ce n'est pas une fonction et vous pouvez utiliser tous les registres que vous voulez. Vous supposez que `__tty_regs_maps` est déjà défini.
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
{{{#!asm
.data
hello: .asciiz "Hello"
.text
la $4, hello // $4 <- address of string
la $5, __tty_regs_map // $5 <- address of tty's registers map
print:
lb $8, ($4) // get current char
sb $8, ($5) // send the current char to the tty
addiu $4, $4, 1 // point to the next char
bnez $8, print // check that it is not null, if ok it must be printed
}}}
'''''''''''''''
}}}
1. En regardant, le dessin de l'espace d'adressage du prototype **almo1**, à quelle adresse devra être initialisé le pointeur de pile pour le kernel. Rappelez pourquoi c'est indispensable de le définir avant d'appeler une fonction C et écrivez le code qui fait l'initialisation, en supposant que l'adresse du pointeur porte le nom `__kdata_end`
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* La pile va être initialisée juste à la première adresse au-delà de la zone kdata : `0x80020000` + `0x003E0000` = `0x80400000`
* La première chose que fait une fonction, c'est décrémenter le pointeur de pile pour écrire `$31`, etc. Il faut donc que le pointeur ait été défini.
{{{#!asm
la $29, __kdata_end
}}}
'''''''''''''''
}}}
== A2. Programmation en C
Vous savez déjà programmer en C, mais vous allez voir des syntaxes ou des cas d'usage que vous ne connaissez peut-être pas encore.
Les questions qui sont posées ici n'ont pas toutes été vues en cours, mais vous connaissez peut-être les réponses, sinon ce sera l'occasion d'apprendre.
**Questions
1. Quels sont les usages du mot clé `static` en C ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
1. Déclarer `static` une variable globale ou une fonction en faisant précéder leur définition du mot clé `static` permets de limiter la visibilité de cette variable ou de cette fonction au seul fichier de déclaration. Notez que par défaut les variables et les fonctions du C ne sont pas `static`, il faut le demander explicitement. C'est exactement l'inverse en assembleur où tous les labels sont static et il faut demander avec `.globl` pour le rendre visible.
1. Déclarer `static` une variable locale permet de la rendre persistante, c'est-à-dire qu'elle conserve sa valeur entre deux appels. Cette variable locale n'est pas dans le contexte de la fonction (celui-ci est dans la pile et il est libéré en sortie de fonction). Une variable locale statique est en fait une variable globale dont l'usage est limité à la seule fonction où elle est définie.
'''''''''''''''
}}}
1. Pourquoi déclarer des fonctions ou des variables `extern` ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* Les déclarations `extern` permettent d'informer que le compilateur qu'une variable ou qu'une fonction définie ailleurs existe. Le compilateur est ainsi informé du type de la variable ou du prototype des fonctions, il sait donc comment les utiliser. En C par défaut les variables et les fonctions
* Il n'y a pas de déclaration extern en assembleur par ce n'est pas un langage typé. Pour l'assembleur, un label c'est juste un nombre.
'''''''''''''''
}}}
1. Comment déclarer un tableau de structures en variable globale ? La structure est nommée `test_s` a deux champs `int` nommés `a` et `b`. Le tableau est nommé `tab` a 2 cases.
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
{{{#!c
struct test_s {
int a;
int b;
};
struct test_s tab[2];
}}}
'''''''''''''''
}}}
1. Quelle est la différence entre `#include "file.h"` et `#include ` ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* 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`.
'''''''''''''''
}}}
1. Comment définir une macroinstruction du processeur uniquement si elle n'est pas définie ? Écrivez un exemple.
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* En utilisant, une directive `#ifndef`, par exemple :
{{{#!c
#ifndef MACRO
#define MACRO
#endif
}}}
'''''''''''''''
}}}
1. Comment être certain de ne pas inclure plusieurs fois le même fichier `.h` ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* En utilisant ce que nous venons de voir dans la question précédente. Si vous définissez une macro 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) et que vous nous commencer tester que cette variable n'est pas définie comme condition d'inclusion.
{{{#!c
————————————————————— début du fichier filename.h
#ifndef _FILENAME_H_
#define _FILENAME_H_
[... contenu du fichier ...]
#endif
————————————————————— fichier de fichier filename.h
}}}
'''''''''''''''
}}}
1. Supposons que la structure `tty_s` et le tableau de registres de `TTY` soient définis comme suit. Écrivez une fonction C `int getchar(void)` bloquante qui attend un caractère tapé au clavier sur le `TTY0`. Nous vous rappelons qu'il faut attendre que le registre `TTY_STATUS` soit différent de 0 avant de lire `TTY_READ`.
{{{#!c
struct tty_s {
int write; // tty's output address
int status; // tty's status address something to read if not null)
int read; // tty's input address
int unused; // unused address
};
extern volatile struct tty_s __tty_regs_map[NTTYS];
}}}
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
{{{#!c
int getchar(void)
{
while (__tty_regs_map[0].status == 0);
return __tty_regs_map[0].read;
}
}}}
'''''''''''''''
}}}
1. Savez-vous à quoi sert le mot clé `volatile` ? Nous n'en avons pas parlé en cours, mais c'est nécessaire pour les adresses des registres de périphérique, une idée ... ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* `volatile` permet de dire à `gcc` que la variable en mémoire peut changer à tout moment, elle est volatile. Ainsi quand le programme demande de lire une variable `volatile` le compilateur doit toujours aller la lire en mémoire. Il ne doit jamais chercher à optimiser en utilisant un registre afin de réduire le nombre de lecture mémoire (load). De même, quand le programme écrit dans une variable `volatile`, cela doit toujours provoquer une écriture dans la mémoire (store).
* Ainsi, les registres de périphériques doivent toujours être directement lus ou écrits à chaque fois que le programme le demande, parce que c'est justement ces lectures et ces écritures qui commandent le périphérique.
'''''''''''''''
}}}
== A3. Compilation
Pour obtenir le programme exécutable, nous allons utiliser :
* `gcc -o file.o -c file.c`
- Appel du compilateur avec l'option `-c` qui demande à `gcc` de faire le préprocessing puis la compilation c pour produire le fichier objet `file.o`
* `ld -o bin.x -Tkernel.ld files.o ...`
- Appel de l'éditeur de lien pour produire l'exécutable `bin.x` en assemblant tous les fichiers objets `.o`, en les plaçant dans l'espace d'adressage et résolvant les liens entre eux (quand un `.o` utilise une fonction ou une variable définie dans un autre `.o`).
* `objdump -D file.o > file.o.s` ou `objdump -D bin.x > bin.x.s`
- Appel du désassembleur prend les fichiers binaires (`.o` ou `.x`) pour retrouver le code produit par le compilateur à des fins de debug ou de curiosité.
**Questions**
Le fichier `kernel.ld` décrit l'espace d'adressage et la manière de remplir les sections dans le programme exécutable.
{{{#!c
__tty_regs_map = 0xd0200000 ;
__boot_origin = 0xbfc00000 ;
__boot_length = 0x00001000 ;
__ktext_origin = 0x80000000 ;
__ktext_length = 0x00020000 ;
[... question 1 ...]
__kdata_end = __kdata_origin + __kdata_length ;
MEMORY {
boot_region : ORIGIN = __boot_origin, LENGTH = __boot_length
ktext_region : ORIGIN = __ktext_origin, LENGTH = __ktext_length
[... question 2 ...]
}
SECTIONS {
.boot : {
*(.boot)
} > boot_region
[... question 3 ...]
.kdata : {
*(.*data*)
} > kdata_region
}
}}}
1. Le fichier commence par la déclaration des variables donnant des informations sur les adresses et les tailles des régions de mémoire. Ces symboles n'ont pas de type et ils sont visibles de tous les programmes c, il faut juste leur donner un type pour le compilateur puisse les exploiter, c'est ce que nous avons fait pour `extern volatile struct tty_s __tty_regs_map[NTTYS]`. En regardant, dans le dessin de la représentation de l'espace d'adressage, complétez les lignes de déclaration des variables pour la région `kdata_region`
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
{{{#!c
__kdata_origin = 0x80020000 ;
__kdata_length = 0x003E0000 ;
}}}
'''''''''''''''
}}}
1. Le fichier contient ensuite la déclaration des régions qui vont être remplies par les sections trouvées dans les fichiers objets. Complétez les lignes propres à la déclaration de la région `kdata_region`.
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
{{{#!c
kdata_region : ORIGIN = __kdata_origin, LENGTH = __kdata_length
}}}
'''''''''''''''
}}}
1. Enfin le fichier contient comment sont remplies les régions avec les sections. Complétez les lignes correspondant à la description du remplissage de la région `ktext_region`. Vous devez la remplir avec les sections `.text` issus de tous les fichiers.
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
{{{#!c
.ktext : {
*(.text)
} > ktext_region
}}}
'''''''''''''''
}}}
Nous allons systématiquement utiliser des Makefiles pour la compilation du code, mais aussi pour lancer le simulateur du prototype **almo1**. Pour cette première séance, les Makefiles ne permettent pas de faire des recompilations partielles de fichiers. Les Makefiles sont utilisés pour agréger toutes les actions que nous voulons faire sur les fichiers, c'est-à-dire : compiler, exécuter avec ou sans trace, nettoyer le répertoire. Nous avons recopiez partiellement le premier Makefile pour montrer sa forme et poser quelques questions, auxquels vous savez certainement répondre.
{{{#!make
# Tools and parameters definitions
# ------------------------------------------------------------------------------
NTTY ?= 2 # default number of ttys
CC = mipsel-unknown-elf-gcc # compiler
LD = mipsel-unknown-elf-ld # linker
OD = mipsel-unknown-elf-objdump # desassembler
SX = almo1.x # prototype simulator
CFLAGS = -c # stop after compilation, then produce .o
CFLAGS += -Wall -Werror # gives almost all C warnings and considers them to be errors
CFLAGS += -mips32r2 # define of MIPS version
CFLAGS += -std=c99 # define of syntax version of C
CFLAGS += -fno-common # do not use common sections for non-static vars (only bss)
CFLAGS += -fno-builtin # do not use builtin functions of gcc (such as strlen)
CFLAGS += -fomit-frame-pointer # only use of stack pointer ($29)
CFLAGS += -G0 # do not use global data pointer ($28)
CFLAGS += -O3 # full optimisation mode of compiler
CFLAGS += -I. # directories where include files like are located
CFLAGS += -DNTTYS=$(NTTY) # #define NTTYS with the number of ttys in the prototype
# Rules (here they are used such as simple shell scripts)
# ------------------------------------------------------------------------------
help:
@echo "\nUsage : make [NTTY=num]\n"
@echo " compil : compiles all sources"
@echo " exec : executes the prototype"
@echo " clean : clean all compiled files\n"
compil:
$(CC) -o hcpu.o $(CFLAGS) hcpu.S
@$(OD) -D hcpu.o > hcpu.o.s
$(LD) -o kernel.x -T kernel.ld hcpu.o
@$(OD) -D kernel.x > kernel.x.s
exec: compil
$(SX) -KERNEL kernel.x -NTTYS $(NTTY)
clean:
-rm *.o* *.x* *~ *.log.* proc?_term? 2> /dev/null || true
}}}
4. Au début du fichier se trouve la déclaration des variables du Makefile, quelle est la différence entre `=`, `?=` et `+=` ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* `=` fait une affectation simple
* `?=` fait une affection de la variable si elle n'est pas déjà définie comme variable d'environnement du shell ou dans la ligne de commande de make, par exemple avec `FROM`
* `+=` concatène la valeur courante à la valeur actuelle, c'est une concaténation de chaîne de caractères.
'''''''''''''''
}}}
1. Où est utilisé `CFLAGS` ? Que fait `-DNTTYS=$(NTTY)` et pourquoi est-ce utile ici ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* La variable `CFLAGS` est utilisée par `gcc`, il y a ici toutes les options indispensables pour compiler mais il en existe des tonnes !
* `-DNTTYS=$(NTTY)` permet de définir un #define en ligne de commande et donc de changer les sources sans ouvrir les codes.
'''''''''''''''
}}}
1. Si on exécute `make` sans cible, que se passe-t-il ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* C'est la première cible qui est choisie, donc c'est équivalent à `make help`. Cela affiche l'usage pour connaître les cibles disponibles.
'''''''''''''''
}}}
1. à quoi servent `@` et `-` au début de certaines commandes ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* `@` permet de ne pas afficher la commande avant son exécution. On peut rendre ce comportement systématique en ajoutant la règle `.SILENT:` n'importe où dans le fichier.
* `-` permet de ne pas stopper l'exécution des commandes même si elles rendent une erreur, c'est-à-dire une valeur de sortie différente de 0.
'''''''''''''''
}}}
----
= B. Travaux pratiques
== B1. 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. 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 exercises pratiques.
**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**
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 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.
'''''''''''''''
}}}
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 `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.
'''''''''''''''
}}}
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 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.
'''''''''''''''
}}}
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 ------------------------------------------------------------------------------------
'''''''''''''''
1. 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 ?
{{{#!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é ?
{{{#!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` ?
{{{#!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 ?
{{{#!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` ?
{{{#!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.
'''''''''''''''
}}}
**Exercices**
- Exécutez le programme en lançant le simulateur avec `make exec`, qu'observez-vous ?
- 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 un fichier `debug.log` contenant des informations par cycle.\\Ce fichier n'est pas exploitable directement par vous, il est nécessaire pour la génération de la trace d'exécution à l'étape suivante.
- Exécutez alors la génération de la trace d'exécution avec `make trace`.\\Cela génère le fichier `trace.log.s` à partir du de l'exécutable désassemblé et du fichier debug.log. Que voyez-vous dans `trace.log.s` ?
- 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.
Ils ne peuvent pas utiliser des fonctions parce qu'ils n'ont pas de pile.
'''''''''''''''
}}}
== B2. 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**
1. Regarder dans le fichier `hcpu.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 `j`,
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`.
- `hcpu.S` ne connait pas cette adresse, mais grâce au `.globl kinit`, l'éditeur de lien saura compléter `hcpu.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 le compilateur.
'''''''''''''''
}}}
1. Quelle est la valeur de `__kdata_end` ? Pourquoi mettre 2 «`_`» au début des variables du `ldscript` ?
{{{#!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, voire identique, à l'étape 1, l'idée est qu'ils ouvrent le code...
'''''''''''''''
}}}
== B3. 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**
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 `hcpu.S`
- '$29' ← '__kdata_end', c'est-à-dire `0x80400000`
'''''''''''''''
}}}
**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 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 qu'ils ouvrent le code
'''''''''''''''
}}}
== B4. 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/
├── 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**
1. ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
-
'''''''''''''''
}}}
**Exercices**
- Exécutez le programme sur le simulateur.
- Qu'observez-vous ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
- Deux fenêtres sont apparues avec un message uniquement dans la fenêtre `proc0_term0`.
'''''''''''''''
}}}
- Est-ce que les deux fenêtres ont le même comportement vis-à-vis du clavier ?
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
- 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`.
'''''''''''''''
}}}
- 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és 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 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 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
- 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()
├── 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**
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_read()` et `tty_write()` sont présents. La structure décrivant la carte des registres du `TTY` est déclaré 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 `hcpu.S` ? Rappeler, pourquoi avoir mis `.globl clock`
{{{#!protected ------------------------------------------------------------------------------------
'''''''''''''''
* La fonction qui lit ce registre est nécessairement en assembleur, elle est donc mise dans hcpu.S.
* `.globl clock` permet de faire en sorte que la fonction soit visible par les autres fichiers C.
'''''''''''''''
}}}
**Exercices**
- Comme d'habitude, exécutez le code et observez.
- Ouvrez le fichier `kernel.x.s` et regarder 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 fonction. Le code de boot a besoin de `kinit()` et on l'obtiens avec la macro `la`.
'''''''''''''''
}}}
- 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.
__É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
**kinit.h**
{{{#!c
#include
#include
char hello[] = "Hello World!\n\n";
char end[] = "\nend!\n";
void kinit (void)
{
tty_write (0, hello, sizeof (hello) ); // print hello string
guess();
tty_write (0, end, sizeof (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
#define msg(s) tty_write(0,s,sizeof(s))
#define get(c) tty_read(0,&c,1)
void guess (void)
{
char c;
int num;
int random;
do {
random = clock() % 10; // only one digit
do {
do {
msg("Donnez un nombre : ");
get(c);
} while ( (c < '0') && (c > '9') );
num = c - '0';
if (num < random)
msg(" —> trop petit\n");
else if (num > random)
msg(" —> trop grand\n");
} while (random != num);
msg("\nGagné\n");
msg("\nOn recommence [Y]/N ? ");
get(c);
msg("\n\n");
} while (c != 'N');
}
}}}
}}}