wiki:SujetTD4

ALMO TD n°4 - Architecture mono-processeur possédant un bus système et des périphériques

Préambule

Le simulateur MARS utilisé jusqu'à présent permet de simuler, instruction par instruction une application écrite en assembleur. Mais l'architecture matérielle modélisée par MARS ne simule que le processeur MIPS32 et sa mémoire. Elle ne modélise pas les périphériques (écran, clavier, souris, disque, contrôleur réseau, etc.) que l'on trouve généralement dans un PC. Les quelques appels systèmes proposés par MARS (permettant d'afficher ou de lire des caractères sur la console) ne sont pas exécutés par le processeur MIPS32 mais sont exécutés "magiquement" par le processeur de la station de travail sur laquelle s'exécute le simulateur MARS, ce qui ne permet pas vraiment de comprendre les interactions entre le programme s'exécutant sur le processeur et les organes périphériques. Par ailleurs, dans le simulateur MARS, la mémoire répond instantanément (en un cycle d'horloge) aux commandes de lecture et d'écriture émises par le processeur, ce qui ne correspond pas à la réalité.

Dans ce TD4 et dans le TP4 associé, nous nous proposons d'étudier une architecture matérielle plus réaliste, dans laquelle on modélise explicitement les communications entre le processeur, la mémoire et les périphériques. Comme illustré dans la figure ci-contre, cette architecture comporte :

  • un processeur MIPS32, avec ses caches de premier niveau (caches L1) pour les instructions et pour les données,
  • un cache de deuxième niveau (cache L2) permettant d'accéder à la mémoire externe,
  • une mémoire ROM (non inscriptible, et non volatile) contenant le code de démarrage,
  • un périphérique contrôlant un terminal texte (TTY : clavier/écran).
  • un bus permettant la communication entre un unique maître et trois cibles.

Par ailleurs, et à la différence des TP précédents, les appels système seront effectivement exécutés par le processeur MIPS32, grâce à un petit système d'exploitation appelé GIET (Gestionnaire d'Interruptions, Exceptions et trappes). Le code source du GIET peut être obtenu ici.

Dans ce TP, nous allons analyser en détail ce qui se passe lors de l'exécution d'un programme C très simple qui se contente d'afficher le célèbre "Hello World!" sur l'écran du terminal.

1. Contrôleur TTY

Pour communiquer avec le contrôleur TTY, le logiciel s'exécutant sur le processeur doit effectuer des lectures et des écritures à certaines adresses, correspondant à des registres internes du composant TTY (on dit parfois que les registres du périphériques sont "mappés" en mémoire).

A chaque terminal sont associés 4 registres :

  • TTY_WRITE : Pour afficher un caractère ASCII sur l'écran du terminal, le processeur doit écrire un octet (instruction sb) à l'adresse du registre TTY_WRITE.
  • TTY_READ : Pour lire un caractère ASCII saisi au clavier du terminal, le processeur doit lire un octet (instruction lb) à l'adresse du registre TTY_READ.
  • TTY_STATUS : Pour savoir si un caractère est disponible dans le registre TTY_READ, le processeur doit lire un mot de 32 bits dans le registre TTY_STATUS.
    • Si le bit n°0 est non-nul, le registre TTY_READ est non vide.
    • Si le bit n°1 est non-nul, le registre TTY_WRITE est non vide.
  • TTY_CONFIG : Le logiciel peut écrire dans ce registre pour configurer le périphérique, mais cette fonctionnalité n'est pas utilisée ici.

Questions :

  • Que se passe-t-il si le TTY reçoit une commande de lecture destinée au registre TTY_WRITE ?
  • Que se passe-t-il si le TTY reçoit une commande d'écriture destinée au registre TTY_READ ?
  • Que se passe-t-il si le TTY reçoit une commande d'écriture destinée au registre TTY_STATUS ?

