142 | | |
143 | | = 2. Langage C pour la programmation système |
144 | | |
145 | | |
146 | | |
147 | | |
148 | | La programmation en C, vous connaissez, mais quand on programme pour le noyau, c'est un peu différent. |
149 | | Il y a des éléments de syntaxe ou des besoins spécifiques. Pour répondre aux questions, vous devez avoir lu les transparents 33 à 53 du cours 10, dans lesquels une séquence complète de code (du boot à exit) est détaillée. |
150 | | |
151 | | |
152 | | **Questions** |
153 | | |
154 | | |
155 | | 1. En assembleur, vous utilisez les sections prédéfinies `.data` et `.text` pour placer respectivement les data et le code, mais vous pouvez créer vos propres sections avec la directive `.section` (nous avons utilisé cette possibilité pour la section `.boot`). Il est aussi possible d'imposer ou de créer des sections en langage C avec la directive `__attribute__((section("section-name")))`. La directive du C `__attribute__` permet de demander certains comportements au compilateur. Ici, c'est la création d'une section, mais il y a beaucoup d'attributs possibles (si cela vous intéresse vous pouvez regarder dans la [https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html doc de GCC sur les attributs]. Comment créer la section `.start` en C ? |
156 | | {{{#!protected ------------------------------------------------------------------------------------ |
157 | | '' |
158 | | Cours 10 / slide 38 |
159 | | - `__attribute__ ((section (".start")))`\\La syntaxe est un peu curieuse avec les doubles underscore et les doubles parenthèses. |
160 | | '' |
161 | | }}} |
162 | | 1. En C, vous savez que les variables globales sont toujours initialisées, soit explicitement dans le programme lui-même, soit implicitement à la valeur `0`. Les variables globales initialisées sont placées dans la section `.data` (ou plutôt dans l'une des sections `data` : `.data`, `.sdata`, `.rodata`, etc.) et elles sont présentes dans le fichier objet (`.o`) produit pas le compilateur. En revanche, les variables globales non explicitement initialisées ne sont pas présentes dans le fichier objet. Ces dernières sont placées dans un segment de la famille [https://www.wikiwand.com/fr/Segment_BSS `.bss`]. Le fichier ldscript permet de mapper l'ensemble des segments en mémoire. Pour pouvoir initialiser à `0` les segments `bss` par programme, il nous faut connaître les adresses de début et de fin où ils sont placés en mémoire.\\ \\Le code ci-dessous est le fichier ldscript du kernel `kernel.ld` (nous avons retiré les commentaires mais ils sont dans les fichiers).\\Expliquez ce que font les lignes 11, 12 et 15. |
163 | | {{{#!java |
164 | | 1 SECTIONS |
165 | | 2 { |
166 | | 3 .boot : { |
167 | | 4 *(.boot) |
168 | | 5 } > boot_region |
169 | | 6 .ktext : { |
170 | | 7 *(.text*) |
171 | | 8 } > ktext_region |
172 | | 9 .kdata : { |
173 | | 10 *(.*data*) |
174 | | 11 . = ALIGN(4); |
175 | | 12 __bss_origin = .; |
176 | | 13 *(.*bss*) |
177 | | 14 . = ALIGN(4); |
178 | | 15 __bss_end = .; |
179 | | 16 } > kdata_region |
180 | | 17 } |
181 | | }}} |
182 | | {{{#!protected ------------------------------------------------------------------------------------ |
183 | | '' |
184 | | Cours 10 / slide 28 |
185 | | - La ligne 11 contient `. = ALIGN(4)`, c'est équivalent à la directive `.align 4` de l'assembleur. |
186 | | Cela permet de déplacer le pointeur de remplissage de la section de sortie courante (c'est-à-dire ici `.kdata`) sur une |
187 | | frontière de 2^4^ octets (une adresse multiple de 16). Cette contrainte est liée aux caches que nous ne verrons pas ici. |
188 | | - La ligne 12 permet de créer la variable de ldscript `__bss_origin` et de l'initialiser à l'adresse courante, |
189 | | ce sera donc l'adresse de début de la zone `bss`. |
190 | | - La ligne 15 permet de créer la variable `__bss_end` qui sera l'adresse de fin de la zone `bss` |
191 | | (en fait c'est la première adresse qui suit juste `bss`. |
192 | | '' |
193 | | }}} |
194 | | 1. 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`: |
195 | | {{{#!c |
196 | | __tty_regs_map = 0xd0200000 ; /* tty's registers map, described in devices.h */ |
197 | | }}} |
198 | | Dans le fichier `harch.c` : |
199 | | {{{#!c |
200 | | 12 struct tty_s { |
201 | | 13 int write; // tty's output address |
202 | | 14 int status; // tty's status address something to read if not null) |
203 | | 15 int read; // tty's input address |
204 | | 16 int unused; // unused address |
205 | | 17 }; |
206 | | 18 |
207 | | 19 extern volatile struct tty_s __tty_regs_map[NTTYS]; |
208 | | }}} |
209 | | À quoi servent les mots clés `extern` et `volatile` ?\\Si `NTTYS` est une macro dont la valeur est `2`, quelle est l'adresse en mémoire `__tty_regs_map[1].read` ? |
210 | | {{{#!protected ------------------------------------------------------------------------------------ |
211 | | '' |
212 | | Cours 10 / slide 53 |
213 | | - `extern` : informe le compilateur que la variable définie existe ailleurs. Grâce à son type, le compilateur sait s'en servir. |
214 | | - `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. |
215 | | - `__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, c'est le troisième entier de la structure, donc en `+8` par rapport au début.\\En conséquence `__tty_regs_map[1].read` est en `0xd0200018` |
216 | | '' |
217 | | }}} |
218 | | 1. 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: |
219 | | - trouver l'adresse de la fonction C qu'il doit appeler pour exécuter le service demandé; |
220 | | - placer cette adresse dans un registre, nous utilisons le registre `$2`; |
221 | | - exécuter l'instruction `jal` (ici, `jal $2`) pour appeler la fonction. |
222 | | |
223 | | Que doivent contenir les registres `$4` à `$7` et comment doit-être la pile et le pointeur de pile? |
224 | | {{{#!protected ------------------------------------------------------------------------------------ |
225 | | '' |
226 | | Cours 10 / slide 42 |
227 | | - C'est un appel de fonction, il faut donc respecter la convention d'appel des fonctions |
228 | | - Les registres `$4`à `$7` contiennent les arguments de la fonction |
229 | | - 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. |
230 | | - 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. |
231 | | - 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. |
232 | | '' |
233 | | }}} |
234 | | 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` |
235 | | {{{#!c |
236 | | 1 int fprintf (int tty, char *fmt, ...) |
237 | | 2 { |
238 | | 3 int res; |
239 | | 4 char buffer[PRINTF_MAX]; |
240 | | 5 va_list ap; |
241 | | 6 va_start (ap, fmt); |
242 | | 7 res = vsnprintf(buffer, sizeof(buffer), fmt, ap); |
243 | | 8 res = syscall (tty, (int)buffer, 0, 0, SYSCALL_TTY_PUTS); |
244 | | 9 va_end(ap); |
245 | | 10 return res; |
246 | | 11 } |
247 | | 12 |
248 | | 13 void exit (int status) |
249 | | 14 { |
250 | | 15 syscall( status, 0, 0, 0, SYSCALL_EXIT); // never returns |
251 | | 16 } |
252 | | }}} |
253 | | |
254 | | Le code de cette fonction est dans le fichier `tp2/4_libc/ulib/crt0.c` |
255 | | {{{#!c |
256 | | 1 //int syscall (int a0, int a1, int a2, int a3, int syscall_code) |
257 | | 2 __asm__ ( |
258 | | 3 ".globl syscall \n" |
259 | | 4 "syscall: \n" |
260 | | 5 " lw $2,16($29) \n" |
261 | | 6 " syscall \n" |
262 | | 7 " jr $31 \n" |
263 | | 8 ); |
264 | | }}} |
265 | | Combien d'arguments a la fonction `syscall()`? |
266 | | Comment la fonction `syscall()` reçoit-elle ses arguments ? |
267 | | A quoi sert la ligne 3 de la fonction `syscall()` et que se passe-t-il si on la retire ? |
268 | | Expliquer la ligne 5 de la fonction `syscall()`. |
269 | | Aurait-il été possible de mettre le code de la fonction `syscall()` dans un fichier `.S` ? |
270 | | {{{#!protected ------------------------------------------------------------------------------------ |
271 | | '' |
272 | | Cours 10 / slide 40 |
273 | | - La fonction `syscall()` a 5 a arguments |
274 | | - Elle reçoit ses 4 premiers arguments dans les registres $4 à $7 et le 5e (le numéro de service) dans la pile. |
275 | | - La ligne 3 sert à dire que syscall est une étiquette utilisée dans un autre fichier. `.globl` signifie **glob**al **l**abel. 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. |
276 | | - 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. |
277 | | - 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 un d'autres fonctions assembleur, alors on créera un fichier assembleur pour les réunir. |
278 | | '' |
279 | | }}} |
280 | | |
281 | | |
282 | | |
283 | | = 3. Passage entre les modes kernel et user |
| 142 | = 2. Passage entre les modes kernel et user |
| 330 | = 3. Langage C pour la programmation système |
| 331 | |
| 332 | |
| 333 | |
| 334 | |
| 335 | La programmation en C, vous connaissez, mais quand on programme pour le noyau, c'est un peu différent. |
| 336 | Il y a des éléments de syntaxe ou des besoins spécifiques. Pour répondre aux questions, vous devez avoir lu les transparents de l'annexe du cours 10, dans lesquels une séquence complète de code (du boot à exit) est détaillée. |
| 337 | |
| 338 | |
| 339 | **Questions** |
| 340 | |
| 341 | |
| 342 | 1. En assembleur, vous utilisez les sections prédéfinies `.data` et `.text` pour placer respectivement les data et le code, mais vous pouvez créer vos propres sections avec la directive `.section` (nous avons utilisé cette possibilité pour la section `.boot`). Il est aussi possible d'imposer ou de créer des sections en langage C avec la directive `__attribute__((section("section-name")))`. La directive du C `__attribute__` permet de demander certains comportements au compilateur. Ici, c'est la création d'une section, mais il y a beaucoup d'attributs possibles (si cela vous intéresse vous pouvez regarder dans la [https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html doc de GCC sur les attributs]. Comment créer la section `.start` en C ? |
| 343 | {{{#!protected ------------------------------------------------------------------------------------ |
| 344 | '' |
| 345 | Cours 10 annexe / slide 8 |
| 346 | - `__attribute__ ((section (".start")))`\\La syntaxe est un peu curieuse avec les doubles underscore et les doubles parenthèses. |
| 347 | '' |
| 348 | }}} |
| 349 | 1. En C, vous savez que les variables globales sont toujours initialisées, soit explicitement dans le programme lui-même, soit implicitement à la valeur `0`. Les variables globales initialisées sont placées dans la section `.data` (ou plutôt dans l'une des sections `data` : `.data`, `.sdata`, `.rodata`, etc.) et elles sont présentes dans le fichier objet (`.o`) produit pas le compilateur. En revanche, les variables globales non explicitement initialisées ne sont pas présentes dans le fichier objet. Ces dernières sont placées dans un segment de la famille [https://www.wikiwand.com/fr/Segment_BSS `.bss`]. Le fichier ldscript permet de mapper l'ensemble des segments en mémoire. Pour pouvoir initialiser à `0` les segments `bss` par programme, il nous faut connaître les adresses de début et de fin où ils sont placés en mémoire.\\ \\Le code ci-dessous est le fichier ldscript du kernel `kernel.ld` (nous avons retiré les commentaires mais ils sont dans les fichiers).\\Expliquez ce que font les lignes 11, 12 et 15. |
| 350 | {{{#!java |
| 351 | 1 SECTIONS |
| 352 | 2 { |
| 353 | 3 .boot : { |
| 354 | 4 *(.boot) |
| 355 | 5 } > boot_region |
| 356 | 6 .ktext : { |
| 357 | 7 *(.text*) |
| 358 | 8 } > ktext_region |
| 359 | 9 .kdata : { |
| 360 | 10 *(.*data*) |
| 361 | 11 . = ALIGN(4); |
| 362 | 12 __bss_origin = .; |
| 363 | 13 *(.*bss*) |
| 364 | 14 . = ALIGN(4); |
| 365 | 15 __bss_end = .; |
| 366 | 16 } > kdata_region |
| 367 | 17 } |
| 368 | }}} |
| 369 | {{{#!protected ------------------------------------------------------------------------------------ |
| 370 | '' |
| 371 | Cours 10 / slide 28 |
| 372 | - La ligne 11 contient `. = ALIGN(4)`, c'est équivalent à la directive `.align 4` de l'assembleur. |
| 373 | Cela permet de déplacer le pointeur de remplissage de la section de sortie courante (c'est-à-dire ici `.kdata`) sur une |
| 374 | frontière de 2^4^ octets (une adresse multiple de 16). Cette contrainte est liée aux caches que nous ne verrons pas ici. |
| 375 | - La ligne 12 permet de créer la variable de ldscript `__bss_origin` et de l'initialiser à l'adresse courante, |
| 376 | ce sera donc l'adresse de début de la zone `bss`. |
| 377 | - La ligne 15 permet de créer la variable `__bss_end` qui sera l'adresse de fin de la zone `bss` |
| 378 | (en fait c'est la première adresse qui suit juste `bss`. |
| 379 | '' |
| 380 | }}} |
| 381 | 1. 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`: |
| 382 | {{{#!c |
| 383 | __tty_regs_map = 0xd0200000 ; /* tty's registers map, described in devices.h */ |
| 384 | }}} |
| 385 | Dans le fichier `harch.c` : |
| 386 | {{{#!c |
| 387 | 12 struct tty_s { |
| 388 | 13 int write; // tty's output address |
| 389 | 14 int status; // tty's status address something to read if not null) |
| 390 | 15 int read; // tty's input address |
| 391 | 16 int unused; // unused address |
| 392 | 17 }; |
| 393 | 18 |
| 394 | 19 extern volatile struct tty_s __tty_regs_map[NTTYS]; |
| 395 | }}} |
| 396 | À quoi servent les mots clés `extern` et `volatile` ?\\Si `NTTYS` est une macro dont la valeur est `2`, quelle est l'adresse en mémoire `__tty_regs_map[1].read` ? |
| 397 | {{{#!protected ------------------------------------------------------------------------------------ |
| 398 | '' |
| 399 | Cours 10 annexe / slide 23 |
| 400 | - `extern` : informe le compilateur que la variable définie existe ailleurs. Grâce à son type, le compilateur sait s'en servir. |
| 401 | - `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. |
| 402 | - `__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, c'est le troisième entier de la structure, donc en `+8` par rapport au début.\\En conséquence `__tty_regs_map[1].read` est en `0xd0200018` |
| 403 | '' |
| 404 | }}} |
| 405 | 1. 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: |
| 406 | - trouver l'adresse de la fonction C qu'il doit appeler pour exécuter le service demandé; |
| 407 | - placer cette adresse dans un registre, nous utilisons le registre `$2`; |
| 408 | - exécuter l'instruction `jal` (ici, `jal $2`) pour appeler la fonction. |
| 409 | |
| 410 | Que doivent contenir les registres `$4` à `$7` et comment doit-être la pile et le pointeur de pile? |
| 411 | {{{#!protected ------------------------------------------------------------------------------------ |
| 412 | '' |
| 413 | Cours 10 annexe / slide 12 |
| 414 | - C'est un appel de fonction, il faut donc respecter la convention d'appel des fonctions |
| 415 | - Les registres `$4`à `$7` contiennent les arguments de la fonction |
| 416 | - 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. |
| 417 | - 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. |
| 418 | - 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. |
| 419 | '' |
| 420 | }}} |
| 421 | 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` |
| 422 | {{{#!c |
| 423 | 1 int fprintf (int tty, char *fmt, ...) |
| 424 | 2 { |
| 425 | 3 int res; |
| 426 | 4 char buffer[PRINTF_MAX]; |
| 427 | 5 va_list ap; |
| 428 | 6 va_start (ap, fmt); |
| 429 | 7 res = vsnprintf(buffer, sizeof(buffer), fmt, ap); |
| 430 | 8 res = syscall (tty, (int)buffer, 0, 0, SYSCALL_TTY_PUTS); |
| 431 | 9 va_end(ap); |
| 432 | 10 return res; |
| 433 | 11 } |
| 434 | 12 |
| 435 | 13 void exit (int status) |
| 436 | 14 { |
| 437 | 15 syscall( status, 0, 0, 0, SYSCALL_EXIT); // never returns |
| 438 | 16 } |
| 439 | }}} |
| 440 | |
| 441 | Le code de cette fonction est dans le fichier `tp2/4_libc/ulib/crt0.c` |
| 442 | {{{#!c |
| 443 | 1 //int syscall (int a0, int a1, int a2, int a3, int syscall_code) |
| 444 | 2 __asm__ ( |
| 445 | 3 ".globl syscall \n" |
| 446 | 4 "syscall: \n" |
| 447 | 5 " lw $2,16($29) \n" |
| 448 | 6 " syscall \n" |
| 449 | 7 " jr $31 \n" |
| 450 | 8 ); |
| 451 | }}} |
| 452 | Combien d'arguments a la fonction `syscall()`? |
| 453 | Comment la fonction `syscall()` reçoit-elle ses arguments ? |
| 454 | A quoi sert la ligne 3 de la fonction `syscall()` et que se passe-t-il si on la retire ? |
| 455 | Expliquer la ligne 5 de la fonction `syscall()`. |
| 456 | Aurait-il été possible de mettre le code de la fonction `syscall()` dans un fichier `.S` ? |
| 457 | {{{#!protected ------------------------------------------------------------------------------------ |
| 458 | '' |
| 459 | Cours 10 annexe / slide 10 |
| 460 | - La fonction `syscall()` a 5 a arguments |
| 461 | - Elle reçoit ses 4 premiers arguments dans les registres $4 à $7 et le 5e (le numéro de service) dans la pile. |
| 462 | - La ligne 3 sert à dire que syscall est une étiquette utilisée dans un autre fichier. `.globl` signifie **glob**al **l**abel. 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. |
| 463 | - 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. |
| 464 | - 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 un d'autres fonctions assembleur, alors on créera un fichier assembleur pour les réunir. |
| 465 | '' |
| 466 | }}} |
| 467 | |
| 468 | |
| 469 | |