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 |