On notera que le contrôleur TTY est capable de contrôler plusieurs terminaux. Cela signifie que chaque terminal possède son propre jeu de 4 registres. Comme les adresses de registres sont alignées sur des frontières de mot (c'est-à-dire que les adresses sont multiples de 4 octets), chaque terminal occupe donc une tranche de 16 octets dans l'espace adressable.

2. Segmentation de l'espace adressable

Puisqu'on modélise maintenant les périphériques, et que c'est le processeur MIPS32 qui va exécuter le code des appels système, il faut définir huit segments dans l'espace adressable : La zone "utilisateur" contient les trois segments que vous connaissez déjà, (code, data et stack), mais il faut en plus définir 5 segments contenant le code et les données du système d'exploitation dans la zone "noyau", qui n'est accessible que lorsque le processeur est en mode superviseur.

Dans la zone utilisateur :

  • Le segment seg_code contient le code du programme utilisateur (défini dans le fichier main.c), ainsi que le code des fonctions permettant à un programme utilisateur d'accéder aux périphériques grâce aux différents appels systèmes proposés (et définis dans le fichier stdio.c). Ce segment a pour adresse de base seg_code_base = 0x00400000.
  • Le segment seg_data contient les données globales du programme utilisateur. Il a pour adresse de base seg_data_base = 0x10000000.
  • Le segment seg_stack contient la pile d'exécution du programme. Il a pour adresse de base seg_stack_base = 0x20000000.

Dans la zone noyau :

  • Le segment seg_reset contient le code de démarrage (ou reset) chargé d'initialiser la machine. Il a pour adresse de base seg_reset_base = 0xBFC00000, et le code correspondant est défini dans le fichier reset.s.
  • Le segment seg_kcode contient le code protégé du système d'exploitation. Il a pour adresse de base seg_kcode_base = 0x80000000. Ce code système sera étudié en détail dans le TD7, et contient en particulier le code du GIET (Gestionnaire des Interruptions, Exceptions et Trappes).
  • Le segment seg_kdata contient les structures de données privées du système d'exploitation. Il a pour adresse de base seg_kdata_base = 0x81000000.
  • Le segment seg_kunc contient les données non cachables du système d'exploitation. Il a pour adresse de base seg_kunc_base = 0x82000000.
  • Le segment seg_tty contient les 4 registres adressables du contrôleur TTY. Il a pour adresse de base seg_tty_base = 0x90000000.

Questions :

  • Quelles contraintes doit respecter l'adresse de base, nommée seg_tty_base, du segment correspondant au périphérique TTY ? Quelle est la longueur de ce segment dans l'hypothèse où l'architecture contient 3 terminaux écran/clavier ?
  • Lorsque l'on re-initialise la plateforme matérielle en activant le signal reset, le processeur se branche à une adresse "cablée" prédéfinie, nommée seg_reset_base, où doit être stockée la première instruction du "code de boot" (appelé aussi code de reset). Quelle est cette adresse ? Quelle est la particularité de la mémoire contenant le "code de reset" ?
  • Lorsque le programme essaie d'exécuter une instruction illégale (exception), ou lorsqu'un programme utilisateur veut demander un service au système d'exploitation (trappe), ou encore lorsque survient un événement extérieur (interruption), le processeur saute à une adresse "cablée" prédéfinie qui est le point d'entrée du Gestionnaire d'Interruptions, Exceptions et Trappes (GIET).

Quelle est cette adresse ? Dans quelle type de mémoire (ROM ou RAM) est stocké le segment seg_kcode ? Où sont rangées les structures de données du GIET ?

  • Pourquoi le code appartenant au système d'exploitation et le code du programme utilisateur doivent-ils être stockés dans des segments différents ? Quelle contrainte doivent respecter les adresses seg_code_base, seg_data_base et seg_stack_base ?

3. Application logicielle

Le programme C ci-dessous permet d'afficher la chaîne de caractères "Hello World!". Il s'exécute en mode utilisateur, sous le contrôle du système d'exploitation GIET, qui a été présenté en cours.

#include <stdio.h>

__attribute__((constructor)) void main(void)
{
    char byte;
    char str[] = "\nHello World!\n";

    while (1) {
        tty_puts(str);
        tty_getc(&byte);

        if (byte == 'q') {
            exit(0);
        }
    }
    exit(0);
}

Ce programme exécute une boucle infinie. À chaque itération de la boucle, on affiche sur l'écran la chaîne de caractères "Hello World!" sur le TTY (grâce à l'appel système tty_puts()), puis on lit un caractère tapé au clavier (grâce à l'appel système tty_getc()). On ne sort de la boucle, et du programme, par l'appel système exit() que lorsque le caractère lu est 'q'. Vous trouverez le code source de ces appels système dans le fichier stdio.c.

  • Pourquoi le programme utilisateur doit-il utiliser des appels système pour accéder au TTY ?
  • Sans plonger dans l'analyse détaillée du code du GIET, rappelez les différentes étapes permettant au logiciel d'afficher sur le terminal le message "Hello world!", depuis le démarrage de la machine. On mentionnera précisément tous les branchements effectués par le processeur vers les différents segments mémoire contenant du code exécutable.
  • Rappelez de même ce qui se passe lorsque le processeur entre dans la fonction tty_getc().
  • Où sont définies les adresses de base des 8 segments utilisés dans cette architecture? Quelles sont les rattachements (ROM, RAM ou TTY) de ces 8 segments ? Où sont définies les longueurs de ces segments ?

4. Code de reset

Lors de l'activation du signal nreset, le processeur passe en mode superviseur et se branche à la première instruction du code de boot, chargé d'initialiser la machine. Pour ce TP, le fichier reset.s contenant un code de boot minimal (en assembleur) vous est fourni ci-dessous.

/*
 * This is a minimal boot code:
 * - It initializes the Status Register (SR).
 * - It defines the stack size and initializes the stack pointer ($29).
 * - It initializes the EPC register, and jumps in user code.
 */

    .section .reset, "ax", @progbits

    .func   reset
    .type   reset, %function

reset:

    .set noreorder

    /* initializes SR register */
    li      $26,    0x00000013          /* UM, EXL and IE */
    mtc0    $26,    $12

    /* initializes stack pointer */
    la      $29,    seg_stack_base
    addiu   $29,    $29,    0x4000      /* stack size = 16K bytes */

    /* jumps in user mode */
    la      $26,    seg_data_base
    lw      $26,    0($26)              /* retrieves the user code's entry point */
    mtc0    $26,    $14
    eret

    .set reorder

    .endfunc
    .size reset, .-reset
  • À quoi correspond la valeur stockée dans le registre SR ?
  • Quelle est la valeur initiale du pointeur de pile ?
  • Quelle est la taille de la zone réservée pour la pile de l'application ?
  • Expliquez brièvement ce que font les quatre dernières instructions.
  • Quelle fonctionnalité très importante manque-t-il dans ce code ?

5. Génération du code binaire

Le logiciel exécutable par le processeur MIPS32, se décompose en deux parties : la partie noyau constituée par le code du système d'exploitation GIET, et la partie utilisateur constituée par le code de l'application. Ces deux parties sont obtenues par la génération de deux fichiers binaires distincts : 'sys.bin' et 'app.bin'.

Pour générer sys.bin, le code binaire du GIET, vous devrez compiler 8 fichiers source:

  • Le fichier reset.s, écrit en assembleur et qui contient le code de démarrage. Ce code (fourni ci-dessous) s'exécute en mode noyau puisque le signal reset force le processeur en mode noyau.
  • Le fichier giet.s, lui aussi écrit en assembleur, et qui contient le coeur du système d'exploitation.
  • Les fichiers common.c, drivers.c, irq_handler.c, ctx_handler.c, exc_handler.c et sys_handler.c, écrits en C et qui contiennent le code des fonctions qui permettent entre autres d'accéder aux périphériques, de gérer les interruptions/appels systèmes, etc. Ils doivent nécessairement s'exécuter en mode noyau. Par exemple, les deux fonctions __tty_write() et __tty_read() permettent d'accéder aux registres du terminal TTY en lecture ou en écriture, et les deux fonctions __procid() et __proctime() permettent d'accéder à deux registres protégés du processeur (identifiant du processeur et compteur de cycle du processeur) .

Pour générer app.bin, le code binaire de l'application logicielle, vous devrez compiler 2 fichiers source:

  • Le fichier stdio.c est une bibliothèque écrite en C qui contient le code des appels système pouvant être utilisés par un programme utilisateur pour accéder aux périphériques ou aux registres protégés du processeur. Ces fonctions commencent à s'exécuter en mode utilisateur, et contiennent toutes (au moins) un appel à l'instruction syscall permettant de passer la main au système d'exploitation, en lui transmettant le numéro de l'appel système à exécuter.
  • Le fichier main.c, également écrit en C, contient le programme utilisateur, et fait appel aux fonctions définies dans le fichier stdio.c pour accéder aux ressources protégées.

Remarque générale :

Le fichier main.c dépend évidemment de l'application logicielle que l'on souhaite exécuter, et il variera entre les différents TP de l'UE ALMO. Le code contenu dans le fichier reset.s (fourni ci-dessous) est chargé d'initialiser les composants de l'architecture matérielle, et Il évoluera également au cours de l'UE ALMO, puisqu'on augmentera progressivement le nombre de composants matériels connectés sur le bus système, qui devront être initialisés.

Questions :

  • Qu'est-ce qu'une chaîne de compilation C croisée ? Quelles sont les principales étapes d'une chaîne de compilation C ? Comment peut-on vérifier le code binaire généré ?
  • Quelle est la commande à lancer pour générer le fichier assembleur ex.s, en partant d'un fichier source en C ex.c ?
  • Quelle est la commande à lancer pour générer le fichier objet ex.o partant d'un fichier assembleur ex.s ?
  • Quelle est la commande à lancer pour générer le fichier objet ex.o directement à partir d'un fichier source en C ex.c ?

Dans le cas de la génération du fichier sys.bin, les 8 fichiers objets .o correspondant aux 8 fichiers sources décrits plus haut sont des entités séparées, qui sont compilées séparément.

Lors de la génération de ces fichiers objets (étapes 1 et 2), le compilateur ne dispose d'aucune information sur les adresses où seront rangés ces objets dans l'espace adressable.

Pour cette raison, dans chaque fichier objet, les instructions sont initialement rangées relativement à l'adresse 0x00000000, de même que les données. La deuxième conséquence de cette compilation séparée est qu'il existe des références non résolues entre les différents fichiers objets. Par exemple, l'adresse où est rangée la première instruction de la fonction d'affichage _putk est référencée dans le fichier drivers.c mais n‘est pas encore définie lorsqu'on compile le fichier drivers.c.

C'est donc le rôle de l'éditeur de liens d'analyser toutes les sections définies dans les fichiers .o, et de construire les segments en regroupant les sections définies dans les fichiers objet (grâce aux directives définies dans les fichier de script ld) et enfin, de résoudre les références non résolues.

  • Quelle est la commande permettant de créer le fichier binaire exécutable sys.bin, correspondant au GIET, à partir des fichiers .o (et en supposant l'existance d'un fichier ld nommé sys.ld) ?
  • Les fichiers de script ld servent non seulement à définir les adresses de base des différents segments, mais aussi à définir les directives de construction des segments. Expliquer la signification des lignes suivantes extraites du fichier sys.ld.
    . = seg_kcode_base;
    seg_kcode : {
        *(.giet)
        *(.text)
    }
Last modified 5 years ago Last modified on Aug 24, 2019, 10:55:38 AM