TD9: Boot et Premier Programme En Mode Kernel (PDF)

Summary

Ce document contient des questions et réponses sur le boot et le premier programme en mode kernel. Il aborde l'architecture matérielle et la programmation en langage assembleur pour comprendre un prototype de système (Amo1).

Full Transcript

DOCS [Start][Config][User][Kernel] — COURS [9bis] [10bis] — TD [​10][​11] — TP [​9][​10][​11] — ZIP [gcc...] Boot et premier programme en mode kernel 1....

DOCS [Start][Config][User][Kernel] — COURS [9bis] [10bis] — TD [​10][​11] — TP [​9][​10][​11] — ZIP [gcc...] Boot et premier programme en mode kernel 1. 2. Analyse de l'architecture Programmation assembleur 3. Compilation et édition de liens 1. Analyse de l'architecture 4. Programmation en C 6. Usage de Make (optionnel) Les trois figures ci-dessous donnent des informations sur l'architecture du prototype almo1 sur lequel vous allez travailler. À gauche, vous avez un schéma 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. À droite, vous avez la représentation de l'espace d'adressage du prototype. Questions 1. Il y a deux mémoires dans almo1 : RAM et ROM. Qu'est-ce qui les distinguent et que contiennent-elles ? (C9 S6+S9) 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. 2. 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 ? (C9 S7) L'espace d'adressage du MIPS est l'ensemble des adresses que peut former le MIPS. Les adresses sont sur 32 bits et désignent chacune un octet, il y a donc 232 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). 3. Dans quel composant matériel se trouve le code de démarrage et à quelle adresse est-il placé dans l'espace d'adressage et pourquoi à cette adresse ? (C9 S6+S7) Le code de boot est dans la mémoire ROM. Il commence à l'adresse 0xBFC00000 parce que c'est l'adresse qu'envoie le MIPS au démarrage. 4. Quel composant permet de faire des entrées-sorties dans almo1 ? Citez d'autres composants qui pourraient être présents dans un autre SoC ? (C9 S6+connaissances personnelles) 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. 5. Il y a 4 registres de commande 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 ? (C9 S10) 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 6. 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 ), à quelle adresse est le registre TTY_READ de TTY1 ? (C9 S10) 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. 7. Que représentent les flèches bleues sur le schéma ci-dessus ? Pourquoi ne vont-elles que dans une seule direction ? (C9 S11) Ces flèches représentent le sens des requêtes d'accès à la mémoire, c'est-à-dire les loads et les stores. Ces requêtes sont émises 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 les 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. 2. 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 pour au moins la raison suivante : 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 le contenu des registres 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. (C9 S10) Écrivez le code permettant d'écrire le code ASCII 'x' sur le terminal 0. Vous avez droit à tous les registres du MIPS puisqu'à ce stade il n'y pas de conventions sur leur utilisation. Ce qu'il faut bien comprendre, c'est que l'adresse du registre TTY_WRITE est l'adresse d'une sortie du SoC, ce n'est pas une mémoire à proprement parler. Il est d'ailleurs interdit de lire à cette adresse. Pour écrire un message à l'écran, il faut écrire tous les caractères du message à cette adresse (0xD0200000). lui $4, 0xD020 // $4 file.o.s ou objdump -D bin.x > bin.x.s Appel du désassembleur qui prend les fichiers binaires (.o ou.x ) pour retrouver le code produit par le compilateur pour le debug. Questions sur l'édition de lien Le 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 `script` pour ld. __tty_regs_map = [... question 1...] ; __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 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+S38+S39) Si ça semble difficile, il faut revoir ce qu'est l'espace d'adressage. __tty_regs_map = 0xD0200000 ; __kdata_origin = 0x80020000 ; __kdata_length = 0x003E0000 ; 2. 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 S38+S39) La syntaxe est assez explicite, cela ne devrait pas poser de problème. kdata_region : ORIGIN = __kdata_origin, LENGTH = __kdata_length 3. 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 S38+S39) Le * devant (.text) est une expression régulière permettant de dire à l'éditeur de liens quels fichiers sont concernés, ici avec * c'est tous les fichiers. Les expressions régulières sont celles qu'on utilise avec le shell..ktext : { *(.text) } > ktext_region 4. Programmation en C Vous 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. 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 ? (c'est une directive que l'on donne au compilateur C) Le cours 9 n'en parle pas, mais dans le code vous trouverez cette directive un peu partout. Il y a deux usages. 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. 2. 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. 2. Pourquoi déclarer des fonctions ou des variables extern ? Ç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. 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. 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. 3. 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. 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. struct test_s { int a; int b; }; struct test_s tab; 4. 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) struct tty_s { int write; // tty's output int status; // tty's status something to read if not null) int read; // tty's input int unused; // unused }; extern volatile struct tty_s __tty_regs_map[NTTYS]; // extern : parce que ce tableau n'est pas dans ce fichier // volatile : parce que le contenu du tableau peut changer tout seul, gcc doit le lire à chaque fois // cela implique que gcc ne peut pas faire d'optimisation avec les registres du MIPS // (cf. note en fin de page) 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. int getchar0(void) { while (__tty_regs_map.status == 0); return __tty_regs_map.read; } 5. É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. int puts0(char *s) { char *p = s; while (*p != '\0') { // p est une variable locale que l'on peut modifier __tty_regs_map.write = *p; // *p désigne le caractère pointé par p p = p + 1; // on incrémente p, donc on pointe le caractère suivant } return p-s; // finalement, on rend le nombre de caractères écrits } 6. Usage de Make (optionnel) 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 recopié partiellement le premier Makefile pour montrer sa forme et poser quelques questions, auxquels vous savez certainement répondre. La syntaxe des Makefile s peut-être très complexe (c'est un vieux langage), ici nous ne verrons qu'une petite partie. Notez que le Makefile voit les variables du shell comme s'il les avait définies lui-même. # Tools and parameters definitions # ------------------------------------------------------------------------------ # -- options used by the prototype simulator NTTY ?= 2 # default number of ttys # -- tools CC = mipsel-unknown-elf-gcc # cross-compiler MIPS LD = mipsel-unknown-elf-ld # linker MIPS OD = mipsel-unknown-elf-objdump # desassembler MIPS SX = almo1.x # prototype simulator (nammed almo1.x) # -- All flags used for the gcc compiler CFLAGS = -c # stop after compilation, then produce.o CFLAGS += -Wall -Werror # near all C warnings that becoming errors CFLAGS += -mips32r2 # define of MIPS version CFLAGS += -std=c99 # define of syntax version of C CFLAGS += -fno-common # no use common sections for nostatic vars CFLAGS += -fno-builtin # no use builtin func of gcc (ie 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. # dir. where include are CFLAGS += -DNTTYS=$(NTTY) # 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 hcpua.o $(CFLAGS) hcpua.S @$(OD) -D hcpua.o > hcpua.o.s $(LD) -o kernel.x -T kernel.ld hcpua.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 5. Où est utilisé CFLAGS ? Que fait -DNTTYS=$(NTTY) et pourquoi est-ce utile ici ? (C9 annexe S8) Le compilateur C peut avoir beaucoup de paramètres. Définir une variable CFLAGS permet de les déclarer une fois au début et d'utiliser cette variable plusieurs fois dans le Makefile. Si on veut changer un argument, il suffit de le faire une seule fois. Ce genre de choses est nécessaire si on veut faire des Makefile s facile à lire et à faire évoluer. La variable CFLAGS est utilisée par gcc , il y a ici toutes les options indispensables pour compiler mais il en existe beaucoup, ce qui fait des tonnes de combinaison d'options ! -DNTTYS=$(NTTY) permet de définir et donner une valeur à une macro (ici définition NTTYS avec la valeur $(NNTY) comme le fait un #define dans un fichier C. Cette commande évite donc d'ouvrir les codes pour les changer. 6. Si on exécute make sans cible, que se passe-t-il ? (C9 annexe S6) 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. 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. Réponses non présentes dans les slides, mais utiles à savoir. 6. à quoi servent @ et - au début de certaines commandes ? @ permets 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. - permets 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. 7. Au début du fichier se trouve la déclaration des variables du Makefile, quelle est la différence entre = , ?= et += ? = 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. Note sur le mot clé volatile 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. 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 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. Dernière modification le 26 nov. 2024 à 15:47:22)

Use Quizgecko on...
Browser
Browser