2019-2020 - Cours d'introduction aux systèmes d'exploitations : de la théorie à la pratique

Note: le cours sera mis à jour au moins une fois par semaine ! Pensez à le retélécharger ou à le consulter sur le site https://darnuria.eu/2019-2020_os.

Bienvenue sur le site compagnon du cours d'introduction aux systèmes d'exploitation, l'enjeu de ce cours est de faire le lien entre théorie et pratique.

Ce cours est en lien direct avec votre cours d'assembleur et votre cours de C avec Frédéric : les savoirs et pratiques que nous allons développer ensemble seront plus facile à acquérir si vous suivez votre cours de C et d'assembleur, et vice-versa. ;)

Liens utiles pour le cours :

Contributions

Contributions:

Environnement de développement pour le cours

Dans ce cours le système de référence sera la distribution GNU/Linux Ubuntu 18.04.03 LTS Bionic Beaver. Si vous utilisez une autre distribution GNU/Linux aucun problème, mais tâchez d'avoir un système à jour et utilisable en classe. ;)

Mac OS X

Si vous utilisez Mac OS X vous pouvez suivre ce guide en anglais : https://solarianprogrammer.com/2017/05/21/compiling-gcc-macos/

Veillez à bien avoir une version récente de gcc telle que la version 7 ou 8.

Windows sous système Linux

Section en cours de construction

Le tutoriel pour installer Windows Subsystem for Linux (WSL) : https://docs.microsoft.com/fr-fr/windows/wsl/install-win10

Éditeur de texte

Concernant l'éditeur de référence, n'importe quel éditeur (moderne) que vous maîtrisez devrait convenir. Idéalement privilégiez un éditeur qui possède des fonctionnalités pour naviguer dans le code et afficher les erreurs à la volée.

Si vous ne savez pas quoi choisir vous pouvez prendre :

A titre personnel j'utilise Spacemacs et VSCodium sur Ubuntu 18.04.03.

Je vais privilégier l'usage de logiciels libres et Open Source, car ce sont des logiciels que vous pouvez aller étudier en lisant leur code.

Je vous proposerai d'ailleurs à plusieurs moments du cours d'étudier le code de plusieurs bibliothèques et logiciels libre proches du système.

Compilateurs et langages

Nous développerons principalement en C avec le standard C18. Nous utiliserons le compilateur gcc en version 8.3 ou supérieur, ou bien le compilateur clang en version 8 ou plus.

J'ai volontairement choisi d'utiliser des compilateurs modernes afin que vous puissiez bénéficier d'erreurs de compilation compréhensibles.

Autres langages

Pour vous faire découvrir certains concepts avancés tel que la concurrence, le parallélisme et la programmation système moderne, il est possible d'utiliser Rust, Go ou encore C++ moderne : nous installerons ensemble l'écosystème.

Plan du cours

Le plan n'est pas définitif : il est à titre indicatif, tous les sujets sont assez entremêlés. Il s'agit de grandes thématiques que nous allons aborder.

Cours 1 et 2 : Introduction et appels système

Objectifs pédagogiques :

C'est quoi un système d'exploitation ?

Un système d'exploitation est un concept général pour désigner un ensemble de services rendus à des programmes. Ces services peuvent être réalisés par des bibliothèques, d'autres programmes ou bien directement par le kernel.

C'est un concept vaste, il existe plusieurs façon de réaliser un système d'exploitation.

C'est quoi un noyau kernel de système d'exploitation

Un noyau ou kernel de système d'exploitation est un programme gérant des ressources matérielles et logicielles ainsi que des opérations d'intercommunication entre ces ressources. Il peut être plus ou moins complexe.

Il existe plusieurs façons de réaliser un kernel. Linux par exemple est un kernel monolithique, au sens où le kernel accomplit énormement de fonctions. D'autres façons de faire existent : par exemple XNU, la base du système d'exploitation Mac OS X, n'est pas un design monolithique.

Ouvertures : Des approches différentes existent. Le multi-kernel barrelfish, ou bien almos-mkh tentent de concevoir des kernels pour les architectures avec plus de 128 processeurs.

C'est quoi un appel système ?

Un appel système est un service effectué par le kernel du système d'exploitation. Lors d'un appel système, votre programme passe en mode kernel (kernel-land). En simplifiant, temporairement votre programme n'exécute plus son code mais celui du kernel pour résoudre cet appel système, par exemple write dans notre exercice ci-dessous. Nous reviendrons sur cette notion au cours 3.

