Changes between Version 48 and Version 49 of AS6-TME-B1


Ignore:
Timestamp:
Feb 2, 2023, 2:05:41 AM (17 months ago)
Author:
franck
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AS6-TME-B1

    v48 v49  
    4747}}}
    4848
    49 Ce TME est sans doute le plus chargé parce qu'il porte sur le tout démarrage du système et il va jusqu'à l'exécution d'une application utilisant des services du noyau grâce aux syscalls. Le code reste néanmoins petit parce que le nombre de services est faible. Il y a également des questions sur la chaine de compilation, mais là encore la complexité est raisonnable.
    50 
    51 Si vous avez des difficultés, tant sur le code que sur les outils, il est important de relire le cours, de poser des questions, de faire des recherches sur internet, etc. puisque vous aurez besoin d'avoir compris ces bases pour aller plus loin. Soyez proactif, vous seul savez ce que vous ne comprenez pas 🙂.
     49Ce TME est sans doute le plus chargé de tous les TME du module parce qu'il porte sur le tout démarrage du système et il va jusqu'à l'exécution d'une application utilisant des services du noyau grâce aux syscalls. Le code reste néanmoins petit parce que le nombre de services est faible. Il y a également des questions sur la chaine de compilation, mais là encore la complexité est raisonnable.
     50
     51Si vous avez des difficultés, tant sur le code que sur les outils, il est important de relire le cours, de poser des questions, de faire des recherches sur internet, etc. puisque vous aurez besoin d'avoir compris ces bases pour aller plus loin. Soyez proactif, vous seul savez ce que vous ne comprenez pas. 🙂
    5252
    5353
     
    11891189{{{#!protected ------------------------------------------------------------------------------------
    11901190'''''''''''''''
    1191 * Non, ce n'est pas un problème puisque ça fonctionne. Le code de boot a besoin de l'adresse de `kinit()` mais on l'obtient avec la macro `la` . C'est l'éditeur de lien qui fera en sorte que dans les code binaire l'adresse de `kinit()` mise dans le registre `$26` soit la bonne. Notez que cela fonctionne parce qu'on fait l'édition de lien du noyau et du code de boot pour fabrique un seul binaire (`kernel.x`), ce qui n'est pas le cas dans un vrai système, savez-vous pourquoi ?
    1192 '''''''''''''''
    1193 }}}
    1194 
    1195 **Exercices**
    1196 
    1197 - Ecrire une fonction `void Capitalize(void)` appelée par la fonction `kinit()` qui lit une phrase terminée par un `\n` et la réécrit en ayant mis en majuscule la première lettre de chaque mot. Vous mettrez cette nouvelle fonction dans le fichier `kinit.c` (ce ne devrait pas être sa place mais c'est juste un exercice). Notez que vous ne pouvez pas utiliser la fonction [http://manpagesfr.free.fr/man/man3/toupper.3.html `toupper()`] parce que c'est une fonction de la `glibc` (la bibliothèque de la librairie de fonctions standards) et que là vous ne l'avez pas. Vous n'êtes pas sur Linux :-)
    1198 {{{#!protected ------------------------------------------------------------------------------------
    1199 '''''''''''''
    1200 - Dans cette fonction `void Capitalize(void)`, pour chaque caractère lu au clavier, vous devez tester s'il est compris entre 'a' et 'z' et si oui, ajouter ('A' - 'a') ...
    1201 '''''''''''''
    1202 }}}
    1203 
    1204 
    1205 == B3. Ajout d'une bibliothèque de fonctions standards pour le kernel (klibc)
    1206 
    1207 
    1208 
    1209 **Objectifs de l'étape**
    1210 
    1211 Le 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`. 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é. 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.
    1212 
    1213 
    1214 **Fichiers**
    1215 
    1216 
    1217 {{{#!xml
    1218 03_klibc/
    1219 ├── kinit.c         : fichier contenant la fonction de démarrage du noyau
    1220 ├── harch.h         : API du code dépendant de l'architecture
    1221 ├── harch.c         : code dépendant de l'architecture du SoC
    1222 ├── hcpu.h          : prototype de la fonction clock()
    1223 ├── hcpua.S         : code dépendant du cpu matériel en assembleur
    1224 ├── kernel.ld       : ldscript décrivant l'espace d'adressage pour l'éditeur de lien
    1225 ├── klibc.h         : API de la klibc
    1226 ├── klibc.c         : fonctions standards utilisées par les modules du noyau
    1227 └── Makefile        : description des actions possibles sur le code : compilation, exécution, nettoyage, etc.
    1228 }}}
    1229 
    1230 
    1231 **Questions**
    1232 
    1233 
    1234 1. Ouvrez le fichier Makefile, En ouvrant tous les fichiers dessiner le graphe de dépendance de `kernel.x` vis-à-vis de ses sources?
    1235 {{{#!make
    1236 kernel.x : kernel.ld obj/hcpua.o obj/kinit.o obj/klibc.o obj/harch.o
    1237 obj/hcpua.o : hcpua.S hcpu.h
    1238 obj/kinit.o : kinit.c klibc.h harch.h hcpu.h
    1239 obj/klibc.o : klibc.c klibc.h harch.h hcpu.h
    1240 obj/harch.o : harch.c klibc.h harch.h hcpu.h
    1241 }}}
    1242 {{{#!protected ------------------------------------------------------------------------------------
    1243 '''''''''''''''
    1244   [[Image(htdocs:img/Makefile.png, width=500, nolink)]]
    1245 Ce fichier est produit par [https://graphviz.org/ Graphviz] à partir du fichier `makefile.dot` et la commande `dot -T png Makefile.dot -oMakefile.png`
    1246  {{{
    1247 digraph G {
    1248 node [shape=box color=brown]
    1249 gcc1[label="gcc -c"];
    1250 gcc2[label="gcc -c"];
    1251 gcc3[label="gcc -c"];
    1252 gcc4[label="gcc -c"];
    1253 ld[label="ld"];
    1254 node [shape=ellipse color=blue]
    1255 "hcpua.S" , "hcpu.h"                          -> gcc1 -> "obj/hcpua.o" -> ld -> "kernel.x"
    1256 "kinit.c" , "klibc.h" , "harch.h" , "hcpu.h"  -> gcc2 -> "obj/kinit.o" -> ld
    1257 "klibc.c" , "klibc.h" , "harch.h" , "hcpu.h"  -> gcc3 -> "obj/klibc.o" -> ld
    1258 "harch.c" , "klibc.h" , "harch.h" , "hcpu.h"  -> gcc4 -> "obj/harch.o" -> ld
    1259 }}}
    1260 '''''''''''''''
    1261 }}}
    1262 
    1263 1. Dans quel fichier se trouvent les codes dépendant du MIPS ?
    1264 {{{#!protected ------------------------------------------------------------------------------------
    1265 '''''''''''''''
    1266 - Ils sont dans le fichier `hcpua.S`
    1267 '''''''''''''''
    1268 }}}
    1269 
    1270 
    1271 **Exercices
    1272 
    1273 
    1274 * Le numéro du processeur est dans les 12 bits de poids faible du registre $15 (`c0_cpuid`) du coprocesseur système (à côté des registres `c0_epc`, `c0_sr`, etc.). 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`, `hcpua.S` et `kinit.c`)
    1275 {{{#!protected
    1276 **hcpua.S**
    1277 {{{#!asm
    1278 .globl cpuid
    1279 cpuid:
    1280     mfc0    $2, $15
    1281     andi    $2, $2, 0xFFF
    1282     jr      $31
    1283 }}}
    1284 **hcpu.h**
    1285 {{{#!c
    1286 /**
    1287  * \brief     cpu identifier
    1288  * \return    a number
    1289  */
    1290 extern unsigned cpuid (void);
    1291 }}}
    1292 }}}
    1293 
    1294 
    1295 
    1296 == B4.  Ajout de la librairie C pour l'utilisateur
    1297 
    1298 
    1299 
    1300 **Objectifs de l'étape**
    1301 
    1302 
    1303 L'application utilisateur n'est pas censée utiliser directement les appels système. Elle utilise une librairie de fonctions standards (la `libc` POSIX, mais également d'autres) 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 pseudoalé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.
    1304 
    1305  ''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.''
    1306 
    1307 Normalement, 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 objets doit être lié avec le code de l'application.
    1308 
    1309 L'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 `uapp` pour les fichiers de l'application et `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`.
    1310 
    1311 On rappelle que le fichier `crt0.c` contient le code d'entrée dans l'application avec la fonction `_start()` appelée par la fonction `kinit()`. C'est aussi, dans ce fichier que l'on met le code assembleur de la fonction `syscall_fct()` permettant de revenir dans le noyau. En conséquence, `crt0.c`, c'est le pont entre le noyau et l'application.
    1312 
    1313  
    1314 **Fichiers**
    1315 
    1316 
    1317 {{{#!xml
    1318 04_libc/
    1319 ├── Makefile        : Makefile racine qui invoque les Makefiles des sous-répertoires et qui exécute
    1320 ├── common ────────── répertoire des fichiers commun kernel / user
    1321 │   └── syscalls.h  : API la fonction syscall et des codes de syscalls
    1322 ├── kernel ────────── Répertoire des fichiers composant le kernel
    1323 │   ├── kinit.c     : fichier contenant la fonction de démarrage du noyau
    1324 │   ├── harch.h     : API du code dépendant de l'architecture
    1325 │   ├── harch.c     : code dépendant de l'architecture du SoC
    1326 │   ├── hcpu.h      : prototype de la fonction clock()
    1327 │   ├── hcpua.S     : code dépendant du cpu matériel en assembleur
    1328 │   ├── hcpuc.c     : code dépendant du cpu matériel en c
    1329 │   ├── klibc.h     : API de la klibc
    1330 │   ├── klibc.c     : fonctions standards utilisées par les modules du noyau
    1331 │   ├── ksyscalls.c : Vecteurs des syscalls
    1332 │   ├── kernel.ld   : ldscript décrivant l'espace d'adressage pour l'édition de liens du kernel
    1333 │   └── Makefile    : description des actions possibles sur le code kernel : compilation et nettoyage
    1334 ├── uapp ──────────── Répertoire des fichiers de l'application user seule
    1335 │   ├── main.c      : fonction principale de l'application
    1336 │   └── Makefile    : description des actions possibles sur le code user : compilation et nettoyage
    1337 └── ulib ──────────── Répertoire des fichiers des bibliothèques système liés avec l'application user
    1338     ├── crt0.c      : fonctions d'interface entre kernel et user, pour le moment : _start()
    1339     ├── libc.h      : API pseudo-POSIX de la bibliothèque C
    1340     ├── libc.c      : code source de la libc
    1341     ├── user.ld     : ldscript décrivant l'espace d'adressage pour l'édition de liens du user
    1342     └── Makefile    : description des actions possibles sur le code user : compilation et nettoyage04_libc/
    1343 
    1344 }}}
    1345 
    1346 **Questions**
    1347 
    1348 1. 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?
    1349 {{{#!protected ------------------------------------------------------------------------------------
    1350 '''''''''''''''
    1351 - Ils sont tous dans le fichier `libc.h`.
    1352 - Non, pour POSIX, les prototypes de fonctions de la libc sont répart:q:qis dans plusieurs fichiers suivant leur rôle. Il y `stdio.h`, `string.h`, `stdlib.h`, etc. Nous n'avons pas voulu ajouter cette complexité.
    1353 '''''''''''''''
    1354 }}}
    1355 1. Dans quel fichier se trouve la définition des numéros de services tels que `SYSCALL_EXIT` ?
    1356 {{{#!protected ------------------------------------------------------------------------------------
    1357 '''''''''''''''
    1358 - Ils sont dans le fichier `common/syscall.h`.
    1359 '''''''''''''''
    1360 }}}
    1361 1. 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 ?
    1362 {{{#!protected ------------------------------------------------------------------------------------
    1363 '''''''''''''''
    1364 - Il est dans le fichier `kernel/ksyscall.c`.
    1365 '''''''''''''''
    1366 }}}
    1367 1. Dans quel fichier se trouve le gestionnaire de syscalls ?
    1368 {{{#!protected ------------------------------------------------------------------------------------
    1369 '''''''''''''''
    1370 - Il est dans le fichier `kernel/hcpua.S`.
    1371 '''''''''''''''
    1372 }}}
    1373 
    1374 
    1375 **Exercice**
    1376 
    1377 
    1378 Pour finir ce TME (un peu long 🤪 ), vous allez juste ajouter une boucle d'affichage des caractères ASCII au début de la fonction `main()` en utilisant la fonction de la libc `fputc(tty,c)` (avec `tty` à 0 pour un affichage sur le terminal `0`, et `c` la variable contenant le caractère à afficher, qui prendra toutes les valeurs entre 32 et 127.
    1379 
    1380 Je vous donne le code dans le corrigé, mais ça fait seulement 2 lignes, alors je pense que vous n'en aurez pas besoin ! 🙂
    1381 {{{#!protected ------------------------------------------------------------------------------------
    1382 '''''''''''''''
    1383 {{{#!c
    1384 for (int c = 32; c < 127; c++) {
    1385     fputc (0, c);
    1386 }
    1387 }}}
    1388 '''''''''''''''
    1389 }}}
    1390 - Ensuite, quand ça marche, exécutez le programme en mode débug (`make debug` au lieu de `make exec`) et ouvrez le fichier `trace0.s`. A quel cycle, commence la fonction `main()` ?
    1391 {{{#!protected ------------------------------------------------------------------------------------
    1392 '''''''''''''''
    1393 - 7351 (sur ma machine, peut-être que cela peut varier si, entre temps, j'ai mis une autre version du simulateur)
    1394 '''''''''''''''
    1395 }}}
    1396 - Recompilez le kernel en utilisant le mode `-O0`(lettre 0 suivie du chiffre zéro), réexécutez l'application en mode debug et regardez à nouveau à quelle cycle commence la fonction `main()` ?
    1397 {{{#!protected ------------------------------------------------------------------------------------
    1398 '''''''''''''''
    1399 - 20603 (sur ma machine) c'est presque 3 fois plus lent !!!
    1400 '''''''''''''''
    1401 }}}
    1402 - Pour finir, recompilez à nouveau le noyau en utilisant le mode `-O3`, réexécutez encore l'application en mode debug et regardez combien de cycles sont nécessaires pour exécuter la fonction `fputc()`. Pour ça, vous ouvrez le fichier `trace0.s`, vous cherchez le premier appel de `fputc()` (vous notez le cycle) et vous cherchez l'instruction `eret` qui marque la sortie du kernel (vous notez le cycle) et vous faites la différence ? Profitez en pour voir l'entrée dans le kernel, l'analyse de la cause, l'utilisation du vecteur de syscall, etc.
    1403 {{{#!protected ------------------------------------------------------------------------------------
    1404 '''''''''''''''
    1405 - 7391 : appel de `fputc()`
    1406 - 7589 : exéction de `eret`
    1407 - → 7589 - 7391 = 198 cycles (pour afficher 1 caractère !)
    1408 '''''''''''''''
    1409 }}}
    1410 - Refaites le calcul pour le deuxième appel de `fputc()`, que constatez-vous ? Avez-vous une explication  ?
    1411 {{{#!protected ------------------------------------------------------------------------------------
    1412 '''''''''''''''
    1413 - 7620 : appel de `fputc()`
    1414 - 7686 : exéction de `eret`
    1415 - → 7686 - 7620 = 66 cycles, c'est plus rapide, c'est à cause des caches que nous verrons plus tard !
    1416 '''''''''''''''
    1417 }}}
    1418 
    1419 
    1420 {{{#!comment
    1421 
    1422 ----------- TROP COMPLEXE !!!
    1423 
    1424 Vous allez utiliser le composant DMA. En cours, nous avons vu que le DMA réalise un copie de mémoire à partir de l'adresse contenu dans le registre DMA_SRC vers l'adresse contenu dans le registre DMA_DST du nombre d'octet contenu dans le registre DMA_LEN.
    1425 Dans l'ordre, on commence par écrire les adresses SRC, DST et IRQ_DISABLE (si besoin), puis on écrit LEN, ce qui provoque le démarrage de la copie par le DMA
    1426 A la fin de l'opération, le registre LEN contient le nombre d'octet non écrits donc 0.
    1427 
    1428 Les registres du DMA sont à l'adresse 0xd1200000
    1429 {{{
    1430 DMA_IRQ_DISABLE         (lecture/écriture)      masquage de la ligne IRQ
    1431 DMA_RESET               (écriture seule)        acquittement de la ligne IRQ 
    1432 DMA_LEN                 (écriture/lecture)      taille en octets à déplacer
    1433 DMA_DST                 (écriture seule)        adresse de destination
    1434 DMA_SRC                 (écriture seule)        adresse source
    1435 }}}
    1436  
    1437 - Vous allez ajouter un appel système nommé `SYSCALL_DMA_MEMCPY` qui réalise la copie d'une zone de la mémoire. Pour cela, vous devez:
    1438   - Ajouter une fonction dans la libc que vous appellerez `dma_memcpy(int *dst, int *src, unsigned len)` qui a un comportement équivalent à `memcpy()` mais pour déplacer des tableaux d'entiers. Vous devez modifier les fichiers `ulib/libc.h` et `ulib/libc.c`.
    1439   - Ajouter la déclaration de `SYSCALL_DMA_MEMCPY` dans le fichier `commun/syscall.h`.
    1440   - Ajouter une entrée dans le tableau `syscall_vector[]` dans le fichier `kernel/ksyscalls.h` pour la fonction `dma_memcpy()`.
    1441   - Ajouter la déclaration d'une variable `__dma_regs_map` dans le fichier `kernel/kernel.ld`.
    1442   - Ajouter une fonction `dma_memcpy()`, vous devez modifier les fichiers `kernel/harch.h` et `kernel/harch.c`
    1443 
    1444 Idéalement, il faudrait vérifier que les adresses utilisées sont uniquement dans la partie de l'espace d'adressage autorisée pour l'utilisateur. Cette vérification serait faite dans une fonction intermédiaire dans `kernel/ksyscalls.c` qui ferait les vérifications et qui appellerait `dma_memcpy()` si les adresses sont correctes. Je vous laisse y réfléchir.
    1445 
    1446 {{{#!protected
    1447 Ajouter le FB et écrire un damier qui s'inverse.
    1448 }}}
     1191* Non, ce n'est pas un problème puisque ça fonctionne. Le code de boot a besoin de l'adresse de `k