{{{#!protected **[https://www-soc.lip6.fr/trac/archi-l3s5/wiki/AS5-TME10?action=edit EDIT]** }}} [[PageOutline]] Cette page décrit la séance complète : partie TD et partie TP. Elle commence par la partie TD avec des questions ou des exercices à faire sur papier, réparties dans 5 sections. Certaines questions de sections différentes sont semblables, c'est normal. Puis, dans la partie TP, il y a des questions sur le code avec quelques exercices de codage simples à écrire et à tester sur le prototype. La partie TP est découpée en 4 é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:AS5-TME9 Séance de TME sur le démarrage du prototype] : ''obligatoire'' * [htdocs:cours/AS5-10-2p.pdf Cours sur l'exécution d'une application en mode user] : ''obligatoire'' * [htdocs:cours/doc_MIPS32.pdf Document sur l'assembleur du MIPS et la convention d'appel des fonctions] : ''recommandé'' * [wiki:Doc-MIPS-Archi-Asm-kernel Documentation sur le mode kernel du MIPS32] : ''obligatoire'' **Récupération du code du TP** * Téléchargez **[htdocs:files/tp2.tgz l'archive code du tp2]** 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 tp2.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 (tp1 et tp2): {{{#!bash 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 └── tp2 ├── 1_klibc ├── 2_appk ├── 3_syscalls ├── 4_libc └── Makefile }}} **Objectif de la séance** Cette séance illustre le [htdocs:cours/AS5-10-2p.pdf cours2]. Les applications de l'utilisateur s'exécutent en mode user. Dans la séance précédente, nous avons vu que les registres de commande des contrôleurs de périphériques sont placés dans l'espace d'adressage du processeur. Les adresses de ces registres ont été placées dans la partie de l'espace d'adressage interdite en mode user. Ainsi, une application n'a pas un accès direct aux périphériques, elle doit utiliser des appels système (avec l'instruction syscall) pour demander au noyau du système d'exploitation. C'est ce que nous allons voir. Le code est désormais découpé en 4 couches logicielles : 1. `1_klibc` le code de boot (utilisé seulement au démarrage); 2. `2_appk` le noyau du système d'exploitation, ici pour l'essentiel, la fonction d'initialisation `kinit()` et le gestionnaire des appels systèmes; 3. `3_syscalls` la bibliothèque de fonctions standards (libc); 4. `4_libc` l'application. == = A. Travaux dirigés == A1. Les modes d'exécution du MIPS Dans cette section, nous allons nous intéresser à ce que propose le processeur MIPS concernant les modes d'exécution. Ce sont des questions portant sur l'usage des modes en général et le comportement du MIPS vis-à-vis de ces modes. Dans la section A3, nous verrons le code de gestion des changements de mode; Le MIPS propose deux modes d'exécution. **Questions** 1. Le MIPS propose deux modes d'exécution, rappelez quels sont ces deux modes et à quoi ils servent? (''Nous l'avons dit dans le descriptif de la séance''). {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Il y a le mode kernel et le mode user. - Le mode kernel est utilisé par le noyau alors que le mode user est utilisé par l'application. ''''''''''''''' }}} 1. Commencez par rappeler ce qu'est l'espace d'adressage du MIPS, puis dîtes ce que veut dire qu'une adresse X mappée en mémoire, et enfin dîtes si une adresse X mappée en mémoire est toujours accessible (en lecture ou en écriture) quelque soit le mode d'exécution du MIPS. {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - L'espace d'adressage du MIPS, c'est l'ensemble des adresses que peut produire le MIPS - On dit qu'une adresse est mappée en mémoire, s'il y a bien une case mémoire pour cette adresse. - Non X n'est pas toujours accessible, si X est < `0x80000000` elle est bien accessible quelque-soit le mode d'exécution du MIPS, mais si X >= `0x80000000` alors X n'est accessible que si le MIPS est en mode kernel. ''''''''''''''' }}} 1. Le MIPS propose des registres à usage général (GPR ''General Purpose Register'') pour les calculs ($0 à $31). Le MIPS propose un deuxième banc de registres à l'usage du système d'exploitation, ce sont les registres système. Comment sont-ils numérotés? Chaque registre porte un nom correspondant à son usage, quels sont ceux que vous connaissez, donner leur nom, leur numéro et leur rôle? Peut-on faire des calculs avec des registres? Quelles sont les instructions qui permettent de les manipuler? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Les registres système sont numérotés de $0 à $31, comme les registres GPR, ce qui peut induire une certaine confusion - Nous avons vu 6 || `cr_sr` || `$12` || contient essentiellement le mode d'exécution du MIPS et le bit d'autorisation des interruptions || `cr_cause` || `$13` || contient la cause d'appel du noyau || `cr_epc` || `$14` || contient l'adresse de l'instruction ayant provoqué l'appel du noyau ou l'adresse de l'instruction suivante || `cr_bar` || `$8 ` || contient l'adresse mal formée si la cause est une exception due à un accès non aligné (p.ex. lw a une adresse non multiple de 4) || `cr_count` || `$9 ` || contient le nombre de cycles depuis le démarrage du MIPS || `cr_procid` || `$15` || contient le numéro du processeur (utile pour les architectures multicores) - non, il n'est pas possible de faire des calculs sur ces registres. - On peut juste les lire et les écrire en utilisant les instructions `mtc0` et `mfc0` ''''''''''''''' }}} 1. Le registre status est composé de plusieurs champs de bits qui ont chacun une fonction spécifique. Décrivez le contenu du registre status et le rôle des bits de l'octet 0(seulement ceux vu en cours). {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' || 0|| IE ||Interrupt Enable||0 → interruptions masquées\\1 → interruptions autorisées || 1|| EXL ||EXception Level ||1 → MIPS en mode exception\\à l'entrée dans le kernel, le MIPS est en mode kernel, interruptions masquées || 2|| ERL ||ERror Level ||0 → interruptions masquées\\1 → interruptions autorisées || 4|| UM ||User Mode ||0 → interruptions masquées\\1 → interruptions autorisées ''''''''''''''' }}} 1. Le registre cause est contient la cause d'appel du kernel. Dites à quel endroit est stockée cette cause et donnez la signification des codes 0, 4 et 8 {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Le champ `XCODE` qui contient le code de la cause d'entrée dans le noyau est codé sur 4 bits entre les bits 2 et 5. - Les valeurs les plus importantes sont: || 0000,,b,, || interruption || un contrôleur de périphérique à lever un signal IRQ || 0100,,b,, || ADEL || lecture non-alignée (p. ex. `lw` a une adresse impaire) || 1000,,b,, || syscall || exécution de l'instruction `syscall` ''''''''''''''' }}} 1. Le registre `EPC` est un registre 32 bits qui contient une adresse. Vous devriez l'avoir décrit dans la question 2, mais expliquez pourquoi ce doit être l'adresse de l'instruction qui provoque une exception qui doit être stockée dans `EPC`? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Une exception, c'est une erreur du programme, telle qu'une division par 0, une lecture non alignée ou une instruction illégale. Il est important que le gestionnaire d'exception sache quelle est l'instruction fautive. C'est pour cette raison que EPC contient l'adresse de l'instruction fautive. Le gestionnaire pourra lire l'instruction et éventuellement corriger le problème. - A titre indicatif, ce n'est pas question, mais pour les syscall, c'est aussi l'adresse de l'instruction syscall, or pour le retour du syscall, on souhaite aller à l'instruction suivante. Il faut donc incrémenter la valeur de `EPC` de 4 (les instructions font 4 octets) pour connaître l'adresse de retour. ''''''''''''''' }}} 1. Nous avons vu trois instructions ne sont pas utilisable lorsque le MIPS est en mode kernel, lesquelles? Que font-elles? Est-ce que `syscall` peut-être utilisée en mode user? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Les trois instructions sont || `mtc0 $GPR, $C0` || `M`ove `T`o `C`oprocessor `0` || `$GPR` → COPRO_0(`$C0`) || `mfc0 $GPR, $C0` || `M`ove `F`rom `C`oprocessor `0` || `$GPR` ← COPRO_0(`$C0`) || `eret` || `E`xpection `RET`urn || `PC` ← `EPC` ; `c0_sr.EXL` ← `0` - Bien sûr que `syscall` peut être utilisé en mode user, puisque c'est comme ça qu'on entre dans le kernel. ''''''''''''''' }}} 1. Quelle est l'adresse d'entrée dans le noyau? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - C'est `0x80000180`i. Il n'y a qu'une adresse pour toutes les causes syscall, exception et interruption. - En fait, on peut considérer que `0xBFC00000` permet aussi d'entrer dans le noyau après un reset. ''''''''''''''' }}} 1. Que se passe-t-il quand le MIPS entre dans le noyau, après l'exécution de l'instruction `syscall`? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - L'instruction `syscall` induit beaucoup d'opérations élémentaires: - `EPC` ← `PC` (adresse de syscall) - `c0_SR.EXL` ← `1` (ainsi les bits `c0_SR.UM` et `c0_SR.IE` ne sont plus utilisés) - `c0_cause.XCODE` ← `8` - `PC` ← `0x80000180` ''''''''''''''' }}} 1. Quelle instruction utilise-t-on pour sortir du noyau et entrer dans l'application ? Dîtes précisément ce que fait cette instruction dans le MIPS? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - C'est l'instruction `eret` qui permet de sortir du noyau. - `PC` ← `EPC` - `c0_SR.EXL` ← `0` (ainsi les bits `c0_SR.UM` et `c0_SR.IE` sont à nouveau utilisés) ''''''''''''''' }}} == A2. Langage C pour la programmation système La programmation en C, vous connaissez, mais quand on programme pour le noyau, c'est un peu différent. Il y a des éléments de syntaxe ou des besoins spécifiques. **Questions** 1. En assembleur, vous utilisez les sections prédéfinies `.data` et `.text` pour placer respectivement les data et le code ou alors vous pouvez créer vos propres sections avec la directive `.section` (nous avons utilisé cette possibilité pour la section `.boot`). Il est aussi possible d'imposer ou de créer des sections en langage C avec le mot clé `__attribute__`. Ce mot clé du C permet de demander certains comportements au compilateur. Il y a en a beaucoup (si cela vous intéresse vous pouvez regarder dans la [https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html doc de GCC sur les attributs]. En cours, nous avons vu un attribut permettant de désigner ou créer une section dans laquelle est mise la fonction concernée. Quelle était la syntaxe de cet attribut (regardez sur le slide 37). {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - `__attribute__ ((section (".crt0")))`. Remarquez la syntaxe un peu curieuse avec les doubles underscore et les doubles parenthèses. ''''''''''''''' }}} 1. En C, vous savez que les variables globales sont toujours initialisées, soit explicitement dans le programme lui-même, soit implicitement à la valeur `0`. Les variables globales initialisées sont placées dans la section `.data` (ou plutôt dans l'une des sections `data` : `.data`, `.sdata`, `.rodata`, etc.) et elles sont présentes dans le fichier objet (`.o`) produit pas le compilateur. En revanche, les variables globales non explicitement initialisées ne sont pas présentes dans le fichier objet. Ces dernières sont placées dans un segment de la famille `.bss`. C'est grâce au fichier ldscript que nous pouvons mapper l'ensemble des segments en mémoire.\\Pour pouvoir initialiser à `0` les segments `bss` par programme, il nous faut connaître l'adresse de début et de fin en mémoire. Le code ci-dessous est le fichier ldscript du kernel `kernel.ld` (nous avons retier les commentaires pour la circonstance.\\Expliquez ce que font les lignes 11, 12 et 15. {{{#!java 1 SECTIONS 2 { 3 .boot : { 4 *(.boot) 5 } > boot_region 6 .ktext : { 7 *(.text*) 8 } > ktext_region 9 .kdata : { 10 *(.*data*) 11 . = ALIGN(4); 12 __bss_origin = .; 13 *(.*bss*) 14 . = ALIGN(4); 15 __bss_end = .; 16 } > kdata_region 17 } }}} {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - La ligne 11 contient `. = ALIGN(4)`, c'est équivalent à la directive `.align 4` de l'assembleur. Cela permet de déplacer le pointeur de remplissage de la section de sortie courante (c'est-à-dire ici `.kdata`) frontière de 2^4^ octets (une adresse multiple de 16). Cette contrainte est liée aux caches que nous ne verrons pas ici. - La ligne 12 permet de créer la variable de ldscript `__bss_origin` et de l'initialiser à l'adresse courante, ce sera donc l'adresse de début de la zone `bss`. - La ligne 15 permet de créer la variable `__bss_end` qui sera l'adresse de fin de la zone `bss` (en fait c'est la première adresse qui suit juste `bss`. ''''''''''''''' }}} 1. Nous connaissons les adresses des registres de périphériques. Ces adresses sont déclarées dans le fichier ldscript `kernel.ld`. Ci-après, nous avons la déclaration de la variable de ldscript `__tty_regs_map`. Cette variable est aussi utilisable dans les programmes C, mais pour être utilisable par le compilateur C, il est nécessaire de lui dire quel type de variable c'est. Est-ce une adresse d'entier? Est-ce une adresse de tableau d'entiers? Ou encore, est-ce une structure?\\Dans le fichier `kernel.ld`: {{{#!c __tty_regs_map = 0xd0200000 ; /* tty's registers map, described in devices.h */ }}} Dans le fichier `harch.c` : {{{#!c 12 struct tty_s { 13 int write; // tty's output address 14 int status; // tty's status address something to read if not null) 15 int read; // tty's input address 16 int unused; // unused address 17 }; 18 19 extern volatile struct tty_s __tty_regs_map[NTTYS]; }}} À quoi servent les mots clés `extern` et `volatile` ?\\Si NTTYS est une macro dont la valeur est 2, quelle est l'adresse en mémoire `__tty_regs_map[1].read` ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - `extern` : informe le compilateur que la variable définie existe ailleurs. Grâce à son type, le compilateur sait s'en servir. - `volatile` : informe le compilateur que la variable peut changer de valeur toute seule et que donc il doit toujours accéder en mémoire à chaque fois que le programme le demande. Il ne peut donc pas optimiser les accès mémoire en utilisant les registres. - `__tty_regs_map` est un tableau à 2 cases (puisque `NTTYS`=2). Chaque case est une structure de 4 entiers, donc `0x10` octets. `read` est le troisième champ, c'est le troisième entier de la structure, donc en `+8` par rapport au début.\\En conséquence `__tty_regs_map[1].read` est en `0xd0200018` ''''''''''''''' }}} 1. Certaines parties du noyau sont en assembleur. Il y a au moins les toutes premières instructions du code de boot (démarrage de l'ordinateur) et l'entrée dans le noyau après l'exécution d'un syscall. Dans ce dernier cas, le gestionnaire de syscall écrit en assembleur a besoin d'appeler une fonction écrite en langage C. Le gestionnaire de syscall trouve l'adresse de la fonction C qu'il doit appeler puis il place cette adresse dans un registre, par exemple `$2`. Il suffit qu'il exécute l'instruction `jal $2` pour appeler la fonction. Que doivent contenir les registres `$4` à `$7` et comment doit-être la pile? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - C'est un appel de fonction, il faut donc respecter la convention d'appel des fonctions - Les registres `$4`à `$7` contiennent les arguments de la fonction - Le pointeur de pile doit pointer sur la case réservée pour le premier argument et les cases suivantes sont réservées arguments suivants. - Ce n'est pas rappelé ici, mais il y a **au plus** 4 arguments (entier ou pointeur) pour tous les syscalls. En conséquence, le pointeur de pile pointe au début d'une zone vide de 4 entiers. ''''''''''''''' }}} 1. Vous avez appris à écrire des programmes assembleur, mais parfois il est plus simple, voire il est nécessaire de mélanger le code C et le code assembleur. Dans l'exemple ci-dessous, nous voyons comment la fonction `kinit()` procède pour entrer dans la fonction placée à l'adresse `__crt0` définie dans le fichier `kernel.ld`. Remarquez la syntaxe, ici `volatile` permet de dire au compilateur d'insérer le code tel que sans le modifier. Notez aussi l'absence de `,` entre les chaînes de caractères. Le premier argument de `__asm__` est une chaîne de caractères unique dans laquelle les instructions sont séparées par de `\n`. Il peut y avoir d'autres arguments, nous ne les verrons pas.\\Dans quelle section se trouve l'adresse `__crt0`? Combien vaut-elle? Est-ce que cette valeur est imposée par le processeur MIPS comme l'adresse de boot ou d'entrée dans le kernel? Quelle fonction est à cette adresse? Pourquoi doit-on écrire ce code en assembleur? {{{#!c 9 void kinit (void) 10 { 11 kprintf (0, banner); 12 13 // put bss sections to zero. bss contains uninitialised global variables 14 extern int __bss_origin; // first int of bss section 15 extern int __bss_end; // first int of above bss section 16 for (int *a = &__bss_origin; a != &__bss_end; *a++ = 0); 17 18 // this code allows to exit the kernel to go to user code 19 __asm__ volatile ( "la $26, __text_origin \n" // get first address of user code 20 "mtc0 $26, $14 \n" // put it in c0_EPC 21 "li $26, 0b00010010 \n" // next status [UM,0,ERL,EXL,IE] 22 "mtc0 $26, $12 \n" // UM <- 1, IE <- 0, EXL <- 1 23 "la $29, __data_end \n" // define new user stack pointer 24 "eret \n"); // j EPC and EXL <- 0 25 } }}} {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - L'adresse `__crt0` est la première adresse de la section `text` dans laquelle se trouve le code de l'application. - Elle vaut `0X7F400000`. - Cette adresse n'est pas imposée par le MIPS. C'est le choix des architectes de SoC. La seule condition est que cette adresse soit dans la partie accessible en mode user. - A cette adresse, on place la fonction `__start()`. - On est obligé d'écrire ce code en assembleur parce que la manière de changer de mode (de `kernel` à `user`) est propre à chaque processeur. Il n'y a aucun moyen de le faire en C. ''''''''''''''' }}} 1. Dans le code C de la question précédente, à quoi servent les lignes 12 à 16? Pourquoi faire des déclarations `extern`? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Ces lignes servent à mettre à 0 la zone des variables globales non initialisées explicitement par le programme. - Les déclarations externes permettent d'informer le compilateur que les adresses `__bss_orgin` et `__bss_end` existent ailleurs. De fait, elles sont définies dans le fichier `kernel.ld`. ''''''''''''''' }}} == A3. Passage entre les modes kernel et user Le noyau et l'application sont deux exécutables compilés indépendamment mais pas qui ne sont pas indépendants. Vous savez déjà que l'application appelle les services du noyau avec l'instruction syscall, mais comment ça se passe vraiment depuis le code C? Certaines questions sont proches de celles déjà posées, c'est volontaire. **Questions** 1. Comment imposer le placement d'adresse d'une fonction ou d'une variable en mémoire? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - C'est l'éditeur de lien qui est en charge du placement en mémoire du code et des données, et c'est dans le fichier ldscript `kernel.ld` ou `user.ld` que le programmeur peut imposer ses choix. - Pour placer une fonction à une place, la méthode que vous avez vu consiste - à créer une section grâce à la directive `.section` en assembleur ou à la directive `__attribute__((section()))` en C - puis à positionner la section créée dans la description des `SECTIONS` du ldscript. ''''''''''''''' }}} 1. Dans la question **A2.5**, nous avons vu comment la fonction `kinit` appelle la fonction `__start()` grâce à un bout de code en assembleur. Nous allons voir maintenant quelles sont les conditions de cet appel. Dans le code de la question **A2.5**, `$26` est un registre de travail pour le kernel. Quels sont les autres registres modifiés? Expliquez pour chacun la valeur affectée. {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Il y a 3 registres affectés, dans l'ordre : - Le registre système `$14` nommé `c0_EPC`, il reçoit l'adresse `__crt0`, c'est-à-dire l'adresse de la fonction `__start()`. - Le registre système `$12` nommé `c0_sr`, il reçoit la valeur `0x12`, donc les bits `UM`, `EXL` et `IE` prennent respectivement les valeurs `1`, `1` et `0` - UM = 1 et IE = 0, signifie que l'on est normalement en mode `user` interruptions masquées, **mais** comme `EXL` est à 1, on reste en mode `kernel` interruptions masquées. L'exécution de l'instruction `eret` mettra `EXL` à `0` pour rendre le bit `UM` actif et passer en mode `user`. - Le registre GPR `$29` reçoit l'adresse de la première adresse après la section `.data`. C'est le haut de la pile. ''''''''''''''' }}} 1. Que faire avant et après l'exécution de la fonction `main()` du point de vue de l'initialisation? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Comme dans la fonction `kinit()`, il faut explicitement initialiser les variables globales non initialisées dans le programme C. ''''''''''''''' }}} 1. Nous avons vu que le noyau est sollicité par des événements, quels sont-ils?Quel est le comportement exact de l'instruction `syscall`? Comment le noyau fait-il pour connaître la cause de son appel? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Il y en a 3 (si on excepte le signal `reset` qui redémarre tout le système: 1. Les appels système donc l'exécution de l'instruction `syscall`. 1. Les exceptions donc les "erreur" de programmation (division par 0, adressage mémoire incorrect, etc.). 1. Les interruptions qui sont des demandes d'intervention provenant des périphériques. - L'instruction `syscall`sysc ''''''''''''''' }}} 1. `$26` et `$27` sont deux registres temporaires que le noyau se réserve pour faire des calculs sans qu'il ait besoin de les sauvegarder dans la pile. Ce ne sont pas des registres système comme `c0_sr` ou `c0_epc`. En effet, l'usage des registres `$26` et `$27` par l'utilisateur ne provoque pas d'exception du MIPS. Toutefois si le noyau est appelé alors il modifie ces registres et donc l'utilisateur perd leur valeur. Le code assembleur ci-après contient les instructions exécutées par le noyau, quelle que soit la cause. Les commentaires présents dans le code ont été volontairement retirés (ils sont dans les fichiers du TP). La section `.kentry` est placée à l'adresse `0x80000000` par l'éditeur de lien. La directive `.org` (ligne 16) permet de déplacer le pointeur de remplissage de la section courante du nombre d'octets donnés en argument, ici `0x180`. Pouvez-vous dire pourquoi ? Expliquer les lignes 25 à 28.\\ \\**`kernel/hcpu.S`** {{{#!c 15 .section .kentry,"ax" 16 .org 0x180 22 23 kentry: 24 25 mfc0 $26, $13 26 andi $26, $26, 0x3C 27 li $27, 0x20 28 bne $26, $27, not_syscall }}} {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - La section `kentry` est placée à l'adresse `0x80000000` or l'entrée du noyau est `0x80000180`, il faut donc déplacer le pointeur de remplissage de la section `ktentry` de `0x180`. Remarquez qu'on aurait pu utiliser une directive `.space 0x180`. - Commentaire du code - Ligne 25 : `$26` **←** `c0_cause`\\⟶ donc le registre GPR réservé au kernel prend la valeur du registre de cause. - Ligne 26 : `$26` **←** `$26 & 0b00111100`\\⟶ C'est un masque qui permet de ne conserver que les 4 bits du champ `XCODE`. - Ligne 27 : `$27` **←** `0b00100000`\\⟶ On initialise le registre GPR réservé au kernel $27 avec la valeur attendue dans $26 s'il s'agit d'une cause `syscall`. - Ligne 28 : si `$26` ≠ `$27` goto not_syscall\\⟶ Si ce n'est pas un syscall, on va plus loin, sinon on continue en séquence. ''''''''''''''' }}} 1. Le gestionnaire de syscall est la partie du code qui gère le comportement du noyau lors de l'exécution de l'instruction `syscall`. C'est un code en assembleur présent dans le fichier `kernel/hcpu.S` que nous allons observer. Pour vous aider dans la compréhension de ce code, vous devez imaginer que l'instruction `syscall` est un peu comme un appel de fonction. Ce code utilise un tableau de pointeurs de fonctions nommé `syscall_vector` définit dans le fichier `kernel/ksyscalls.c`. Les lignes `36` à `43` sont chargées d'allouer dans la pile. Dessinez l'état de la pile après l'exécution de ces instructions. Que fait l'instruction lige `44` et quelle conséquence cela a? Que font les lignes `46` à `51`? Et enfin que font les lignes `53` à `59`\\ \\**`common/syscalls.h`** {{{#!c 1 #define SYSCALL_EXIT 0 2 #define SYSCALL_TTY_WRITE 1 3 #define SYSCALL_TTY_READ 2 4 #define SYSCALL_CLOCK 3 5 #define SYSCALL_NR 32 }}} **`kernel/ksyscalls.c`** {{{#!c void *syscall_vector[] = { [0 ... SYSCALL_NR - 1] = unknown_syscall, [SYSCALL_EXIT] = exit, [SYSCALL_TTY_READ] = tty_read, [SYSCALL_TTY_WRITE] = tty_write, [SYSCALL_CLOCK] = clock, }; }}} **`kernel/hcpu.S`** {{{#!c 34 ksyscall: 35 36 addiu $29, $29, -8*4 37 mfc0 $27, $14 38 mfc0 $26, $12 39 addiu $27, $27, 4 40 sw $31, 7*4($29) 41 sw $27, 6*4($29) 42 sw $26, 5*4($29) 43 sw $2, 4*4($29) 44 mtc0 $0, $12 45 46 la $26, syscall_vector 47 andi $2, $2, SYSCALL_NR-1 48 sll $2, $2, 2 49 addu $2, $26, $2 50 lw $2, ($2) 51 jalr $2 52 53 lw $26, 5*4($29) 54 lw $27, 6*4($29) 55 lw $31, 7*4($29) 56 mtc0 $26, $12 57 mtc0 $27, $14 58 addiu $29, $29, 8*4 59 eret }}} {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - État de la pile après l'exécution des lignes 36 à 43 {{{ +----------+ | $31 | Nous allons exécuter jal un peu plus et perdre $31, il faut le sauver +----------+ | C0_EPC | C'est l'adresse de retour du syscall +----------+ | C0_SR | le registre status est modifié plus loin, il faut le sauver pour le restaurer +----------+ | $2 | C'est le numéro de syscall qui pourra être accédé par la fonction appelé en 5e argument +----------+ | | place réservée pour le 4e argument actuellement dans $7 +----------+ | | place réservée pour le 3e argument actuellement dans $6 +----------+ | | place réservée pour le 2e argument actuellement dans $5 +----------+ $29 → | | place réservée pour le 1e argument actuellement dans $4 +----------+ }}} - L'instruction ligne 44 met `0` dans le registre `c0_sr`. Ce qui a pour conséquence de mettre à `0` les bits `UM`, `EXL` et `IE`. On est donc en mode kernel avec interruptions masquées.\\ \\''Notez qu'interdire les interruptions pendant l'exécution des syscall est contraignant. Pour le moment, ce n'est pas important puisque nous ne traitons pas encore les interruptions, mais lorsque nous les traiterons, elles seront masquées. En conséquence, il sera interdit aux fonctions qui traitent les appels système d'exécuter des attentes longues (comme une boucle qui attend le changement d'état d'un registre de périphérique) car sinon, le noyau sera bloqué (plus rien ne bouge).'' - Commentaire du code - Ligne 46 : `$26` **←** l'adresse du tableau syscall_vector\\⟶ On s'apprête à y faire un accès indexé par le registre `$2` - Ligne 47 : `$2` **←** `$2 & 0x1F`\\⟶ pour éviter de sortir du tableau si l'utilisateur à mis n'importe quoi dans `$2` - Ligne 48 : `$2` **←** `$2 * 4`\\⟶ Les cases du tableau sont des pointeurs et font 4 octets - Ligne 49 : `$2` **←** `$26 + $2`\\⟶ - Ligne 50 : **←** \\⟶ - Ligne 51 : **←** \\⟶ ''''''''''''''' }}} == A4. Génération du code exécutable Pour simuler le logiciel, il faut produire deux exécutables. Nous utilisons, ici, un Makefile hiérarchique et des règles explicites. Cela sort du cadre de l'architecture, mais vous avez besoin de ce savoir-faire pour comprendre le code, alors allons-y. **Questions** 1. Rappelez là quoi sert un Makefile? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - Le rôle principal d'un Makefile est de décrire le mode d'emploi pour construire un fichier dit **`cible`** à partir d'un ou plusieurs fichiers **`source`** dits de dépendance en utilisant des commandes du `shell`. Ce rôle pourrait tout aussi bien être occupé par un script `shell` et d'ailleurs dans le premier TP nous avons vu un usage du Makefile dans lequel nous avions rassemblé plusieurs scripts `shell` sous forme de règles. - Le second rôle d'un Makefile est de permettre la reconstruction efficace du fichier **`cible`** lorsqu'un seul seul fichier **`source`** change. Pour ce rôle, le Makefile exprime toutes les étapes de constructions de la **`cible`** finale et des **`cibles`** intermédiaires sous forme d'un arbre dont les feuilles sont les fichiers **`sources`**. ''''''''''''''' }}} 1. Vous n'allez pas à avoir à écrire un Makefile complètement, toutefois vous allez devoir les modifier en ajoutant des règles. Nous avons vu brièvement la syntaxe utilisée dans les Makefiles de ce TP au cours n°1. Les lignes qui suivent sont des extraits du premier Makefile. Quelles sont la cible finale, les cibles intermédiaires et les sources? A quoi servent les variables automatiques de make? Dans ces deux règles, donnez-en la valeur. {{{#!make kernel.x : kernel.ld obj/hcpu.o obj/kinit.o obj/klibc.o obj/harch.o $(LD) -o $@ -T $^ $(OD) -D $@ > $@.s obj/hcpu.o : hcpu.S $(CC) -o $@ $(CFLAGS) $< $(OD) -D $@ > $@.s }}} {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} 1.Dans le TP, à partir de la deuxième étape, nous avons trois répertoire de sources {{{ 4_libc/ ├── Makefile : Makefile racine qui invoque les Makefiles des sous-répertoires et qui exécute ├── common ────────── répertoire des fichiers commun kernel / user ├── kernel ────────── Répertoire des fichiers composant le kernel │   └── Makefile : description des actions possibles sur le code kernel : compilation et nettoyage ├── uapp ──────────── Répertoire des fichiers de l'application user seule │   └── Makefile : description des actions possibles sur le code user : compilation et nettoyage └── ulib ──────────── Répertoire des fichiers des bibliothèques système liés avec l'application user └── Makefile : description des actions possibles sur le code user : compilation et nettoyage }}} {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} {{{#!comment Je retire cette partie, elle est trop hors sujet. == A5. Libc Cette partie ne concerne pas vraiment le noyau, mais il y a peut-être des choses que vous ignorez sur le C, ou certaines opérations, qu'il est nécessaire de connaître pour ce petit système. Cela n'a pas été présenté en cours, alors les questions sont précédées d'une présentation du problème et sa solution. **Questions** 1. fonction C à nombre d'arguments variables `fprintf`? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} 1. génération de nombres pseudoaléatoires `rand`? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} 1. traduction d'une chaîne de caractère en nombre `atoi`? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} }}} ------------------------------------------------------------------------------------------------------------------------------------ = B. Travaux pratiques 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. Le code se trouve dans `$AS5/tp2/`, ouvrez un terminal et allez-y. Dans ce répertoire, vous avez 4 sous-répertoires et un Makefile. Le fichier `$AS5/tp2/Makefile` permet de faire le ménage en appelant les Makefiles des sous-répertoires avec la cible `clean`. == B1. Ajout d'une bibliothèque de fonctions standards pour le kernel (klic) Le noyau gère les ressources matérielles et logicielles utilisées par les applications. Il a besoin de fonctions standards pour réaliser des opérations de base, telles qu'une fonction `print` ou une fonction `rand`. Ces fonctions ne sont pas très originales, mais elles recèlent des subtilités que vous ne connaissez peut-être pas encore. En outre, nous allons utiliser un Makefile définissant un graphe de dépendance explicite entre les fichiers cibles et les fichiers sources avec des règles de construction. **Objectifs** - Ajouter une bibliothèque de fonctions standards - Utiliser un Makefile avec des règles explicites **Fichiers** {{{ 1_klibc/ ├── kinit.c : fichier contenant la fonction de démarrage du noyau ├── harch.h : API du code dépendant de l'architecture ├── harch.c : code dépendant de l'architecture du SoC ├── 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 ├── klibc.h : API de la klibc ├── klibc.c : fonctions standards utilisées par les modules du noyau └── Makefile : description des actions possibles sur le code : compilation, exécution, nettoyage, etc. }}} **Questions** 1. Ouvrez le fichier Makefile, En ouvrant tous les fichiers dessiner le graphe de dépendance de `kernel.x` vis-à-vis de ses sources? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' kernel.x : kernel.ld obj/hcpu.o obj/kinit.o obj/klibc.o obj/harch.o obj/hcpu.o : hcpu.S obj/kinit.o : kinit.c klibc.h obj/klibc.o : klibc.c klibc.h harch.h obj/harch.o : harch.c harch.h ''''''''''''''' }}} 1. ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - ''''''''''''''' }}} **Exercices * Ajout de la fonction cpuid() qui lit le registre $15 du coprocesseur système. == B2. Programme utilisateur mais exécuté en mode kernel Nous allons désormais avoir deux exécutables: le noyau et l'application. Dans cette étape, nous allons voir comment le noyau fait pour appeler l'application, alors que celle-ci n'est pas compilée en même temps que le noyau. **Objectifs** **Fichiers** {{{ 2_appk/ ├── Makefile : Makefile racine qui invoque les Makefiles des sous-répertoires et qui exécute ├── kernel ────────── Répertoire des fichiers composant le kernel │   ├── kinit.c : fichier contenant la fonction de démarrage du noyau │   ├── harch.h : API du code dépendant de l'architecture │   ├── harch.c : code dépendant de l'architecture du SoC │   ├── hcpu.h : prototype de la fonction clock() │   ├── hcpu.S : code dépendant du cpu matériel en assembleur │   ├── klibc.h : API de la klibc │   ├── klibc.c : fonctions standards utilisées par les modules du noyau │   ├── kernel.ld : ldscript décrivant l'espace d'adressage pour l'édition de liens du kernel │   └── Makefile : description des actions possibles sur le code kernel : compilation et nettoyage └── user ──────────── Répertoire des fichiers composant l'application user ├── crt0.c : fonctions d'interface entre kernel et user, pour le moment : crt0() ├── main.c : fonction principale de l'application ├── user.ld : ldscript décrivant l'espace d'adressage pour l'édition de liens du user └── Makefile : description des actions possibles sur le code user : compilation et nettoyage }}} **Questions** 1. Question ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - réponse ''''''''''''''' }}} == B3. Programme utilisateur utilisé en mode user mais sans libc Le programme utilisateur doit absolument s'exécuter en mode user et il doit passer par des appels système pour accéder aux services du noyau. Les services, ici, sont limités (l'accès au TTY, exit et clock), il n'empêche que pour gérer ces appels, il faut l'analyseur des causes d'appels à l'entrée du noyau et un gestionnaire de `syscall`. Il faut aussi le gestionnaire d'exceptions, parce que s'il y a une erreur de programmation, le noyau doit afficher quelque chose pour aider le programmeur. **Objectifs** **Fichiers** {{{ 3_syscalls/ ├── Makefile : Makefile racine qui invoque les Makefiles des sous-répertoires et qui exécute ├── common ────────── répertoire des fichiers commun kernel / user │   └── syscalls.h : API la fonction syscall et des codes de syscalls ├── kernel ────────── Répertoire des fichiers composant le kernel │   ├── kinit.c : fichier contenant la fonction de démarrage du noyau │   ├── harch.h : API du code dépendant de l'architecture │   ├── harch.c : code dépendant de l'architecture du SoC │   ├── hcpu.h : prototype de la fonction clock() │   ├── hcpu.S : code dépendant du cpu matériel en assembleur │   ├── klibc.h : API de la klibc │   ├── klibc.c : fonctions standards utilisées par les modules du noyau │   ├── kpanic.h : déclaration du tableau de dump des registres en cas d'exception │   ├── kpanic.c : fonction d'affichage des registres avant l'arrêt du programme │   ├── ksyscalls.c : Vecteurs des syscalls │   ├── kernel.ld : ldscript décrivant l'espace d'adressage pour l'édition de liens du kernel │   └── Makefile : description des actions possibles sur le code kernel : compilation et nettoyage └── user ──────────── Répertoire des fichiers composant l'application user ├── crt0.c : fonctions d'interface entre kernel et user, pour le moment : crt0() ├── main.c : fonction principale de l'application ├── user.ld : ldscript décrivant l'espace d'adressage pour l'édition de liens du user └── Makefile : description des actions possibles sur le code user : compilation et nettoyage }}} **Questions** 1. Question ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - réponse ''''''''''''''' }}} == B4. Accès aux registres de contrôle des terminaux `TTY` **Objectifs** **Fichiers** {{{ 4_libc/ ├── Makefile : Makefile racine qui invoque les Makefiles des sous-répertoires et qui exécute ├── common ────────── répertoire des fichiers commun kernel / user │   └── syscalls.h : API la fonction syscall et des codes de syscalls ├── kernel ────────── Répertoire des fichiers composant le kernel │   ├── kinit.c : fichier contenant la fonction de démarrage du noyau │   ├── harch.h : API du code dépendant de l'architecture │   ├── harch.c : code dépendant de l'architecture du SoC │   ├── hcpu.h : prototype de la fonction clock() │   ├── hcpu.S : code dépendant du cpu matériel en assembleur │   ├── klibc.h : API de la klibc │   ├── klibc.c : fonctions standards utilisées par les modules du noyau │   ├── kpanic.h : déclaration du tableau de dump des registres en cas d'exception │   ├── kpanic.c : fonction d'affichage des registres avant l'arrêt du programme │   ├── ksyscalls.c : Vecteurs des syscalls │   ├── kernel.ld : ldscript décrivant l'espace d'adressage pour l'édition de liens du kernel │   └── Makefile : description des actions possibles sur le code kernel : compilation et nettoyage ├── uapp ──────────── Répertoire des fichiers de l'application user seule │   ├── main.c : fonction principale de l'application │   └── Makefile : description des actions possibles sur le code user : compilation et nettoyage └── ulib ──────────── Répertoire des fichiers des bibliothèques système liés avec l'application user ├── crt0.c : fonctions d'interface entre kernel et user, pour le moment : crt0() ├── libc.h : API pseudo-POSIX de la bibliothèque C ├── libc.c : code source de la libc ├── main.c : fonction principale de l'application ├── user.ld : ldscript décrivant l'espace d'adressage pour l'édition de liens du user └── Makefile : description des actions possibles sur le code user : compilation et nettoyage }}} **Questions** 1. Question ? {{{#!protected ------------------------------------------------------------------------------------ ''''''''''''''' - réponse '''''''''''''''