En général les appels système nécessitent de dialoguer avec le matériel hardware, ou de respecter des besoins de sécurité avec ou sans support matériel (par exemple le dialogue interprocessus, le réseau, etc.).

Comment réalise-t-on un appel système ?

Les appels système dans les systèmes modernes font appel à une instruction spécialisée souvent nommée syscall. Voici un panorama des instructions réalisant cette tâche sur plusieurs architectures:

Vous ne verrez pas souvent cette instruction dans vos programmes en C car les appels syscalls sont souvent réalisés par des bibliothèques système pour vous telles que la libc. Sous Linux, en général vous utilisez la glibc.

Note x86_64 : vous avez peut-être vu ou utilisé sysenter, ou bien int 0x80. Ce sont des façons d'appeler un syscall pour x86_64 en mode 32bits. Pour information, int 0x80 est considérée comme dépréciée.

Dans le TP nous allons voir comment réaliser cet appel système en C et, pour aller plus loin en assembleur, sans C.

TP: Write "Hello, World!"

Nous allons réaliser un petit programme C et l'analyser.

Objectifs:

Les commandes d'analyse vous seront utiles en cours de sécurité. ;)

Installation du compilateur GCC-8

Si ce n'est pas déjà fait nous allons installer un compilateur C à jour :

$ sudo apt install gcc-8 # gcc-8 est le programme que nous voulons installer
# ^    ^   ^             # C'est aussi un argument de ligne de commande.
# |    |   \ est un argument de la commande `apt`
# |    \ apt est une commande qui permet de gérer vos paquets.
# \ sudo demande les droits du super utilisateur pour installer un paquet.

# Si tout va bien vous deviez voir le message suivant s'afficher
$ gcc-8 --version
gcc-8 (Ubuntu 8.3.0-6ubuntu1~18.04.1) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Note: vous verrez le caractère $ dans les exemples de ligne de commande que je vous donne. C'est pour indiquer que c'est une commande à saisir dans votre shell bash ou zsh.

Une fois gcc-8 installé nous allons écrire le programme C suivant :

// Headers de la bibliothèque standard unistd, vous pouvez les retrouver
// sur ubuntu ici: `/usr/include/unistd.h`
#include <unistd.h>

/*
 * Questions :
 * - Réussir à faire fonctionner ce programme pour qu'il affiche la chaine msg.
 * - Que fait l'appel système write ?
 * - Comment tracer/lister des appels système sur un programme ?
 */
int main(void) {
  const char msg[] = "Hello, world!";
  // msg est une chaine qui sera stockée dans le
  // segment de données de votre programme.
  write(???, ???, ???);
  // \ les arguments sont à corriger, le programme ne compile pas actuellement.
  return 0;
}

Je vous invite à consulter le manuel (commande man) de l'appel système write.

Vous pouvez compiler ce programme avec la commande suivante :

$ gcc-8 -Wall -Wextra ex0_write.c
# Cette commande produit l'exécutable `a.out`
# que vous pouvez exécuter comme cela :
$ ./a.out

Un appel système «à la main» sur l'architecture x86_64

Une façon de faire sans le langage C, ni libc, aurait été d'écrire le programme en assembleur x86_64 suivant :

; hello.s
global _start

section .text

_start:

; numéro d'appel système
; write est l'appel système numéro 1
; C'est Linux (et POSIX) qui a défini cette convention
; que nous devons respecter pour faire un write.

; STDOUT == 1
; La sortie standard STDOUT est le file descriptor 1
; c'est une convention imposée par POSIX et respectée par Linux.

mov rax, 1      ; write(
mov rdi, 1      ;   STDOUT_FILENO,
mov rsi, msg    ;   "Hello, world!\n",
mov rdx, msglen ;   sizeof("Hello, world!\n")
syscall         ; );

mov rax, 60     ; exit(
mov rdi, 0      ;   EXIT_SUCCESS
syscall         ; );

section .rodata
msg: db "Hello, world!", 10
msglen: equ $ - msg

Et de l'assembler et de le lier (le linker) avec les commandes ci-dessous :

$ nasm -f elf64 -o hello.o hello.s
$ ld -o hello hello.o
$ ./hello
Hello, world!

Si vous voulez en savoir plus sur ma source, il s'agit d'un billet de blog Hello world in Linux x86-64 assembly sur le blog de Jim Fisher.

Analyses

Observation d'un fichier compilé - objdump

Une fois ce code fonctionnel nous pouvons l'analyser.

