TD10 Application simple en mode utilisateur en MIPS PDF

Summary

Ce document présente une application simple en mode utilisateur dans l'architecture MIPS. Il explique les interactions entre le code de démarrage, le noyau, l'application et les bibliothèques système ainsi que les modes d'exécution du processeur MIPS, kernel et user. Il contient des questions sur les instructions système et leur utilisation dans le contexte de MIPS.

Full Transcript

DOCS [Start][Config][User][Kernel] — COURS [9bis] [10bis] — TD [​9][​11] — TP [​9][​10][​11] — ZIP [gcc...] Application simple en mode utilisateur 1. 2....

DOCS [Start][Config][User][Kernel] — COURS [9bis] [10bis] — TD [​9][​11] — TP [​9][​10][​11] — ZIP [gcc...] Application simple en mode utilisateur 1. 2. Les modes d'exécution du MIPS et les instructions système Passage entre les modes kernel et user 3. Langage C pour la programmation système Le schéma présenté rapidement au cours 10 (slides 26 à 31) et en détail dans 4. Génération du code exécutable (optionnel) l'annexe du cours 10 (slides 1 à 32) représente l'exécution d'une application utilisateur très simple dont le comportement est défini par la fonction main(). L'exécution part du démarrage du SoC et va jusqu'à l'exécution de la fonction exit() qui stoppe l'avancée du programme. L'objectif de ce schéma est de comprendre les interactions entre le code de boot, le noyau, l'application et les bibliothèques système. Le schéma ci-dessous ne contient pas l'intégralité du code pour des raisons évidentes de lisibilité, mais ce qui reste devrait suffire. En bas à gauche, c'est le code de boot qui, ici, se contente d'initialiser la pile d'exécution du noyau et d'entrer dans le noyau par la fonction kinit() (kernel init). Ce code s'exécute en mode kernel , mais il ne fait pas partie du noyau car, dans un vrai système, il doit charger le noyau depuis le disque dur, mais, ici, le noyau est déjà en mémoire alors c'est plus simple. En bas, c'est le noyau avec la fonction kinit() qui initialise les structures de données internes du noyau. Ici, il s'agit juste de mettre les variables globales non initialisées à 0, puis d'appeler la routine app_load qui va entrer dans la première fonction de l'application utilisateur nommée _start(). Dans le noyau, sur la figure, on voit aussi la routine kentry qui est le point d'entrée du noyau pour la gestion des services. Actuellement, il n'y a que le gestionnaire d'appel système ( syscall ). Son comportement est succinctement résumé. En haut, c'est l'application utilisateur, décomposée en trois parties. La première à gauche est la fonction _start() appelée par le noyau au tout début de l'application. Cette fonction initialise à 0 les variables globales non initialisées dans le programme, puis elle appelle la fonction main(). Si on sort de la fonction main() avec un return , la fonction _start fait l'appel système exit(). La seconde partie au centre contient le code de l'utilisateur (notez que la fonction main() ou l'une des fonctions appelées par la fonction main() peut demander une sortie anticipée de l'application en appelant directement exit() ). Enfin, la troisième partie, à droite, c'est le code des bibliothèques système utilisées par l'application, ce sont elles qui font les appels système, ici, seule la fonction clock() est représentée. Le but de cette séance est de s'intéresser à des points particulier de ce schéma : D'abord, nous abordons les 2 modes d'exécution du MIPS, kernel et user, utilisés respectivement pour le noyau et l'application utilisateur. Puis, nous voyons les passages du noyau à l'application et de l'application au noyau. Ensuite, nous nous intéressons à comment écrire le code C et assembleur pour contrôler le placement en mémoire. Enfin, il y a quelques quelques questions sur comment compiler pour faciliter la compréhension des TPs. 1. Les modes d'exécution du MIPS et les instructions système 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 en particulier. Questions 1. Le MIPS propose deux modes d'exécution, rappelez quels sont ces deux modes, quel est le mode utilisé par le noyau et quel est le mode utilisé par l'application ? (C10 S6+S7) 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 Le mode kernel permet d'accéder à tout l'espace d'adressage et donc aux périphériques dont les registres sont mappés à des adresses accessibles uniquement lorsque le processeur est en mode kernel. 2. Commencez par rappeler ce qu'est l'espace d'adressage du MIPS et dîtes ce que signifie «une adresse X est mappée dans l'espace d'adressage du MIPS». Est-ce qu'une adresse X mappée dans l'espace d'adressage du MIPS est toujours accessible (en lecture ou en écriture) quelque soit le mode d'exécution du MIPS. (C10 S7) L'espace d'adressage du MIPS, c'est l'ensemble des adresses que peut produire le MIPS, il y a 232 adresses d'octets. On dit qu'une adresse X est mappée dans l'espace d'adressage, si cette adresse 'X' est bien dans un segment d'adresses utilisables de l'espace d'adressage. Autrement dit, le MIPS peut faire des lectures et des écritures à cette adresse, ou encore qu'il y a bien une case mémoire pour cette adresse X`. Non X n'est pas toujours accessible, si X < 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. 3. 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 dans le coprocesseur 0. Chaque registre du coprocesseur 0 porte un nom correspondant à son usage, nous en avons vu 3 en cours (C10 S7+S10 à S14) : c0_sr , c0_cause et c0_epc. Donner leur numéro et leur rôle en une phrase ? Les registres du coprocesseur 0 sont numérotés de $0 à $31 , comme les registres GPR, ce qui peut induire une certaine confusion, parce qu'avec cette syntaxe, si on demande ce qui se trouve dans le registres $14 sans préciser qu'il s'agit du registre $14 du coprocesseur 0 , alors on ne peut pas répondre. C'est pour cette raison qu'il est préférable d'utiliser leur nom ( EPC ou c0_epc pour $14 par exemple ou alors c0_$14 ) Nous avons vu les 3 principaux c0_sr $12 contient essentiellement le mode d'exécution du MIPS et le bit d'autorisation des interruptions c0_cause $13 contient la cause d'appel du noyau contient l'adresse de l'instruction ayant provoqué l'appel du noyau ou l'adresse de l'instruction c0_epc $14 suivante Il y en a d'autres, dont certains seront utilisés plus tard contient l'adresse mal formée si la cause est une exception due à un accès non aligné (p.ex. lw a c0_bar $8 une adresse non multiple de 4) c0_count $9 contient le nombre de cycles depuis le démarrage du MIPS c0_procid $15 contient le numéro du processeur (utile pour les architectures multicores) 4. Les deux instructions qui permettent de manipuler les registres du coprocesseur 0 sont mtc0 et mfc0 (C10 S11). Quelle est leur syntaxe ? réponse dans Documentation MIPS Architecture et assembleur (4.) Est-ce qu'on peut manipuler ces registres de coprocesseur avec d'autres instructions ? Écrivez les instructions permettant de faire c0_epc = c0_epc + 4 (vous utiliserez le registre GPR $8 ) 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 ) Attention à l'ordre des registres dans les instructions. L'ordre est toujours le même, c'est d'abord le registre $GPR puis le registre $C0, le sens de l'échange est défini par l'opcode de l'instruction (move TO ou move FROM coprocessor 0). $C0 peut être c0_sr (i.e. $12) ou c0_cause (i.e. $13) ou c0_epc (i.e. $14) non, il n'est pas possible d'utiliser d'autres instructions pour les manipuler, on peut juste les lire et les écrire en utilisant les instructions mtc0 et mfc0 c0_epc = c0_epc + 4 mfc0 $8, $14 addiu $8, $8, 4 mtc0 $8, $14 5. Le registre status ( c0_sr ou $12 du coprocesseur 0 ) 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 0, 1 et 4 de l'octet 0. (C10 S12+S13+S15) réponse dans Documentation MIPS Architecture et assembleur (6.) 0 → interruptions masquées 0 IE Interrupt Enable 1 → interruptions autorisées si ERL et EXL sont tous les deux à 0 1 → MIPS en mode exception à l'entrée dans le kernel 1 EXL EXception Level le MIPS est en mode kernel, interruptions masquées 0 → MIPS en mode kernel 4 UM User Mode 1 → MIPS en mode user, seulement si ERL et EXL sont tous les deux à 0 6. Le registre cause ( c0_cause ou $13 du coprocesseur 0 ) 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 (C10 S14+S15) réponse dans Documentation MIPS Architecture et assembleur (7.) 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 codes les plus importantes à connaitre sont 0 et 8 (interruption et syscall). Les autres codes sont pour les exceptions, c'est-à-dire des fautes faites par le programme. 0 0000b interruption un contrôleur de périphérique à lever un signal IRQ 4 0100b ADEL lecture non-alignée (p. ex. lw a une adresse impaire) 8 1000b syscall exécution de l'instruction syscall 7. Le registre c0_epc ( $14 du coprocesseur 0 ) est un registre 32 bits qui contient une adresse. Vous devriez l'avoir décrit dans la question 2. Expliquez pourquoi, dans le cas d'une exception, ce doit être l'adresse de l'instruction qui provoque une exception qui doit être stockée dans c0_epc ? (C10 S15) Une exception, c'est dû à une erreur du programme, telle que la lecture d'un mot à une adresse non mappée, 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 le registre c0_epc contient l'adresse de l'instruction fautive. Le gestionnaire d'exceptions dans le noyau pourra lire l'instruction et éventuellement corriger le problème. A titre indicatif, ce n'est pas la question, mais pour les syscall , c'est aussi l'adresse de l'instruction syscall qui est stockée dans c0_epc , or pour le retour de syscall , on souhaite aller à l'instruction suivante. Il faut donc incrémenter la valeur de c0_epc de 4 (les instructions font 4 octets) pour connaître la vraie adresse de retour du syscall. 8. Nous avons vu trois instructions utilisables seulement lorsque le MIPS est en mode kernel, lesquelles? Que font-elles? Est-ce que l'instruction syscall peut-être utilisée en mode user? (C10 S11) Les trois instructions sont mtc0 , mfc0 (déjà vues au dessus) et eret 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 pour les demandes de services. 9. Quelle est l'adresse d'entrée dans le noyau au démarrage (à la sortie du code de boot) et après (depuis l'application) ? (C10 S15 S20) Au démarrage, le boot saute à l'adresse de la fonction kinit() pour entrer dans le noyau. En dehors du démarrage, c'est 0x80000180. Il n'y a qu'une adresse pour toutes les causes syscall , exceptions et interruptions. 10. Que se passe-t-il lorsqu'on entre dans le noyau après de l'exécution de l'instruction syscall ? (C10 S15) L'instruction syscall induit 4 opérations élémentaires dans le MIPS: c0_epc ← PC (sauvegarde dans c0_epc adresse de l'instruction syscall ) c0_sr.EXL ← 1 (ainsi les bits c0_sr.UM et c0_sr.IE ne sont plus utilisés) c0_cause.XCODE ← 8 (c'est la cause syscall ) PC ← 0x80000180 (c'est l'adresse d'entrée dans le noyau) Ces 4 opérations élémentaires sont réalisées par l'instruction syscall ! 11. Quelle instruction utilise-t-on pour sortir du noyau afin d'entrer dans l'application ? Dîtes précisément ce que fait cette instruction dans le MIPS. (C10 S15) C'est l'instruction eret qui permet de sortir du noyau. C'est la seule instruction permettant de sortir du noyau. PC ← c0_epc (c'est l'équivalent du jr $31 pour sortir d'une fonction) c0_sr.EXL ← 0 (ainsi les bits c0_sr.UM et c0_sr.IE sont à nouveau utilisés) Ces 2 opérations élémentaires sont réalisées par l'instruction eret ! 2. Passage entre les modes kernel et user Le noyau et l'application sont deux exécutables compilés indépendamment mais qui ne sont pas indépendants puisqu'on doit passer du noyau à l'application et inversement. Vous savez déjà que l'application appelle les services du noyau avec l'instruction syscall , voyons comment cela 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 lorsqu'on produit un programme binaire exécutable, c'est-à-dire quel outil de la chaîne de compilation réalise ce placement en mémoire et avec quel fichier de configuration ? (C9 S18+S22+S23 C10 annexe S6+S8) 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 les fichiers ldscript kernel.ld ou user.ld que le programmeur peut imposer ses choix de placement dans l'espace d'adressage. Pour placer une fonction à une adresse précise, la méthode que vous avez vu consiste à créer une section grâce à la directive.section en assembleur ou grâce à la directive __attribute__((section())) pour les programmes C, dans les deux cas le programmeur choisit un nom de section. puis à positionner la section ainsi créée dans la description des SECTIONS du fichier ldscript concerné. 2. La première fonction d'un programme utilisateur est la fonction _start() , c'est elle qui appelle la fonction main(). La fonction _start() est donc dans le code de l'application, et non pas dans le noyau. Cependant le noyau doit connaître son adresse afin de pouvoir y sauter et ainsi entrer dans l'application. Dans le code ci-après, nous voyons comment la fonction kinit() appelle cette fonction _start(). Deux fichiers sont impliqués : kinit.c dans lequel se trouve la fonction void kinit(void) et hcpua.S dans lequel se trouve la fonction void app_load(void *) en charge d'appeler la fonction _start(). kinit.c: void kinit (void) { [...] extern int _start; # declaree ailleurs a une adresse connue de l'editeur de lien app_load (&_start); # appel de la fonction app_load definie dans hcpua.S } hcpua.S:.globl app_load app_load: mtc0 $4, $14 # $4 contient l'argument li $26, 0x12 # $26 ktext_region 9.kdata : { 10 *(.*data*) 11. = ALIGN(4); 12 __bss_origin =.; 13 *(.*bss*) 14. = ALIGN(4); 15 __bss_end =.; 16 } > kdata_region 17 } Expliquez ce que font les lignes 11, 12 et 15 ? (C10 S32) 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 ) sur une frontière de 24 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. 3. 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, par exemple une adresse d'entier ou une adresse de tableau d'entiers, Ou encore, une adresse de structure. Dans le fichier kernel.ld : __tty_regs_map = 0xd0200000 ; Dans le fichier harch.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]; Si NTTYS est une macro dont la valeur est 2 , quelle est l'adresse en mémoire __tty_regs_map.read ? __tty_regs_map est un tableau à 2 cases (puisque NTTYS = 2 ). Chaque case est une structure de 4 entiers, donc 0x10 octets (16 octets). read est le troisième champ de la structure, c'est un entier, donc en +8 par rapport au début de la strucrure. En conséquence __tty_regs_map.read est en 0xd0200018 À quoi servent les mots clés extern et volatile ? (C10 annexe S23 et connaissance du C) 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. 4. 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 (kentry) après l'exécution d'un syscall. Le gestionnaire de syscall est écrit en assembleur et il a besoin d'appeler une fonction écrite en langage C. Ce que fait le gestionnaire de syscall est: trouver l'adresse de la fonction C qu'il doit appeler pour exécuter le service demandé; placer cette adresse dans un registre, nous utilisons le registre $2 ; exécuter l'instruction jal (ici, jal $2 ) pour appeler la fonction. Que doivent contenir les registres $4 à $7 et comment doit-être la pile et le pointeur de pile? (Connaissance assembleur) 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, pour l'application user, il y a au plus 4 arguments (entier ou pointeur) pour tous les syscalls. Le gestionnaire de syscall ajoute un cinquième argument avec le numéro de service qu'il a reçu dans $2. En conséquence, le pointeur de pile pointe au début d'une zone vide de 4 entiers suivi d'un 5e avec le numéro du service. L'intérêt d'ajouter le numéro de service comme cinquième argument, c'est qu'il est possible de faire une fonction unique qui gère un ensemble de syscalls avec un switch/case sur le numéro de service. On ne le fait pas dans cette version. 5. Vous avez appris à écrire des programmes assembleur, mais parfois il est plus simple, voire nécessaire, de mélanger le code C et le code assembleur. Dans l'exemple ci-dessous, nous voyons comment la fonction syscall() est écrite. Cette fonction utilise l'instruction syscall. Deux exemples d'usage de la fonction syscall() pris dans le fichier tp2/4_libc/ulib/libc.c. 1 int fprintf (int tty, char *fmt,...) // tty identifiant du terminal 2 { // fmt chaine format, suivie d'arguments optionnels 3 int res; 4 char buffer[PRINTF_MAX]; 5 va_list ap; 6 va_start (ap, fmt); // définit le dernier argument non-optionnel 7 res = vsnprintf(buffer, sizeof(buffer), fmt, ap); // remplit le buffer avec la chaîne à afficher 8 res = syscall (tty, (int)buffer, 0, 0, SYSCALL_TTY_PUTS); // ← appel système 9 va_end(ap); 10 return res; 11 } 12 13 void exit (int status) 14 { 15 syscall( status, 0, 0, 0, SYSCALL_EXIT); // ← appel système 16 } Le code de la fonction syscall() en assembleur est dans le fichier C : tp2/4_libc/ulib/crt0.c 1 // int syscall (int a0, int a1, int a2, int a3, int syscall_code) 2 __asm__ ( 3 ".globl syscall \n" 4 "syscall: \n" 5 " lw $2,16($29) \n" 6 " syscall \n" 7 " jr $31 \n" 8 ); Combien d'arguments a la fonction syscall() ? Comment la fonction syscall() reçoit-elle ses arguments ? A quoi sert la ligne 3 de la fonction syscall() et que se passe-t-il si on la retire ? Expliquer la ligne 5 de la fonction syscall(). Aurait-il été possible de mettre le code de la fonction syscall() dans un fichier.S ? (C10 S31) La fonction syscall() a 5 a arguments Elle reçoit ses 4 premiers arguments dans les registres $4 à $7 et le 5e (le numéro de service) dans la pile. La ligne 3 sert à dire que syscall est une étiquette utilisée dans un autre fichier..globl signifie global label. Si on la retire, il y aura un problème lors de l'édition de lien. syscall() ne sera pas trouvé par l'éditeur de liens. Le noyau attend le numéro de service dans $2. Or le numéro du service est le 5e argument de la fonction syscall(). La ligne 5 permet d'aller le chercher dans la pile. oui, ce code de la fonction syscall() qui fait appel à l'instruction syscall aurait pu être mis dans un fichier en assembleur, mais cela aurait demandé d'avoir un fichier de plus, pour une seule fonction. Dans une version plus évoluée du système, il y aura d'autres fonctions assembleur, alors on créera un fichier assembleur pour les réunir. 4. Génération du code exécutable (optionnel) 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 à quoi sert un Makefile? (C9 annexe S5 à S7) 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 partielle du fichier cible lorsque quelques fichiers source changent (pas tous). Pour ce rôle, le Makefile exprime toutes les étapes de construction de la cible finale et des cibles intermédiaires sous forme d'un arbre dont les feuilles sont les fichiers sources. Les commandes d'une règle ne sont exécutées que si la date de la cible est plus ancienne que la date de l'une des sources dont elle dépend. 2. Vous n'allez pas à avoir à écrire un Makefile complètement. Toutefois, si vous ajoutez des fichiers source, 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. Les lignes qui suivent sont des extraits de 1_klibc/Makefile (le Makefile de l'étape-1). Dans cet extrait, 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. (C9 annexe S5 à S7) kernel.x : kernel.ld obj/hcpua.o obj/kinit.o obj/klibc.o obj/harch.o $(LD) -o $@ -T $^ $(OD) -D $@ > [email protected] obj/hcpua.o : hcpua.S hcpu.h $(CC) -o $@ $(CFLAGS) $< $(OD) -D $@ > [email protected] La cible finale est : kernel.x Les cibles intermédiaires sont : kernel.ld , obj/hcpua.o , obj/kinit.o , obj/klibc.o et obj/harch.o. La source est : hcpua.S Les variables automatiques servent à extraire des noms dans la définition de la dépendance ( cible : dépendances ) dans la première règle : $@ = cible = kernel.x $^ = l'ensemble des dépendances = kernel.ld , obj/hcpua.o , obj/kinit.o , obj/klibc.o et obj/harch.o dans la seconde règle : $@ = cible = obj/hcpua.o $< = la première des dépendances = hcpua.S 3. Dans le TP, à partir de la deuxième étape, nous avons trois répertoires de sources kernel , ulib et uapp. Chaque répertoire contient une fichier Makefile différent destiné à produire une cible différente grâce à une règle nommée compil , c.-à-d. si vous tapez make compil dans un de ces répertoires, cela compile les sources locales. Il y a aussi un Makefile dans le répertoire racine 4_libc. Dans ce dernier Makefile, une des règles est destinée à la compilation de l'ensemble des sources dans les trois sous-répertoires. Cette règle appelle récursivement la commande make en donnant en argument le nom du sous-répertoire où descendre : make -C [cible] est équivalent à cd ; make [cible] ; cd.. Ecrivez la règle compil du fichier 4_libc/Makefile. (Ce n'est pas dit dans le cours, mais la question contient la réponse...) 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 compil: make -C kernel compil make -C ulib compil make -C uapp compil Dernière modification le 27 nov. 2023 à 13:59:07)

Use Quizgecko on...
Browser
Browser