Changes between Initial Version and Version 1 of App-TP


Ignore:
Timestamp:
Feb 23, 2026, 11:17:48 PM (2 weeks ago)
Author:
franck
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • App-TP

    v1 v1  
     1[[PageOutline]]
     2{{{#!html
     3<h1> <font size="+2"> Application simple en mode utilisateur </font></h1>
     4}}}
     5
     6
     7Le TP est découpé en 4 étapes. Pour chaque étape, nous donnons :
     81. une brève description avec une liste des objectifs principaux de l'étape,
     92. une liste des fichiers avec un bref commentaire sur chaque fichier,
     103. une liste de questions simples dont les réponses sont dans le code, le cours ou le TD, 4
     114. un petit exercice de codage.
     12
     13
     14
     15**Récupération du code du tp2**
     16
     17
     18
     19* Ouvrez un `terminal`
     20* Allez dans le répertoire des TPs: **`cd ~/kO6`** \\
     21  ''... ou bien le répertoire que vous avez choisi pour faire les TPs.''
     22* Copiez les codes du **tp2**:\\
     23  **`cp -rp /Infos/lmd/2024/licence/ue/LU3IN029-2024oct/kO6/tp2 .`**
     24* Exécutez la commande : **`cd tp2 ; tree -L 1`**.\\
     25  Vous devriez obtenir ceci :
     26{{{#!c
     27.
     28├── 1_klibc
     29├── 2_appk
     30├── 3_syscalls
     31├── 4_libc
     32└── Makefile
     33}}}
     34
     35
     36**Objectif de la séance**
     37
     38
     39Les 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 de faire l'accès. C'est ce que nous allons voir.
     40
     41Vous allez suivre 4 étapes :
     42- **`1_klibc`**\\⟶ Ajout d'une librairie de fonctions standards pour le noyau (klibc), mais pas d'application utilisateur ;
     43- **`2_appk`**\\⟶ Appel d'une application utilisateur par la fonction d'initialisation `kinit()`, mais sans  gestionnaire des appels systèmes dans le noyau, il n'est donc pas possible d'utiliser `syscall` ;
     44- **`3_syscalls`**\\⟶ Ajout du gestionnaire des appels système et une application, mais **sans** la librairie de fonctions standards utilisateur (libc);
     45- **`4_libc`**\\⟶ Ajout d'une librairie de fonctions standards utilisateur minimaliste (libc) et d'une application.
     46
     47
     48Pour 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 et vous guidez, un peu, pour l'exercice. 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.
     49
     50Le code se trouve normalement dans `kO6/tp2/` (sinon téléchargez-le), ouvrez un terminal et allez-y. Dans ce répertoire, vous avez 4 sous-répertoires et un Makefile. Le fichier `kO6/tp2/Makefile` qui fait un appel récursif à `make` et qui permet de faire le ménage en appelant les Makefiles des sous-répertoires avec la cible `clean`.
     51
     52
     53
     54= 1. Ajout d'une bibliothèque de fonctions standards pour le kernel (klibc)
     55
     56
     57
     58**Objectifs de l'étape**
     59
     60Le 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` (générateur de nombres pseudo-aléatoires). Ces fonctions ne sont pas très originales, mais elles recèlent des subtilités que vous ne connaissez peut-être pas encore, vous pouvez les regarder par curiosité, même si ce n'est pas le but du TP. 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.
     61
     62
     63**Fichiers**
     64
     65
     66{{{#!c
     671_klibc/
     68├── kinit.c         : fichier contenant la fonction de démarrage du noyau
     69├── harch.h         : API du code dépendant de l'architecture
     70├── harch.c         : code dépendant de l'architecture du SoC
     71├── hcpu.h          : prototype de la fonction clock()
     72├── hcpua.S         : code dépendant du cpu matériel en assembleur
     73├── kernel.ld       : ldscript décrivant l'espace d'adressage pour l'éditeur de lien
     74├── klibc.h         : API de la klibc
     75├── klibc.c         : fonctions standards utilisées par les modules du noyau
     76└── Makefile        : description des actions possibles sur le code : compilation, exécution, nettoyage, etc.
     77}}}
     78
     79
     80**Questions**
     81
     82
     831. Ouvrez le fichier Makefile (vous pouvez regarder les dépendances en ouvrant quelques fichiers sources), puis dessiner le graphe de dépendance de `kernel.x` vis-à-vis de ses sources?\\La réponse peut-être visible avec la commande `dot -Tpng Makefile.dot -oMakefile.png` à partir du fichier  `Makefile.dot` reproduit ci-après ''(`dot` est un outil de [https://www.graphviz.org graphviz] ... essayez c'est magique :-)''  \\ \\`Makefile.dot`
     84{{{#!c
     85digraph G {
     86node [shape=box color=brown]
     87gcc1[label="gcc -c"];
     88gcc2[label="gcc -c"];
     89gcc3[label="gcc -c"];
     90gcc4[label="gcc -c"];
     91ld[label="ld"];
     92node [shape=ellipse color=blue]
     93"hcpua.S" , "hcpu.h"                          -> gcc1 -> "obj/hcpua.o" -> ld -> "kernel.x"
     94"kinit.c" , "klibc.h" , "harch.h" , "hcpu.h"  -> gcc2 -> "obj/kinit.o" -> ld
     95"klibc.c" , "klibc.h" , "harch.h" , "hcpu.h"  -> gcc3 -> "obj/klibc.o" -> ld
     96"harch.c" , "klibc.h" , "harch.h" , "hcpu.h"  -> gcc4 -> "obj/harch.o" -> ld
     97}
     98}}}
     99{{{#!protected ------------------------------------------------------------------------------------
     100''
     101{{{#!make
     102kernel.x : kernel.ld obj/hcpua.o obj/kinit.o obj/klibc.o obj/harch.o
     103obj/hcpua.o : hcpua.S hcpu.h
     104obj/kinit.o : kinit.c klibc.h harch.h hcpu.h
     105obj/klibc.o : klibc.c klibc.h harch.h hcpu.h
     106obj/harch.o : harch.c klibc.h harch.h hcpu.h
     107}}}
     108 `dot -Tpng Makefile.dot -oMakefile.png`
     109  [[Image(htdocs:img/Makefile.png, width=500, nolink)]]
     110''
     111}}}
     1121. Dans quel fichier se trouvent les codes dépendant du MIPS (donc écrits en assembleur) ?
     113{{{#!protected ------------------------------------------------------------------------------------
     114''
     115- Ils sont dans le fichier `hcpua.S`
     116''
     117}}}
     118
     119
     120**Exercices
     121
     122
     123* Dans un SoC, on peut avoir plusieurs processeurs. Le noyau a besoin de savoir sur quelle instance de processeur il s'exécute. Le numéro du processeur est dans les 12 bits de poids faible du registre du **coprocesseur-0 $15** (`c0_cpuid`) (à côté des registres `c0_epc`, `c0_sr`, etc. ce n'est pas le registre GPR $15). Ajoutez la fonction `int cpuid(void)` qui lit le registre `c0_cpuid` et qui rend un entier contenant juste les 12 bits de poids faible.\\Vous pouvez vous inspirez fortement de la fonction `int clock(void)`. Comme il n'y a qu'un seul processeur dans cette architecture, `cpuid` rend toujours `0`.\\Ecrivez un programme de test (vous devrez modifier les fichiers `hcpu.h` pour y mettre le prototype de la fonction, `hcpua.S` pour y mettre le code et `kinit.c` pour appeler la fonction)
     124
     125{{{#!protected
     126**hcpua.S**
     127{{{#!asm
     128.globl cpuid
     129cpuid:
     130    mfc0    $2, $15
     131    andi    $2, $2, 0xFFF  // masque pour ne conserver que les 12 bits de poids faible
     132    jr      $31
     133}}}
     134**hcpu.h**
     135{{{#!c
     136/**
     137 * \brief     cpu identifier
     138 * \return    a number
     139 */
     140extern unsigned cpuid (void);
     141}}}
     142}}}
     143
     144
     145
     146= 2. Programme utilisateur mais exécuté en mode kernel
     147
     148
     149
     150**Objectifs de l'étape**
     151
     152Nous 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 même que celle-ci n'est pas compilée en même temps que le noyau. Nous allons passer du noyau à l'application à la fin de la fonction `kinit()`.
     153
     154Nous allons donc entrer dans l'application, en revanche, dans cette étape, nous n'allons pas mettre en place la gestion des syscalls. **C'est-à-dire qu'il ne sera pas possible de revenir dans le noyau depuis l'application**. C'est bien entendu une étape intermédiaire, parce qu'il faudra absolument pouvoir invoquer le noyau depuis l'application pour accéder aux périphériques.\\**Pour pouvoir quand même accéder aux registres de périphériques, nous allons EXCEPTIONNELLEMENT exécuter l'application en mode kernel. Ainsi, l'application pourra accéder aux adresses de l'espace d'adressage réservées au mode `kernel`.**
     155
     156Nous avons deux exécutables à compiler et donc deux `Makefile`s de compilation. Nous avons aussi un `Makefile` qui invoque récursivement les `Makefile`s de compilation.
     157
     158
     159**Fichiers**
     160
     161
     162{{{#!xml
     1632_appk/
     164├── Makefile        : Makefile racine qui invoque les Makefiles des sous-répertoires et qui exécute
     165├── kernel ────────── Répertoire des fichiers composant le kernel
     166│   ├── kinit.c     : fichier contenant la fonction de démarrage du noyau
     167│   ├── harch.h     : API du code dépendant de l'architecture
     168│   ├── harch.c     : code dépendant de l'architecture du SoC
     169│   ├── hcpu.h      : prototype de la fonction clock()
     170│   ├── hcpua.S     : code dépendant du cpu matériel en assembleur
     171│   ├── klibc.h     : API de la klibc
     172│   ├── klibc.c     : fonctions standards utilisées par les modules du noyau
     173│   ├── kernel.ld   : ldscript décrivant l'espace d'adressage pour l'édition de liens du kernel
     174│   └── Makefile    : description des actions possibles sur le code kernel : compilation et nettoyage
     175└── user ──────────── Répertoire des fichiers composant l'application user
     176    ├── crt0.c      : fonctions d'interface entre kernel et user, pour le moment : _start()
     177    ├── main.c      : fonction principale de l'application
     178    ├── user.ld     : ldscript décrivant l'espace d'adressage pour l'édition de liens du user
     179    └── Makefile    : description des actions possibles sur le code user : compilation et nettoyage
     180}}}
     181
     182
     183**Questions**
     184
     185
     1861. Combien de fichiers de type ldscript avons-nous en tout ?
     187{{{#!protected ------------------------------------------------------------------------------------
     188''
     189- Il en faut deux désormais, un pour le kernel `kernel/kernel.ld` et un pour l'application `user/user.ld`
     190''
     191}}}
     1921. Dans quel fichier se trouve la première fonction de l'application et comment s'appelle-t-elle?
     193{{{#!protected ------------------------------------------------------------------------------------
     194''
     195- Dans le fichier `user/crt0.c`, c'est la fonction `_start()`.
     196''
     197}}}
     1981. Quelle est la fonction du noyau qui appelle cette fonction et dans quel fichier?
     199{{{#!protected ------------------------------------------------------------------------------------
     200''
     201- C'est la fonction `kinit()`, dans le fichier `kernel/kinit.c`.
     202''
     203}}}
     2041. Comment le noyau fait-il pour démarrer l'application en mode `kernel`? (la réponse est dans la fonction de la question précédente).
     205{{{#!protected ------------------------------------------------------------------------------------
     206''
     207- Dans la fonction `kinit()`, on appelle `app_load(&_start)`, c'est une fonction écrite en assembleur, donc forcément dans `hcpua.S`. Dans cette fonction on initialise `c0_epc` avec l'adresse de `_start()`, on initialise le bit `UM` du registre status `c0_sr` avec `0`. Ainsi, après l'exécution de `eret`, nous serons en mode `kernel`.
     208{{{#!c
     209.globl app_load // ----------------- void app_load (void * fun) called by kinit()
     210app_load:                         // call when we exit kinit() function to go to user code
     211
     212    mtc0   $4,      $14           // put _start address in c0_EPC
     213    li     $26,     2             // define next status reg. value
     214    mtc0   $26,     $12           // UM <- 0, IE <- 0, EXL <- 1
     215    eret                          // j EPC and EXL <- 0
     216}}}
     217''
     218}}}
     219
     220
     221**Exercice**
     222
     223
     224- Vous n'allez pas faire grand-chose pour cette étape parce qu'il est impossible de revenir dans le noyau après l'entrée dans l'application... tant qu'il n'y a pas le gestionnaire de syscalls dans le noyau. En conséquence, affichez juste un second message depuis la fonction `main()`.
     225
     226
     227
     228= 3. Programme utilisateur utilisé en mode user mais sans libc
     229
     230
     231
     232**Objectifs de l'étape**
     233
     234
     235Le 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 analyser la cause 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.
     236
     237Le passage de l'application au noyau par le biais de l'instruction `syscall` impose que les numéros de services soient identiques pour le noyau et pour l'application. Ces numéros de service (comme `SYSCALL_TTY_PUTS`, `SYSCALL_EXIT` sont définis dans le fichier `syscall.h` communs au noyau et à l'application. Ce fichier est mis dans un répertoire à part nommé `common`. Il n'y a qu'un seul fichier ici, mais dans un système plus élaboré, il y en a d'autres.
     238
     239
     240**Fichiers**
     241
     242
     243{{{#!xml
     2443_syscalls/
     245├── Makefile        : Makefile racine qui invoque les Makefiles des sous-répertoires et qui exécute
     246├── common ────────── répertoire des fichiers commun kernel / user
     247│   └── syscalls.h  : API la fonction syscall et des codes de syscalls
     248├── kernel ────────── Répertoire des fichiers composant le kernel
     249│   ├── kinit.c     : fichier contenant la fonction de démarrage du noyau
     250│   ├── harch.h     : API du code dépendant de l'architecture
     251│   ├── harch.c     : code dépendant de l'architecture du SoC
     252│   ├── hcpu.h      : prototype de la fonction clock()
     253│   ├── hcpua.S     : code dépendant du cpu matériel en assembleur
     254│   ├── hcpuc.c     : code dépendant du cpu matériel en c
     255│   ├── klibc.h     : API de la klibc
     256│   ├── klibc.c     : fonctions standards utilisées par les modules du noyau
     257│   ├── kpanic.h    : déclaration du tableau de dump des registres en cas d'exception
     258│   ├── kpanic.c    : fonction d'affichage des registres avant l'arrêt du programme
     259│   ├── ksyscalls.c : Vecteurs des syscalls
     260│   ├── kernel.ld   : ldscript décrivant l'espace d'adressage pour l'édition de liens du kernel
     261│   └── Makefile    : description des actions possibles sur le code kernel : compilation et nettoyage
     262└── user ──────────── Répertoire des fichiers composant l'application user
     263    ├── crt0.c      : fonctions d'interface entre kernel et user, pour le moment : _start()
     264    ├── main.c      : fonction principale de l'application
     265    ├── user.ld     : ldscript décrivant l'espace d'adressage pour l'édition de liens du user
     266    └── Makefile    : description des actions possibles sur le code user : compilation et nettoyage
     267}}}
     268
     269
     270**Questions**
     271
     272
     2731. Dans quel fichier se trouve la définition des numéros de services tels que `SYSCALL_EXIT` ?\\(''Ces numéros sont communs au noyau et à l'application, alors ils ont été mis dans un répertoire commun aux deux exécutables.'')
     274{{{#!protected ------------------------------------------------------------------------------------
     275''
     276- Ils sont dans le fichier `common/syscall.h`. C'est dit dans l'énoncé !
     277''
     278}}}
     2791. Dans quel fichier se trouve le vecteur de syscall, c'est-à-dire le tableau `syscall_vector[]` contenant les pointeurs sur les fonctions qui réalisent les services correspondants aux syscall ? C'est un tableau indexé par les numéros de syscall qui permet de connaitre la fonction à exécuter pour chaque numéro de service.
     280{{{#!protected ------------------------------------------------------------------------------------
     281''
     282- Il est dans le fichier `kernel/ksyscall.c`.
     283''
     284}}}
     2851. Dans quel fichier se trouve le gestionnaire de syscalls ? (''c'est de l'assembleur'') et pourquoi est-ce de l'assembleur ?
     286{{{#!protected ------------------------------------------------------------------------------------
     287''
     288- Il est dans le fichier `kernel/hcpua.S`.
     289- Le code du gestionnaire est totalement spécifique au MIPS, on ne peut pas l'écrire en C.
     290''
     291}}}
     292
     293
     294**Exercice**
     295
     296
     297- Vous allez ajouter un appel système nommé `SYSCALL_CPUID` qui rend le numéro du processeur. Nous allons lui attribuer le numéro 4 (notez que ces numéros de services n'ont rien à voir avec les numéros utilisés pour le simulateur MARS). Pour ajouter un appel système, vous devez modifier les fichiers : 
     298  - `kernel/hcpua.S` : pour ajouter `cpuid(void)` déjà écrite à l'étape précédente;
     299  - `kernel/hcpu.h` : pour le prototype de la fonction `cpuid() (afin que gcc la connaisse pour la compilation de `kernel/ksyscall.c`;
     300  - `common/syscalls.h` : pour ajouter le numéro de syscall `SYSCALL_CPUID`;
     301  - `kernel/ksyscall.c` : pour ajouter la fonction `cpuid()` dans le vecteur de syscall.
     302- Modifiez aussi la fonction `main()` pour utiliser votre nouvel appel système. Vous pouvez utiliser le mode debug pour voir dans la trace d'exécution l'appel à
     303
     304
     305
     306= 4.  Ajout de la librairie C pour l'utilisateur
     307
     308
     309
     310**Objectifs de l'étape**
     311
     312
     313L'application utilisateur n'est pas censée utiliser directement les appels système. Elle utilise une librairie de fonctions standards (la `libc` POSIX, mais pas uniquement) et ce sont ces fonctions qui réalisent les appels système. Toutes les fonctions de la `libc` n'utilisent pas les appels système. Par exemple, les fonctions `int rand(void)` ou `int strlen(char *)` (rendent, respectivement, un nombre pseudo aléatoire et la longueur d'une chaîne de caractères) n'ont pas besoin du noyau. Les librairies font partie du système d'exploitation mais elles ne sont pas dans le noyau.
     314
     315''Le terme « librairie » vient de l'anglais « library » qui signifie bibliothèque. On utilise souvent le mot librairie même si le sens en français n'est pas le même que celui en anglais. Disons que, dans notre contexte, les deux mots sont synonymes.''
     316
     317Normalement, les librairies système sont des « vraies » librairies au sens `gcc` du terme. C'est-à-dire des archives de fichiers objet (`.o`). Ici, nous allons simplifier et ne pas créer une ''vraie'' librairie, mais seulement un fichier objet `libc.o` contenant toutes les fonctions. Ce fichier objet doit être lié avec le code de l'application.
     318
     319L'exécutable de l'application utilisateur est donc composé de deux parties : d'un côté, le code de l'application et, de l'autre, le code de la librairie `libc` (+ `crt0`). Nous allons répartir le code dans deux répertoires:
     320- `uapp` pour les fichiers de l'application utilisateur
     321- `ulib` pour les fichiers qui ne sont pas l'application, c'est-à-dire la `libc`, le fichier `crt0.c` mais aussi le fichier ldscript `user.ld`.
     322
     323 
     324**Fichiers**
     325
     326
     327{{{#!xml
     3284_libc/
     329├── Makefile        : Makefile racine qui invoque les Makefiles des sous-répertoires et qui exécute
     330├── common ────────── répertoire des fichiers commun kernel / user
     331│   └── syscalls.h  : API la fonction syscall et des codes de syscalls
     332├── kernel ────────── Répertoire des fichiers composant le kernel
     333│   ├── kinit.c     : fichier contenant la fonction de démarrage du noyau
     334│   ├── harch.h     : API du code dépendant de l'architecture
     335│   ├── harch.c     : code dépendant de l'architecture du SoC
     336│   ├── hcpu.h      : prototype de la fonction clock()
     337│   ├── hcpua.S     : code dépendant du cpu matériel en assembleur
     338│   ├── hcpuc.c     : code dépendant du cpu matériel en c
     339│   ├── klibc.h     : API de la klibc
     340│   ├── klibc.c     : fonctions standards utilisées par les modules du noyau
     341│   ├── ksyscalls.c : Vecteurs des syscalls
     342│   ├── kernel.ld   : ldscript décrivant l'espace d'adressage pour l'édition de liens du kernel
     343│   └── Makefile    : description des actions possibles sur le code kernel : compilation et nettoyage
     344├── uapp ──────────── Répertoire des fichiers de l'application user seule
     345│   ├── main.c      : fonction principale de l'application
     346│   └── Makefile    : description des actions possibles sur le code user : compilation et nettoyage
     347└── ulib ──────────── Répertoire des fichiers des bibliothèques système liés avec l'application user
     348    ├── crt0.c      : fonctions d'interface entre kernel et user, pour le moment : _start()
     349    ├── libc.h      : API pseudo-POSIX de la bibliothèque C
     350    ├── libc.c      : code source de la libc
     351    ├── user.ld     : ldscript décrivant l'espace d'adressage pour l'édition de liens du user
     352    └── Makefile    : description des actions possibles sur le code user : compilation et nettoyage
     353}}}
     354
     355
     356**Questions**
     357
     358
     3591. Pour ce petit système, dans quel fichier sont placés tous les prototypes des fonctions de la libc? Est-ce ainsi pour POSIX sur LINUX?
     360{{{#!protected ------------------------------------------------------------------------------------
     361''
     362- Ils sont tous dans le fichier `libc.h`.
     363- Non, pour POSIX, les prototypes de fonctions de la libc sont répartis dans plusieurs fichiers suivant leur rôle. Il y a `stdio.h`, `string.h`, `stdlib.h`, etc. Nous n'avons pas voulu ajouter cette complexité.
     364''
     365}}}
     366
     367
     368**Exercice**
     369
     370
     371- Vous allez juste ajouter la fonction `int cpuid()` dans la librairie `libc`.
     372- Au premier TP, vous deviez créer un petit jeu 'guess', vous pouvez en faire une application utilisateur, en utilisant cette fois les fonctions de la `libc`.