Pour ce faire nous allons commencer par observer la structure de l'exécutable produit à l'aide de la commande :

# Le paramètre -S est pour entremêler source et asm.
$ objdump -S a.out | less
0000000000000685 <main>:
 685:   55                      push   %rbp
 686:   48 89 e5                mov    %rsp,%rbp
 689:   48 83 ec 20             sub    $0x20,%rsp
 68d:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
 694:   00 00
 696:   48 89 45 f8             mov    %rax,-0x8(%rbp)
 69a:   31 c0                   xor    %eax,%eax
 69c:   48 b8 48 65 6c 6c 6f    movabs $0x77202c6f6c6c6548,%rax
 6a3:   2c 20 77
 6a6:   48 89 45 ea             mov    %rax,-0x16(%rbp)
 6aa:   c7 45 f2 6f 72 6c 64    movl   $0x646c726f,-0xe(%rbp)
 6b1:   66 c7 45 f6 21 00       movw   $0x21,-0xa(%rbp)
 6b7:   48 8d 45 ea             lea    -0x16(%rbp),%rax
 6bb:   ba 0e 00 00 00          mov    $0xe,%edx
 6c0:   48 89 c6                mov    %rax,%rsi
 6c3:   bf 01 00 00 00          mov    $0x1,%edi
 6c8:   e8 a3 fe ff ff          callq  570 <write@plt>
 6cd:   b8 00 00 00 00          mov    $0x0,%eax
 6d2:   48 8b 4d f8             mov    -0x8(%rbp),%rcx
 6d6:   64 48 33 0c 25 28 00    xor    %fs:0x28,%rcx
 6dd:   00 00
 6df:   74 05                   je     6e6 <main+0x61>
 6e1:   e8 9a fe ff ff          callq  580 <__stack_chk_fail@plt>
 6e6:   c9                      leaveq
 6e7:   c3                      retq
 6e8:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
 6ef:   00

Note: vous pouvez avoir un dump plus intérressant en compilant avec l'option -g qui exporte des informations utiles.

Voici le code en assembleur x86_64 de votre fonction main.

Où est notre appel à write? Indice : vous pouvez faire une recherche avec la commandegrep ou avec / dans less.

Ficher vu en hexadécimal - hexdump

A présent on va observer notre fichier en vue hexadécimale pour rechercher notre chaîne de caractères "Hello, world!".

Vous devriez obtenir quelque chose comme cela : il s'agit d'une vue en hexadécimal de votre programme, pour être précis, aux alentours de là où la chaîne msg est stockée.

La commande hexdump -C ficher_a_dump se lit de la façon suivante : numéro de ligne, 64 bits de données ou 16 bytes (octets), leur représentation en caractères.

00000700  20 00 55 48 8d 2d ae 06  20 00 53 41 89 fd 49 89  | .UH.-.. .SA..I.|
00000710  f6 4c 29 e5 48 83 ec 08  48 c1 fd 03 e8 27 fe ff  |.L).H...H....'..|
00000720  ff 48 85 ed 74 20 31 db  0f 1f 84 00 00 00 00 00  |.H..t 1.........|
00000730  4c 89 fa 4c 89 f6 44 89  ef 41 ff 14 dc 48 83 c3  |L..L..D..A...H..|
00000740  01 48 39 dd 75 ea 48 83  c4 08 5b 5d 41 5c 41 5d  |.H9.u.H...[]A\A]|
00000750  41 5e 41 5f c3 90 66 2e  0f 1f 84 00 00 00 00 00  |A^A_..f.........|
00000760  f3 c3 00 00 48 83 ec 08  48 83 c4 08 c3 00 00 00  |....H...H.......|
00000770  01 00 02 00 48 65 6c 6c  6f 2c 20 77 6f 72 6c 64  |....Hello, world|
00000780  21 00 00 00 01 1b 03 3b  38 00 00 00 06 00 00 00  |!......;8.......|
00000790  dc fd ff ff 84 00 00 00  0c fe ff ff ac 00 00 00  |................|
000007a0  1c fe ff ff c4 00 00 00  7c fe ff ff 54 00 00 00  |........|...T...|
000007b0  6c ff ff ff dc 00 00 00  dc ff ff ff 24 01 00 00  |l...........$...|

Note: votre hexdump peut différer selon vos options de compilation.

Analyse des appels système - strace

strace est un logiciel permettant de suivre et observer des appels système sur un programme donné en paramètre.

Si vous devez debugger des appels système c'est un outil très puissant.

Note: C'est un outil utile pour les challenges de sécurité orientés système ! ;)

