Changes between Version 20 and Version 21 of Archi-1-TD9


Ignore:
Timestamp:
Nov 6, 2023, 6:08:22 PM (13 months ago)
Author:
franck
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • Archi-1-TD9

    v20 v21  
    204204
    205205
    206 = 3. Compilation
     206= 3. Compilation et édition de liens
    207207
    208208
     
    213213  - 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`
    214214* `ld -o bin.x -Tkernel.ld files.o ...`
    215   - Appel de l'éditeur de liens 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. \\Autrement dit, quand un fichier `h.o` utilise une fonction `fg()` ou une variable `vg` définie dans un autre fichier `g.o` (`h` et `g` sont là pour illustrer), alors l'éditeur de liens place dans l'espace d'adressage les sections `.text` et `.data` des fichiers `h.o` et `g.o`, puis il détermine alors quelles sont les adresses de `fg()` et `vg` en mémoire et il complètent les instructions de `h` qui utilisent ces adresses.
     215  - Appel de l'éditeur de liens 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. \\Autrement dit, quand un fichier `f1.o` utilise une fonction `funf2()` ou une variable `varf2` définie dans un autre fichier `f2.o`, alors l'éditeur de liens place dans l'espace d'adressage les sections `.text` et `.data` des fichiers `f1.o` et `f2.o`, puis il détermine alors quelles sont les adresses de `funf2()` et `varf2` dans l'espace d'adressage et il complètent les instructions de `f1` qui utilisent ces adresses.
    216216* `objdump -D file.o > file.o.s` ou `objdump -D bin.x > bin.x.s`
    217   - Appel du désassembleur qui prend les fichiers binaires (`.o` ou `.x`) pour retrouver le code produit par le compilateur à des fins de debug ou de curiosité.
     217  - Appel du désassembleur qui prend les fichiers binaires (`.o` ou `.x`) pour retrouver le code produit par le compilateur pour le debug.
    218218
    219219**Questions sur l'édition de lien**
     
    221221Le fichier `kernel.ld` décrit l'espace d'adressage et la manière de remplir les sections dans le programme exécutable. Ce fichier est utilisé par l'éditeur de lien. C'est un `ldscript`, c'est-à-dire un [https://www.wikiwand.com/fr/Langage_de_script `script`] pour `ld`.
    222222{{{#!c
    223 __tty_regs_map   = 0xd0200000 ;
     223__tty_regs_map   = [... question 1 ...] ;
    224224__boot_origin    = 0xbfc00000 ;
    225225__boot_length    = 0x00001000 ;
     
    245245}
    246246}}}
    247 
    248 1. Le fichier `kernel.ld` 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 que 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`. Pour répondre, il faut savoir interpréter le dessin représentant l'espace d'adressage. (C9 S7+S22+S23)
     2471. Le fichier `kernel.ld` 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. 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`. Pour répondre, il faut savoir interpréter le dessin représentant l'espace d'adressage. (C9 S7+S22+S23)
    249248{{{#!protected ------------------------------------------------------------------------------------
    250249''
    251250* Si ça semble difficile, il faut revoir ce qu'est l'espace d'adressage.
    252251{{{#!c
     252__tty_regs_map   = 0xD0200000 ;
    253253__kdata_origin   = 0x80020000 ;
    254254__kdata_length   = 0x003E0000 ;
     
    256256''
    257257}}}
    258 1. Le fichier contient ensuite la déclaration des régions (dans `MEMORY{...}`) qui seront remplies par l'éditeur de lien avec les sections trouvées dans les fichiers objets selon un ordre décrit dans la partie `SECTIONS{}` du `ldscript`.  Complétez cette partie (la zone `[... question 2 ...]`) pour ajouter les lignes correspondant à la déclaration de la région `kdata_region` ? (C9 S22+S23)
     2581. Le fichier contient ensuite la déclaration des régions (dans `MEMORY{...}`), c'est-à-dire les segments d'adresse en mémoire qui seront remplies par l'éditeur de lien avec les sections trouvées dans les fichiers objets selon un ordre décrit dans la partie `SECTIONS{}` du `ldscript`.  Complétez cette partie (la zone `[... question 2 ...]`) pour ajouter les lignes correspondant à la déclaration de la région `kdata_region` ? (C9 S22+S23)
    259259{{{#!protected ------------------------------------------------------------------------------------
    260260''
     
    265265''
    266266}}}
    267 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.\\Il faut bien comprendre que `.ktext` est une section produite par l'éditeur de liens. C'est ce que l'on appelle une section de sortie. `.text` est une section que l'éditeur de liens trouve dans un fichier objet `.o`, c'est ce que l'on appelle une section d'entrée. Comme il y a plusieurs fichiers objet, on doit dire à l'éditeur de lien de prendre toutes les sections `.text` de tous les fichiers qu'on lui donne. (C9 S22+S23)
     2671.  Enfin le fichier décrit 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.\\Il faut bien comprendre que `.ktext` est une section produite par l'éditeur de liens. C'est ce que l'on appelle une section de sortie. `.text` est une section que l'éditeur de liens trouve dans un fichier objet `.o`, c'est ce que l'on appelle une section d'entrée. Comme il y a plusieurs fichiers objet, on doit dire à l'éditeur de lien de prendre toutes les sections `.text` de tous les fichiers qu'on lui donne. (C9 S22+S23)
    268268{{{#!protected ------------------------------------------------------------------------------------
    269269''
     
    277277}}}
    278278
    279 **Questions sur les Makefiles**
     279
     280
     281= 3. Programmation en C
     282
     283
     284
     285
     286Vous savez déjà programmer en C, mais vous allez voir ici des syntaxes ou des cas d'usage que vous ne connaissez peut-être pas encore.
     287Les 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.
     288
     289**Questions
     290
     2911. Quels sont les usages du mot clé `static` en C ?  (c'est une directive que l'on donne au compilateur C)
     292{{{#!protected ------------------------------------------------------------------------------------
     293''
     294* Le cours 9 n'en parle pas, mais dans le code vous trouverez cette directive un peu partout. Il y a deux usages.
     295 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ù tout label est implicitement  `static` ; il faut demander avec la directive `.globl` de le rendre visible.
     296 1. Déclarer `static` une variable locale (dans une fonction donc) 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 d'exécution de la fonction (c'est-à-dire qu'elle n'est pas dans la pile parce que le contexte d'exécution est libéré en sortie de fonction). Une variable locale `static` est en fait allouée comme une variable globale mais son usage est limité à la seule fonction où elle est définie.
     297''
     298}}}
     2991. Pourquoi déclarer des fonctions ou des variables `extern` ?
     300{{{#!protected ------------------------------------------------------------------------------------
     301''
     302* Ça non plus ce n'est pas dit dans le cours mais c'est sensé être connu. Notez que la directive externe est implicite en C et qu'on peut donc ne pas l'écrire. On la met pour la lisibilité du code.
     303* Les déclarations `extern` permettent d'informer que le compilateur qu'une variable ou qu'une fonction existe et est définie dans un autre fichier. Le compilateur connaît ainsi le 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 doivent être déclarées / leur existence et leur type doivent être connus avant leur utilisation.
     304* Il n'y a pas de déclaration `extern` en assembleur parce que ce n'est pas un langage typé. Pour l'assembleur, un label c'est juste une adresse donc un nombre.
     305''
     306}}}
     3071. Comment déclarer un tableau de structures en variable globale ? La structure est nommée `test_s`, elle a deux champs `int` nommés `a` et `b`. Le tableau est nommé `tab` et a 2 cases.
     308{{{#!protected ------------------------------------------------------------------------------------
     309''
     310* Là encore, ce sont des connaissances censées être connues, mais c'est important parce qu'on a besoin de le comprendre pour la déclaration des registres du TTY.
     311{{{#!c
     312struct test_s {
     313  int a;
     314  int b;
     315};
     316struct test_s tab[2];
     317}}}
     318''
     319}}}
     3201. Supposons que la structure `tty_s` et le tableau de registres de `TTY` soient définis comme suit. Écrivez la fonction C `int getchar0(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`. `NTTYS` est un `#define` définit dans le Makefile de compilation avec le nombre de terminaux du SoC (en utilisant l'option `-D` de gcc). (C9 S10)
     321{{{#!c
     322struct tty_s {
     323    int write;          // tty's output
     324    int status;         // tty's status something to read if not null)
     325    int read;           // tty's input
     326    int unused;         // unused
     327};
     328extern volatile struct tty_s __tty_regs_map[NTTYS];
     329// extern   : parce que ce tableau n'est pas dans ce fichier
     330// volatile : parce que le contenu du tableau peut changer tout seul, gcc doit le lire à chaque fois
     331//            cela implique que gcc ne peut pas faire d'optimisation avec les registres du MIPS
     332//            (cf. note en fin de page)
     333}}}
     334{{{#!protected ------------------------------------------------------------------------------------
     335''
     336* En principe, cela ne devrait pas poser de difficulté, mais cela nécessite d'avoir compris comment fonctionne le TTY et de savoir écrire une fonction en C. Pour aider, au cas où, on peut donner la description de ce qui est attendu:\\Tant que le registre `status` est à 0 alors attendre, puis lire le registre `read`.\\Notez que cela nécessite de savoir accéder aux champs d'une structure.
     337{{{#!c
     338int getchar0(void)
     339{
     340   while (__tty_regs_map[0].status == 0);
     341   return __tty_regs_map[0].read;
     342}
     343}}}
     344''
     345}}}
     3461. Écrivez la fonction C `int puts0(char *s)` qui écrit tous les caractères de la chaîne `s` sur le terminal TTY0. La fonction doit rendre le nombre de caractères écrits. On suppose que les registres des TTYs sont définis comme dans la question précédente.
     347{{{#!c
     348int puts0(char *s)
     349{
     350   int res = 0;
     351   while (*s) {                       // s est une variable locale que l'on peut modifier
     352     __tty_regs_map[0].write = *s;    // *s désigne le caractère pointé par s
     353     s = s + 1;                       // on incrémente s, donc on pointe le caractère suivant
     354     res = res + 1;                   // et on incrémente le nombre de caractères écrits
     355   }
     356   return res;                        // finalement, on rend le nombre de caractères écrits
     357}
     358}}}
     359''
     360}}}
     361
     362
     363
     364== 4. Usage de Make
     365
     366
    280367
    281368Nous 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 recopié partiellement le premier Makefile pour montrer sa forme et poser quelques questions, auxquels vous savez certainement répondre.
     
    285372# Tools and parameters definitions
    286373# ------------------------------------------------------------------------------
     374# -- options used by the prototype simulator
    287375NTTY   ?= 2 #                          default number of ttys
    288376
    289 CC      = mipsel-unknown-elf-gcc #     compiler
    290 LD      = mipsel-unknown-elf-ld #      linker
    291 OD      = mipsel-unknown-elf-objdump # desassembler
    292 SX      = almo1.x #                    prototype simulator
    293 
     377# -- tools
     378CC      = mipsel-unknown-elf-gcc #     cross-compiler MIPS
     379LD      = mipsel-unknown-elf-ld #      linker MIPS
     380OD      = mipsel-unknown-elf-objdump # desassembler MIPS
     381SX      = almo1.x #                    prototype simulator (nammed almo1.x)
     382
     383# -- All flags used for the gcc compiler
    294384CFLAGS  = -c #                         stop after compilation, then produce .o
    295385CFLAGS += -Wall -Werror #              near all C warnings that becoming errors
     
    335425{{{#!protected ------------------------------------------------------------------------------------
    336426''
    337 * Mettre une règle `help` comme règle par défaut permet de documenter l'usage du `Makefile`, ce qui est plutôt une bonne pratique quand il y a beaucoup de cible et de paramètres. C'est d'autant plus vrai qu'on utilise les Makefiles comme des ensembles de `shell script`.
     427* On exécute le règle help. Mettre une règle `help` comme règle par défaut permet de documenter l'usage du `Makefile`, ce qui est plutôt une bonne pratique quand il y a beaucoup de cible et de paramètres. C'est d'autant plus vrai qu'on utilise les Makefiles comme des ensembles de `shell script`.
    338428* C'est la première cible qui est choisie, donc ici c'est équivalent à `make help`. Cela affiche l'usage pour connaître les cibles disponibles.
    339429''
     
    356446}}}
    357447
    358 
    359 
    360 
    361 
    362 = 4. Programmation en C
    363 
    364 
    365 
    366 
    367 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.
    368 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.
    369 
    370 **Questions
    371 
    372 1. Quels sont les usages du mot clé `static` en C ?  (c'est une directive que l'on donne au compilateur C)
    373 {{{#!protected ------------------------------------------------------------------------------------
    374 ''
    375 * Le cours 9 n'en parle pas, mais dans le code vous trouverez cette directive un peu partout. Il y a deux usages.
    376  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ù tout label est implicitement  `static` ; il faut demander avec la directive `.globl` de le rendre visible.
    377  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 (c'est-à-dire qu'elle n'est pas dans la pile parce que le contexte est libéré en sortie de fonction). Une variable locale `static` est en fait allouée comme une variable globale mais son usage est limité à la seule fonction où elle est définie.
    378 ''
    379 }}}
    380 1. Pourquoi déclarer des fonctions ou des variables `extern` ?
    381 {{{#!protected ------------------------------------------------------------------------------------
    382 ''
    383 * Ça n'ont plus ce n'est pas dit dans le cours mais c'est sensé être connu, sinon c'est qu'il y a des choses à apprendre. Notez que la directive externe est implicite en C et qu'on peut donc ne pas l'écrire. On la met pour la lisibilité du code.
    384 * Les déclarations `extern` permettent d'informer que le compilateur qu'une variable ou qu'une fonction existe et est définie ailleurs. Le compilateur connaît ainsi le 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 doivent être déclarées / leur existence et leur type doivent être connus avant leur utilisation.
    385 * Il n'y a pas de déclaration `extern` en assembleur parce que ce n'est pas un langage typé. Pour l'assembleur, un label c'est juste une adresse donc un nombre.
    386 ''
    387 }}}
    388 1. Comment déclarer un tableau de structures en variable globale ? La structure est nommée `test_s`, elle a deux champs `int` nommés `a` et `b`. Le tableau est nommé `tab` et a 2 cases.
    389 {{{#!protected ------------------------------------------------------------------------------------
    390 ''
    391 * Là encore, ce sont des connaissances censées être connues, mais c'est important parce qu'on a besoin de le comprendre pour la déclaration des registres du TTY.
    392 {{{#!c
    393 struct test_s {
    394   int a;
    395   int b;
    396 };
    397 struct test_s tab[2];
    398 }}}
    399 ''
    400 }}}
    401 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`. `NTTYS` est un `#define` définit dans le Makefile de compilation avec le nombre de terminaux du SoC (en utilisant l'option `-D` de gcc). (C9 S10)
    402 {{{#!c
    403 struct tty_s {
    404     int write;          // tty's output
    405     int status;         // tty's status something to read if not null)
    406     int read;           // tty's input
    407     int unused;         // unused
    408 };
    409 extern volatile struct tty_s __tty_regs_map[NTTYS];
    410 }}}
    411 {{{#!protected ------------------------------------------------------------------------------------
    412 ''
    413 * En principe, cela ne devrait pas poser de difficulté, mais cela nécessite d'avoir compris comment fonctionne le TTY et de savoir écrire une fonction en C. Pour aider, au cas où, on peut donner la description de ce qui est attendu:\\Tant que le registre `status` est à 0 alors attendre, puis lire le registre `read`.\\Notez que cela nécessite de savoir accéder aux champs d'une structure.
    414 {{{#!c
    415 int getchar(void)
    416 {
    417    while (__tty_regs_map[0].status == 0);
    418    return __tty_regs_map[0].read;
    419 }
    420 }}}
    421 ''
    422 }}}
    423 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 ... ?
    424 {{{#!protected ------------------------------------------------------------------------------------
    425 ''
    426 * Ce n'est pas dit dans le cours, mais c'est un concept important. Quand le programme doit aller chercher une donnée dans la mémoire puis faire plusieurs calculs dessus, le compilateur optimise en réservant un registre du processeur pour cette variable afin de ne pas être obligé d'aller lire la mémoire à chaque fois. Mais, il y a des cas où ce comportement n'est pas souhaitable (il est même interdit). C'est le cas pour les données qui se trouvent dans les registres de périphériques. Ces données peuvent être changées par le périphérique sans que le processeur le sache, de sorte qu'une valeur lue par le processeur à l'instant `t` n'est plus la même (dans le registre du périphérique) à l'instant `t+1`. Le compilateur ne doit pas optimiser, il doit aller chercher la donnée en mémoire à chaque fois que le programme le demande.
    427 * `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).
    428 * Ainsi, les registres de périphériques doivent toujours être impérativement 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.
    429 ''
    430 }}}
     448-----
     449> ''Note sur le mot clé `volatile`
     450>   * ''Quand le programme doit aller chercher une donnée dans la mémoire puis faire plusieurs calculs dessus, le compilateur optimise en réservant un registre du processeur pour cette variable afin de ne pas être obligé d'aller lire la mémoire à chaque fois. Mais, il y a des cas où ce comportement n'est pas souhaitable (il est même interdit). C'est le cas pour les données qui se trouvent dans les registres de contrôleur de périphériques. Ces données peuvent être changées par le périphérique sans que le processeur le sache, de sorte qu'une valeur lue par le processeur à l'instant `t` n'est plus la même (dans le registre du périphérique) à l'instant `t+1`. Le compilateur ne doit pas optimiser, il doit aller chercher la donnée en mémoire à chaque fois que le programme le demande.''\\ \\
     451>    * ''`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). ''
     452>    * ''Ainsi, les registres de périphériques doivent toujours être impérativement 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.''