Par exemple voici la «trace» de notre programme obtenue avec : $ strace ./a.out > log.

execve("./a.out", ["./a.out"], 0x7ffd9e7b3190 /* 74 vars */) = 0
brk(NULL)                               = 0x559cc2ee0000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/darnuria/.local/lib/tls/haswell/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/darnuria/.local/lib/tls/haswell/x86_64", 0x7ffe99a92ba0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/darnuria/.local/lib/tls/haswell/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/darnuria/.local/lib/tls/haswell", 0x7ffe99a92ba0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/darnuria/.local/lib/tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/darnuria/.local/lib/tls/x86_64", 0x7ffe99a92ba0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/darnuria/.local/lib/tls/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/darnuria/.local/lib/tls", 0x7ffe99a92ba0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/darnuria/.local/lib/haswell/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/darnuria/.local/lib/haswell/x86_64", 0x7ffe99a92ba0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/darnuria/.local/lib/haswell/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/darnuria/.local/lib/haswell", 0x7ffe99a92ba0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/darnuria/.local/lib/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/darnuria/.local/lib/x86_64", 0x7ffe99a92ba0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/darnuria/.local/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/darnuria/.local/lib", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
openat(AT_FDCWD, "tls/haswell/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "tls/haswell/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "tls/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "haswell/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "haswell/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=123633, ...}) = 0
mmap(NULL, 123633, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa8fbe6b000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa8fbe69000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa8fb872000
mprotect(0x7fa8fba59000, 2097152, PROT_NONE) = 0
mmap(0x7fa8fbc59000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7fa8fbc59000
mmap(0x7fa8fbc5f000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa8fbc5f000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7fa8fbe6a4c0) = 0
mprotect(0x7fa8fbc59000, 16384, PROT_READ) = 0
mprotect(0x559cc1eff000, 4096, PROT_READ) = 0
mprotect(0x7fa8fbe8a000, 4096, PROT_READ) = 0
munmap(0x7fa8fbe6b000, 123633)          = 0
write(1, "Hello, world!\0", 14)         = 14
exit_group(0)                           = ?
+++ exited with 0 +++

On voit à la fin notre write avec les arguments que nous lui avons donné ! Le reste est introduit par notre shell (bash ici) et la libc pour diverses raisons.

A titre de comparaison voici un strace de notre version «à la main» strace ./hello > log:

execve("./write", ["./write"], 0x7ffce51e2390 /* 74 vars */) = 0
write(1, "Hello, world!\n", 14)         = 14
exit(0)                                 = ?
+++ exited with 0 +++

Aucune des deux versions est meilleure : si les appels système sont présent dans la version en C c'est souvent pour des raisons de praticité, comptatibilité ou de sécurité.

Exercice 01: Travail à la maison - filedex

Dans ce travail à la maison je vous invite à réaliser un programme en C qui affiche des informations sur des fichiers.

Objectifs:

Difficultés:

Notation:

Temps de travail estimé: 3h à 4h lectures et questions incluses.

Ce programme devra afficher sur la sortie standard les informations sur un fichier, notament sa taille, son type et ses droits d'accès, le chemin path du fichier sera donné en paramètre à votre programme.

Pour compiler ce fichier vous pourrez faire: gcc -Wall -Werror -Wextra filedex.c.

Et son usage sera le suivant :

# Sans arguments
$ ./a.out
Usage: ./a.out <pathname>
# Avec un argument qui est un chemin de fichier
$ ./a.out a.out
File type:                regular file
I-node number:            13268379
Mode:                     100755 (octal)
Link count:               1
Ownership:                UID=1000   GID=1000
Preferred I/O block size: 4096 bytes
File size:                12744 bytes
Blocks allocated:         32
Last status change:       Mon Oct  7 16:50:11 2019
Last file access:         Mon Oct  7 16:50:13 2019
Last file modification:   Mon Oct  7 16:50:11 2019

Pour se simplifier la vie on ne gère pas les erreurs dans ce travail pratique.

Pour ce faire vous devrez utiliser et manipuler l'appel système fstat, pour obtenir des metadonnées de fichier, utiliser open pour obtenir un descripteur de fichier et utiliser printf pour formater et afficher ces informations.

Votre documentation principale sera man 2 fstat, surtout la section: EXAMPLE.

Je vous invite très fortement à commencer avec l'exemple issu du manuel de fstat.

Attention : par mesure de sécurité et de prudence, n'exécutez jamais, sauf indication contradictoire, en sudo ou root les programmes de ce cours. Un mauvais usage des appels système peut parfois causer des soucis.

Questions

Une fois ce programme réalisé, je vous demande d'expliquer ligne à ligne votre programme, quels concepts de C vous manipulez, quels appels système vous réalisez et ce que représentent les informations que vous allez afficher. Vous pouvez rester succincts mais l'objectif est de retranscrire votre travail de recherche documentaire.

En plus veuillez répondre aux questions suivantes en commentaire en haut de votre programme :

Bonus

Vous pouvez compiler avec plusieurs niveaux d'optimisations options -O0, -O1, -O2, -O3 et constater des changements dans le code emit.

Corrections

Exercice 01 - stat

Voir Rappels de langage C sur les structs.

Voici le code source commenté ligne à ligne de filedex, vous pouvez trouver un point de départ pour résoudre l'exercice dans le manuel de l'appel man 2 stat.

// Programme commenté inspiré du manuel de l'appel système stat.
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/sysmacros.h>

int main(int argc, char *argv[]) {

  // On quitte si on à pas 2 arguments
  // (par defaut argv contient le nom du programme en argument 0)
  if (argc != 2) {
    // On affiche un message d'erreur pour expliquer le problème rencontré.
    fprintf(stderr, "Usage: %s <pathname>\n", argv[0]);
    // On signale une erreur au processus parent.
    exit(EXIT_FAILURE);
  }

  // On alloue dans la *pile* une structure de type `struct stat`
  struct stat sb;
  // On realise notre appel système ici lstat qui va remplir la structure
  // sb si tout se passe bien.
  // Le passage est par pointeur.
  const int ret = lstat(argv[1], &sb);

  // Si lstat echoue ret vaut -1
  if (ret == -1) {
    perror("lstat");
    exit(EXIT_FAILURE);
  }

  // On affiche des informations renvoyées par lstat
  // parfois des *cast* ou changements de types sont neccessaires.
  // Aussi parfois on a besoin de macro définies definies dans sysmacro
  // pour obtenir la partie pertinante d'un entier a affichier.
  printf("ID of containing device:  [%lx,%lx]\n",
      (long) major(sb.st_dev), (long) minor(sb.st_dev));

  printf("File type:                ");

  switch (sb.st_mode & S_IFMT) {
  case S_IFBLK:  printf("block device\n");            break;
  case S_IFCHR:  printf("character device\n");        break;
  case S_IFDIR:  printf("directory\n");               break;
  case S_IFIFO:  printf("FIFO/pipe\n");               break;
  case S_IFLNK:  printf("symlink\n");                 break;
  case S_IFREG:  printf("regular file\n");            break;
  case S_IFSOCK: printf("socket\n");                  break;
  default:       printf("unknown?\n");                break;
  }

  printf("I-node number:            %ld\n", (long) sb.st_ino);

  printf("Mode:                     %lo (octal)\n",
          (unsigned long) sb.st_mode);

  printf("Link count:               %ld\n", (long) sb.st_nlink);
  printf("Ownership:                UID=%ld   GID=%ld\n",
          (long) sb.st_uid, (long) sb.st_gid);

  printf("Preferred I/O block size: %ld bytes\n",
          (long) sb.st_blksize);
  printf("File size:                %lld bytes\n",
          (long long) sb.st_size);
  printf("Blocks allocated:         %lld\n",
          (long long) sb.st_blocks);

  printf("Last status change:       %s", ctime(&sb.st_ctime));
  printf("Last file access:         %s", ctime(&sb.st_atime));
  printf("Last file modification:   %s", ctime(&sb.st_mtime));

  // On signale au parent que tout s'est bien passé.
  exit(EXIT_SUCCESS);
}

Concept UNIX: «Tout est fichier»

C'est quoi un fichier

Un « fichier » ou file en anglais est une façon de conceptualiser une donnée sur un ordinateur. Un fichier est associé à un nom de fichier filename, et à un chemin d'accès absolu depuis une origine. Sur UNIX cette origine c'est /, par exemple cat.jpg dans votre home serait au chemin: /home/axel/cat.jpg.

Cette arborescence simplifée pour aller jusque à cat.jpg donne quelque chose comme ça :

/
├── bin    # Binaires du système
├── boot   # Contient le kernel et le bootloader
├── cdrom  # Legacy? ;)
├── dev    # Contient les devices / periphériques
├── etc    # Les configurations
├── home   # Dossier home des utilisateurs
   └── axel
       └── cat.jpg
├── lib    # Bibliothèques et plus
├── lib32  # Bibliothèques compilées 32bits
├── lib64  # Bibliothèques compilées 64bits
├── lost+found
├── media  # Point de montages des supports amovibles
├── mnt
├── opt
├── proc   # Informations sur les processus executées
├── root   # Home du super utilisateur
├── run
├── sbin
├── srv
├── sys    # Informations du kernel exposées aux programmes
├── tmp    # Contient des données temporaires
├── usr    # UNIX System Resources - config et plus des programmes
└── var

Un fichier peut représenter une donnée qui existe sur votre support de stockage, Solid state disk SSD ou Hard Disk Drive HDD, mais nous verrons plus loin que ça peut représenter bien plus.

C'est à la fois une façon d'organiser l'information (des données) accessibles sur un ordinateur par un humain mais aussi une interface entre programmes et le kernel.

NOTE: Sous Windows il existe plusieurs racines, une par disque, C:, D: et les chemins sont séparés par des \.

Et le Tout-fichier : pourquoi ?

Le système que nous utilisons pour pratiquer est Ubuntu 18.04.03, il s'agit d'un système d'exploitation de la famille des «UNIX», son kernel, en français noyau, s'appelle Linux. Vous verrez parfois GNU/Linux, GNU est la suite de logiciel et bibliothèques nécessaires pour exploiter un noyau.

Unix est une famille de systèmes d'exploitations connus pour proposer une approche où toutes les ressources sont utilisables via une interface de fichier, très peu d'appels système dérogent à cette méthode.

Sous UNIX vous voulez écrire sur la console ? Vous utiliserez le même appel système que pour écrire dans un fichier: write man 2 write, pour lire il existe read man 2 read.

Pour aller plus loin:

Si vous voulez en savoir plus je vous recommande cet article: « Tearing printf appart (2018).

Vos programmes sont connectés à 3 flux d'entrée sorties:

Ces trois flux se comportent comme des fichiers un peu spéciaux et sont très utilisés quand vous travaillez avec un terminal. On vera avec la communication interprocessus que ces trois flux sont très utilles

Pour communiquer entre deux programmes il existe des mécanismes reposant aussi sur une Application Programming Interface (API) reposant sur le concept de fichier.

Pour accéder aux informations d'un processus ? C'est aussi par une API fichier. Regardez le contenu du chemin: /proc/<processus_id>/ il y a plein d'informations relatives à un processus en exécution.

Bref sous Linux, comme sous MacOSX qui est aussi un Unix «Tout est fichier», sur Windows c'est une autre façon de concevoir (autre paradigme).

Introduction sur la mémoire

Sur un ordinateur moderne, la mémoire joue un rôle centrale.

Elle remplis plusieurs missions:

Matériellement la mémoire est très souvent sous la forme de «barrettes» de mémoire contenant des banques de mémoire d'une taille sur un multiple de 2 par exemples en 4 banques de 256 Mo ce qui donne 1Go.

Addressage

Cette mémoire est addressable (accessible) par une unité minimale appellée byte, en général un byte est égal à 8bits soit un octet, cependant historiquement il a exister des machines avec des bytes de 10bits. Le mode d'addressage leplus rapide est appellée le mot mémoire ou word, par exemple: Un processeur RiscV ou mips32 32bits possède des bytes de 8bits et il est possible d'addresser sur 8bits, 16bits ou 32bits.

Le processeur et les autres périphériques y accède par le biais d'un canal de communication matériel arbitré nommée un bus, ici le bus mémoire. Si cette notion vous intérresse je vous invite à lire sur l'architecture des ordinateurs.

Vous avez du le remarquer dans vos programmes vous gériez votre mémoire comme si vos processus (voir chapitre suivant) étaient seuls sur l'ordinateur. Tout ce qui compte est d'avoir des adresses sur la mémoire pour écrire ou lire des valeurs.

Lien avec le C: Historiquement les types char, short, int, long derivent d'une volonté de supporter toutes les architectures possibles, beaucoup de ces architectures ésothériques ont disparu car leurs particularités n'étaient pas si pertinantes.

Références sur les layouts sur Rust mais ça parle de C: "Notes on Type Layouts and ABIs in Rust".

Mémoire virtuelle

En effet de nos jours vous compilez vos programmes pour des adresses dites «virtuelles», chaque processus peut avoir les mêmes adresse c'est le matériel via un composant nommée la MMU (Mémory Management Unit) et le gestionnaire de mémoire virtuelle (VMM) de votre système d'exploitation. Qu'une traduction vers des adresses physiques correspondants à des emplacements dans les banques de mémoire de vos barrettes de mémoire à lieu.

Quel rapport avec nos programmes en C? On peut compiler pour un ordinateur sans se soucier de l'occupation réel de la mémoire dans une certaine mesure. Nos programmes peuvent être interrompus si ils accedent à de la mémoire non allouée bref c'est très pratique on reviendra dessus lors que l'ont parlera de mémoire plus en détail.

Pour être précis nos systèmes modernes gérent la mémoire virtuelle de façon paginée et segmenté, c'est à dire la mémoire est découpée en bloc de taille fixe ici 1Kio et on regroupe ses blocs en segments avec un sens, par exemple la pile .stack, le code .text ou le .tas.

Nos processus peuvent exploiter la mémoire dans ces conditions sur un segment de 4Gb sur un processeur 32bits et 8TB sur un processeur 64bits, le «layout» d'un processus (imagions un programme C) ressemble à ça du point de vue des segments:

4GB (32bits /8TB (64bits)
👇
┌───────────┐
│   stack   │ <- Contexte de main(), parametres en ligne de commande argc/argv  
│           │    appels de fonctions, croit vers le bas
├ ─ ─ ─ ─ ─ ┤
│     ⬇     │ <- Espace non allouée
│           │
│           │
│           │
├───────────┤
│   data    │ <- Données statiques, chaines et tableaux constants
├───────────┤
│   text    │ <- Instructions; code compilée (en lecture seul)
└───────────┘
👆 0

Si vous utilisez malloc, il peut resembler à ça

4GB (32bits /8TB (64bits)
👇
┌───────────┐
│   stack   │ <- Contexte de main(), parametres en ligne de commande argc/argv  
│           │    appels de fonctions, croit vers le bas
├ ─ ─ ─ ─ ─ ┤
│     ⬇     │ <- Espace non allouée
│           │
│           │
│     ⬆     │
├ ─ ─ ─ ─ ─ ┤
│   heap    │ <- Espace allouée par malloc() croit vers le haut.
├───────────┤
│   data    │ <- Données statiques, chaines et tableaux constants
├───────────┤
│   text    │ <- Instructions; code compilée (en lecture seul)
└───────────┘
👆 0

Schemas librement inspirées de stackExchange: Unix & Linux.

Processus - gestion des ressources

NOTE: En construction

Sur un ordinateur, si vous voulez avoir plusieurs taches, il y a plusieurs façon de faire historiquement plusieurs approches ont été prises: Faire que toute les taches coopéerents, c'est très utilisé en embarqué (IoT) mais sur vos machines on préfére utiliser un model preemptif ou ajouter un acteur ne neccessite pas d'informer tout les autres.

Le concept de processus que nous allons étudier est dans cette lignée, par défaut un processus ignore qu'il fonctionnent sur un ordinateur avec d'autres processus. Ce concept représente une ou plusieurs fils de calcul pour realiser une tache agisant ensemble, en utilisant des ressources sur un ordinateur.

Un processus par défaut à l'illusion d'avoir l'ordinateur pour lui seul, le système d'exploitation propose cette abstraction pour lui. Par exemple: vous demandez de la mémoire on vous réponds oui ou non mais vous n'avez pas à demander aux autres programmes pour en avoir.

Les ressources d'un processus sont par défaut privée à lui, un autre processus n'est pas en mesure sauf partage d'avoir accès aux données d'un autre. On vera qu'il existe diverses exceptions ou méthodologies pour partager.

Vos processus dit utilisateurs ont peu de droits sur le matériel. Si on devait donner un diagramme des codes les plus privilègiées aux moins privilégiée une version simple serait: Kernel > Processus de l'administrateur (sudo) > processus utilisateurs

Le résumé:

Un processus possède par défaut :

Partage avec:

Un processus doit demander au système pour:

Pour faire le lien avec notre cours précédent, plusieurs ressources sont accèssibles, simplement avec open, write, read d'autres utilisent un appel système dédiée. C'est un choix de réalisation pris par les personnes develloppant un noyau de système d'exploitaton.

Création

Si vous souhaitez créer un nouveau processus la manière de faire sous unix est via la fonction fork, sous linux l'appel système est clone cependant nous utiliseront fork car clone est un appel système assez complexe à maitriser.

-> Fork concept du processus etc

Note: En informatique vous verrez souvent le mot «paradigme», ce mot sert à désigner une façon de structurer, concevoir les choses.

Exercice sur les processus

Création, éxecution: µshell

Objectifs:

Dans ce tp nous allons partir du programme minimal suivant:

#include <unistd.h> // fork, getppid, getpid
#include <stdio.h>  // printf
#include <sys/types.h> // pid_t etc

int main(void) {
  // /!\ Après cette ligne notre programme s'execute dans 2 procesuss différents. /!\
  pid_t fork_data = fork();
  // On récupére le process identifier de l'enfant et du parent.
  pid_t pid = getpid();
  // On récupére le pid du parent.
  pid_t ppid = getppid();
  if (fork_data > 0) {
    printf(
    "Je suis la maman chat,
    "mon pid est: %i, celui de mon chaton est: %i,"
    "celui de mon parent %i\n", pid, fork_data, ppid
    );
  } else if (fork_data == 0) {
    printf("Je suis le chaton, mon pid est: %i, celui de ma maman chat est: %i\n", pid, ppid);
  } else {
    perror("Quelque chose d'imprevu est arrivé, fork impossible.");
  }
  return EXIT_SUCCESS;
}

Questions

Que se passe t'il si le parent s'arrête avant le printf de son enfant? Quel sera la valeur de ppid, comment corriger pour que le parent attendent la fin de son enfant? indice: waitpid.

Maintenant, que le parent attend bien son enfant, on souhaite executer le programme (compilé) suivant dans l'enfant:

// hi.c
// executable hi:
// Compiler avec gcc -Wall -Wextra hi.c -o hi
#include <stdio.h>
void main(void) {
  puts("Hello I am an awesome process");
}

Pour ce faire on peut utiliser execve qui permet d'executer une image mémoire (votre executable), en lui donnant ses arguments et son environnement. Inspirez vous des exemples du manuel man 2 execve pour écrire ce mini-programme.

Pour ne pas avoir à gérer le path, commencez par écrire en dur dans votre microShell, le path du programme hi ci dessus.

Étapes bonus:

À l'issu de ses deux étapes bonus félicitation vous avez un micro-shell!

Exercices en plus

Commandes shell utiles sur les fichiers

A ce titre sur Linux ou MacOSX il existe quelques commandes très pratiques pour manipuler des fichiers voici une liste non exhaustive que nous utiliseront souvent.

cd : Changement de dossier de travail courant

La commande cd cd permet de changer de dossier de travail courant (cwd: current working directory) donc de vous déplacer dans vos dossiers.

L'appel système indispensable pour que cette commande fonctionne est chdir ou bien fchdir. chdir manipule un chemin _path_,fchdir` directement un descripteur de fichier.

// Extrait du man 2 chdir
#include <unistd.h>

int chdir(const char *path);
int fchdir(int fd);
Exercice : Personnal teleporter

Difficulté: Facile Temps estimé: 1h à 2h.

Objectif:

Concepts de C neccessaires:

L'objectif de ce tp est d'écrire un programme qui permet de se téléporter dans votre système si on lui donne un chemin valide en argument.

Il ne s'agit absolument pas d'une pâle copie de la commande cd re-stylisé pour être compatible avec le 21ème siècle.

Écrire un programme qui prends un argument et change votre dossier courant pour le dossier disponnible au bout du path donné en argument.

Bonus:

pwd : print current working directory

syscall: getcwd

#include <unistd.h>

char * getwd(char * buf, size_t size);

Note d'ouverture: Sur certains système, getcwd peut être implementé sans syscall mais juste une surcouche wrapper au dessus de fonctions plus basiques, c'est un choix de design.

Exercice: WhereAmI

Objectif:

Écrire un programme en C, nommé qui affiche le chemin complet vers votre dossier courrant, inspiré du programme pwd proposé dans la suite coreutils.

Voici le squelette du programme pour faire cette tache:

#include <stdio.h> // printf
#include <stdlib.h> // size_t
#include <unistd.h> // getwcd.h

int main(void) {
  // On demande un espace contigue de 256 bytes
  // on aurais pu la macro BUFSIZ a la place de 256.
  char buffer[256];

  // Quelle fonction utiliser? Voir les paragrames au dessus
  // Quel paramettre on donne pour recevoir le chemin courant
  get???(???, sizeof(buffer));

  printf("Tu es dans: %s\n", buffer);
  return EXIT_SUCCESS;
}
$ cd /home/axel
$ # On suppose que votre programme est dans votre home
$ ./whereAmI
Vous êtes dans: /home/axel/

Bonus: