# Systèmes d'exploitation

## Concepts et implémentations appliqués à l'architecture Intelx86

Stéphane Duverger, Airbus Group Innovations

# Introduction

Ensemble de composants logiciels qui permet de faire le lien entre tous les composants matériels.

- User programs
- ....
- User interface
- System calls
- IO /
- ....
- Hardware

## Principaux objectifs d'un OS

### Offrir des services

- Gérer le hardware, les I/O
- En temps et en espace
- Exécuter des applications

### Quelques uns des services

- **Communication entre applications**
  - Systèmes de fichiers
  - IPC (inter process communication)
- **Communication extérieure (réseau)**
  - Piles protocolaires - OSI
  - Drivers wifi, ethernet
- **Couches graphiques**
  - gestion du framebuffer
  - accélération matérielle 3D
- **Sécurité**
  - Isolation
  - Limitation
  - Gestion d'erreurs

## Le kernel, ou noyau de l'OS

Dans un OS, on scinde en général deux mondes, le kernel et le UserLand. On s'intéressera ici quasi exclusivement au Kernel.
Il s'agit du composant majeur et critique de l'OS. Il impose ainsi une architecture des appels systèmes. Il existe trois architecture typiques :

- Noyau Monolithique
- Micro-Noyau
- Exo-noyau, Hybride

<p align="center">
  <img src="./Images/kernel_types.png">
</p>

Le KernelLand est la partie ayant le plus de privilèges dans la machine, d'ou le risque d'un user _root_ qui obtient tous les privilèges et peut ainsi pénétrer le Kernel land.

## Comment marche malloc() ?

```malloc``` sert à allouer de la mémoire depuis le Userland. ```malloc``` va récupérer des pages de mémoires au niveau du kernel qu'il va pouvoir agencer comme il le souhaite en fonction des besoins de l'application.
Pour demander de la mémoire au noyau, ```malloc``` utilise un appel système.

## Objectifs du cours

**Rappels des principes des OS**

- Quel que soit leur design/architecture/concepts
- Pas de théorie sur
  - Les ordonnanceurs, le temps réel
  - La gestion abstraite de la mémoire (LRU...)
  - Les architectures de noyaux (monolithic, micro...)

**Implémentation pour Intelx86**

**Objectif**

- Aborder la programmation des OS, en particulier du point de vue CPU

# Démarrage d'un PC

Au démarrage du PC, le BIOS prend la main. Composant logiciel embarqué sur la carte mère mappée en mémoire à l'adresse 0xfffffff0 qui est là première chose cherchée par le processeur au démarrage.

Puis saute en 0xf0000, qui est la BIOS code area.

**B.I.O.S. - Basic Input Output System**
Responsable de l'initialisation des composants essentiels du système

- Controleur de RAM
- Table de gestion des interruptions
- Tables ACPI ()

Le BIOS permet de booter sur un device qui peut alors contenir un OS :

- HDD/SSD
- Disquettes
- USB

**Cas d'un HDD**

- Tables de partitions, boot flag
- **Master Boot Record**, premier secteur du disque CHS(0, 0, 0) qui contient l'info initiale de boot
- **Volume Boot Record**, premier secteur d'une partition bootable

**Boot Sector**

- Contenu dans 512 octets et se termine par "\x55\xAA" (marqueur de fin)
- Chargé par le BIOS en 0x7c00
- Permet de charger en mémoire le **Boot Loader**.

**Boot Loader**

- A pour bot de charger le noyau de l'OS et simplifier son démarrage (mode protégé...)
- Plus gros qu'un boot-sector, un mini-OS parfois
- Permet la passerelle entre le noyau et le BIOS :
  - Détecte les périphs de stockage
  - Parcourt le file system
  - Récupère les system map (i.e. la taile de la RAM)+

Quelques bootloader connus :

- LILO historique et minimaliste
- u-Boot pour l'embarqué ARM
- GRUB, spécification multi-boot, très courant

# Modes opératoires

On diffère plusieurs modes opératoires pour le CPU:

- Réel
  - Mode d'exécution du CPU au démarrage
  - BIOS + Boot Loader puis tout début de l'OS puis switch en protégé.
- Virutel 8086
- System management - SMM
  - Mode de privilège absolu, au dessus du kernel.
- Protégé
  - Mode usuel de fonctionnement pour l'OS.
- IA32e - x86-64
- Virtualisé - VMX

Il est possible de passer d'un mode à l'autre suivant ce schéma :

<p align="center">
  <img src="./Images/transitions_mode_cpu.png">
</p>

## Rappels ASM x86

**Registres généraux**

- Multi-usages : [r,e]ax, [r,e]bx, [r,e]cx, [r,e]dx, [r,e]si, [r,e]di
- Pour la pile : [r,e]sp, [r,e]bp

**En 64bits r8-r15**

- Selon le mode d'exécution (16/32/64) et/ou l'usage de préfixe
  - ax = 16 bits, eax = 32 bits, rax 64 bits.

**Registres de segments - sélecteurs**

```x86asm
cs, ss, ds, es, fs, gs 
```

**Registre systèmes**
Inaccessibles depuis le UserLand

```x86asm
controle : cr0, cr2, cr3, cr4
segmentation : gdtr, idtr, ltr, tr
model specific : MSRs
```

Les **registres de controle** permettent d'activer des fonctionnalités du CPU, dont le passage de mode réel à mode protégé (```cr0.pe```) et d'activer la pagination (```cr0.pg, cr3```).

## Le Mode réel

### Gestion des interruptions en Mode Réel

Il s'agit du mode initial du CPU qui permet une interraction facile avec le BIOS. Il se base sur un mode d'exécution 16 bits avec un adressage de 20 bits. Dans ce mode de fonctionnement, il n'y a aucune notion de privilèges, donc pas de protection.

Premiers x86  : 8086,80186,80286 qui permettent des modes pseudos protégés non désactivables

Des segments de 64k

```x86asm
mov ax, 0x1234
mov [ax], 0xdead
```

- Registre d'offset sur 16bits => $2^{16} =$ 64 kb
- Bus d'afressage sur 20 bits => $2^{20}$ = 1Mb
- Usage des registres de segments
- Calcul du CPU pour trouver une adresse : $addr = selecteur_{registre}*16 + offset$

Si on essaye

Les adresses mémoires que l'on manipule dans les architectures x86 sont en fait des offset relatifs au début des segments.

### Gestion des interruptions en Mode Réel

Les périphériques génèrent des interruptions. Ces signaux sont interprétés par le PIC, ou APIC _le controleur d'interruption (avancé)_ qui les signale au CPU un index donné. Le CPU consulte par la suite la table des vecteurs d'interruptions située en 0 (IVT).

Chacune des entrée fait 32 bits : 16 bits d'offset, et 16 bits de segment. Cette TVI est limitée à 256 entrées.

<p align="center">
  <img src="./Images/interrupt.png">
</p>

Ce Mode réel montre rapidement ses limites

## Les Modes exotiques

### Virtual 8086

- Permet l'émulation hardware du mode réel depuis le mode protégé.
  - Accès aux interruptions du mode réel
  - Interception des I/Os
  - Gestion de la mémoire < 1Mb laissée à l'OS

_Un exemple : DosBox_

### System Management Mode

- Mode le plus privilégié
- Utilisé par les firmware - BIOS
- Configuré au boot
- accessible via une System Management Interrupt
- Généralement difficilement accessible

### VMX

- Gestiond e la virtualisation matérielle
- 2 Modes d'exécution
  - ```vmx-root``` pour l'hyperviseur
  - ```vmx-nonroot```, pour lamachine virtuelle
- Permet de filtrer la plupart des événements systèmes
  - instructions sensibles
  - interruptions/exceptions
  - la gestion de la mémoire
  - accès aux périphériques
- Proposer une vision contrôlée de la machine

## Le Mode Protégé

# Segmentation

## Gestion mémoire du mode protégé

En mode protégé, on a un espace mémoire en 32 bits, ce qui engendre un espace linéaire de 32 bits => $2^32$ <=> 4Gb. Pendant un temps supérieur à la RAM installée. On va segmenter cet espace, et ainsi mettre en place un adressage relatif au début d'un segment(adresse logique).

## La segmentation

Il s'agit donc d'une prolongation du principe de segments du mode réel. On a plusieurs types de modèles, _flat/protected_ et _flat/multiy-segment_. Chaque segment a des propriétés de type, de taille et de droits.

Avec cette segmentation on peut réellement découper la mémoire et attribuer certaines propriétés à ces segments de mémoire. On peut donc créer des segments read only, read write, execution...

<p align="center">
  <img src="./Images/segments.png">
</p>

Pour faciliter ce fonctionnement, on met en place une table GDT - Global Descriptor Table - par coeur de CPU. Cela permet de faciliter l'accès à des sélecteurs de segments et des descripteurs de segments. Les descripteurs contiennent les informations sur le type de données écrites dans la mémoire et leurs droits associés, tandis que les sélecteur continnent les adresses.

<p align="center">
  <img src="./Images/tables.png">
</p>

## Les sélecteurs de segments

Ils permettent l'accès à un descripteur donné. Chaque sélecteur permet de définir :

- Un niveau de privilège
- Une table (locale ou globale)
- Un indice de descripteur dans cette table

<p align="center">
  <img src="./Images/register_segment.png">
</p>

Les sélecteurs de segments sont en général **CS, DS, SS** qui pointent vers une table GDTR en mémoire qui elle contient les descripteurs de segments.

## Descripteur de segment

Ils contiennent toutes les informations relatives auc segments :

- base et limite
- type de segment
  - code (X,RX), data (R,W)
  - système : TSS, Task Gate, Interrupt Gate, Call Gate

<p align="center">
  <img src="./Images/descriptor.png">
</p>

- Base : Début du segment
- Limite : Taille - 1, dernier octet accessible
- Granularité, on l'utilise pour multiplier nos bits de limite et ainsi pouvoir arriver à 4GB.

# Niveaux de privilèges

- mode réel n’avait qu’un seul niveau (ring 0)
- mode protégé introduit des anneaux (ring level)
- permet d’isoler des composants logiciels de niveau
  - de confidentialité différents
  - d’intégrité différents  

<p align="center">
  <img src="./Images/rings.png">
</p>

## Identification du niveau de privilèges

- CPL, Current Privilege Level, contenu dans CS
- RPL - Requestor Privilege Level, au niveau du sélecteur
- DPL Descriptor Privilege Level, au niveau du descripteur

## Changement de niveau de privilèges

- On ne peut accéder à un segment de données plus privilégié
- On ne peut charger un descripteur de code qu'à niveau de privilège équivalent
  - Aussi bien plus faible que plus privilégié
  - Passer par des call gate ou interrupt gate

<p align="center">
  <img src="./Images/privileges.png">
</p>

## En résumé

Des descripteurs sécurisants

- les niveaux de privilèges isolent les composants : Qui ?
- les bases et limites aussi à leur manière : Où/Combien ?
- les attributs également (read, write, ...) : Quoi/Comment ?

Du point de vue de la sécurité

- simulation d’une architecture harvard (contre von Neumann)
- imaginez un descripteur par buffer ? anti-overflow
- Linux Kernel PaX protection : SEGMEXEC
_<https://pax.grsecurity.net/docs/segmexec.txt>_

Et pourtant ...

- les OS modernes n’utilisent pas la segmentation (modèle flat)
- quasiment disparue en 64 bits
- basent leur sécurité sur la pagination

# Interruptions et Exceptions

Les interruptions et les exceptions ont pour intérêt d'éviter l'attente active et bloquer le CPU, d'apporter une composante événementielle à l'OS et peut être réveillé sur demande en fonction des besoins du programme.

Les exceptions sont générées

- par le CPU, en cas d'erreurs (#GP, #NP).
- par du code
- ou d'autres sources
  - fault, on la gère et on ré-excécutee l'instruction
  - trap, on la gère et on continue après l'instruction
  - abort, non récupérable

Il y a une classification qui permet une priorisation des exceptions en cas de déclenchement simultanné.  En général, chacune des erreurs vient en général avec un code d'erreur.

## Exceptions Usuelles

- General Protection Fault #GP
  - Survient dans de nombreux cas
    - Liés  la segmentation (taille, droits, pvl...)
    - Configuration invalide des registres de contrôle, MSRs
  - Du type fault
  - Fournit un code d'erreur
    - Sélecteur de segment fautif si erreur de segmentation
    - ou 0 pour les autres erreurs
- BreakPoint #BP
  - Survient quand l'instruction int3 est exécutée
  - Du type trap
  - Pas de code d'erreur

## Interruptions 

Qui dit Interruptions dit aussi reprise d'exécution, ainsi, au moment  de l'interruption, l'état actuel du CPU doit être sauvegardé. Ainsi, on doit mettre en oeuvre les concepts de pile d'interruption, d'Interrupt Gate (descripteur de segment d'interruption), et d'appels systèmes (implémentation historique).

## Table des interruptions IDT

Les descripteurs référencent des segments de code et du handler associé. Ils indiquent également le DPL nécessaire pour y accéder qui sont généralement en ring 0, à l'exception du handler d'appels systèmes qui sera en ring 3.

<p align="center">
  <img src="./Images/IDT.png">
</p>

## Contexte d'interruption

On parle du contexte pour indiquer l'état du CPU à un instant donné. Cela sous entend 

- Un niveau actuel de privilèges
- La valeur des registres généraux (eax, ebx, ..., esp)
- Le registre d'état (EFLAGS)

Dans le cadre du déclenchement d'une exception/interruption, on peut potentiellement changer subitement le niveau de privilèges (ring 3 => ring 0)

Le CPU doit donc sauvegarder les informations d'état du niveau de provenance pour y retourner après l'exécution de l'exception. Le noyau s'occupe du reste.

**En trois étapes**

- Au moment de l'interruption, le CPU sauvegarde le minimum et détermine s'il y a eu changement de niveau de privilèges
- Avant de traiter l'interruption, l'OS sauvegarde ce qu'il pense être nécessaire de restaurer
- Au retour de l'interruption, l'OS utiliser une instruction spécifique (iret)

## Comment le CPU sait où sauvegarder ?

Dans le cas d'une transition ring 3 => ring 0 :

- Le CPU cherche à changer de pile (registre esp)
- Sauver les informations du ring 3 dans le ring de destination

Mais l'une des question serait , où trouver la nouvelle valeur à mettre dans esp ?
On peut y parvenir grâce au TSS - Task State Segment

# Pagination

Introduction d'un nouveau système de ségragation mémoire, avec l'arrivée de nouveaux concepts, comme les adresses virtuelle. C'est permis par un nouveau composant qui permet les calculs, la MMU, Memory Management Unit

## Définiition 

On a un programme qui va avoir des adresses de 1000 à 2000 (adresse relative), qui va être traduite vers une adresses linéaire qui n'est plus nécessaireemnt l'adresse physique finale dans la RAM. Avec la segmentation, au démarrage, bien qu'on ait pas accès à la RAM entière, on pouvait écrire un peu n'importe où, sans que cela ait d'impact. Avec la pagination, on peut dire rajouter une étape, qui va permettre une traduction des adresses linéaires, qui fait que l'on peut dire que l'adresse linéaire 0xffffff (non accessible par segmentation) correspond en réalité à l'adresse 0x00. La pagination introduit donc des une table de traduction des adresses virtuelles  -> adresses physiques par taches.

On parle ici

- Espace d'adressage
- mappping memoire
- page trables/directory (via la MMU)
- page table entries (PTE)

<p align="center">
  <img src="./Images/pagination.png">
</p>

Chacue page de mémoire physique fait 4kB, et on n'a qu'un seul Page Directory, avec 1024 entrées (PDE - Page DIrectory Entry, 4kB)

PDE 

- 1024 entrées, Page Directory Entry, 4kB
  - Une entrée => Une Page Table 
  - Une entrée définit l'adresse physique d'une Page Table

Page Table

- 1024 Entrées, Page Table Entry
  - Contient l'adresse de la mémoire physique associée à cette table

Page 
- 4kB

Une page table permet de mapper 4Mb de mémoire => Un Page Directory permet de mapper 1024  Page Table => 1024*4Mb = 4Gb.

Si on a une continuité de pages dans les adresses des pages au niveau de l'entrée d'une page table, on peut également définir des blocs de pages pour avoir une seule page mémoire plus grande.

### Dans le détail 

- le PGD voit son adresse physique stockée dans cr3
- chaque table contient au plus 1024 entrées de 4 octets
- une entrée de table (PTE) définie une page de 4KB
- une table de pages couvre donc 1024 ∗ 4KB = 4MB d’espace virtuel
- une entrée de répertoire (PDE) peut définir
- une table de pages de 1024 entrées
- une page de 4MB (bit pse)
- un répertoire de pages couvre donc 1024 ∗ 4MB = 4GB d’espace virtuel

ON REECRIT CR3 POUR CHACUN DES PROCESS. ET ON DEFINIT AINSI UN PAGE DIRECTRORY PAR PROCESS. 

A chaque changement de process, on doit flusher les traductions d'adresses, pour ne pas qu'un autre process puisse accéder aux mémoires associées aux autres process. 

Ce tableau de correspondances @virt <=> @phys s'appelle la TLB, et est flushé à chaque changement de process et de contexte.


**Comment le processeur traduit les adresses virtuelles en mode 32 Bits ?**

On découpe une adresse virtuelle en 3 parties : 

- 10 bits de poids fort indique l'index de l'entrée dans le page directory
- 10 Bits suivant servent d'index dans la table de page => adresses physique de la page de mémoire associée, à l'offset 0.
- 12 bits de poids faibles qui servent d'offset pour se ballader dans la page de mémoire physique.

<p align="center">
  <img src="./Images/v_addr.png">
</p>


## Découpage des entrées des Page Tables et Page Directory

- Bit 0 : Flag Present, qui génère une Page fault si = 0
- Bit 1 : Read or Write : 0 = Read Only, 1 = Read Write
- Bit 2 : Niveau de privilège, 0 = User ou 1 = Superviseur

<p align="center">
  <img src="./Images/desc_PDE_PTE.png">
</p>

Il a fallu attendre les processeurs 64 bits pour avoir des PTE plus grande, et ainsi préciser en plus de R/W, l'aspect exécutable d'une page de mémoire.

Si on a plusieurs instances d'un même process, si le Bit G est à 1, on peut la conserver dans les caches de traduction malgré un changement de contexte.

## En 64 bits...

<p align="center">
  <img src="./Images/64bits_vaddr.png">
</p>

## Sécurité et Pagination

La sécurité proposée par la pagination par défaut est vraiment relative, on a quasiment aucune protection noyau/tache, du au bit User/Superviseur, les données sont peu protégée, on a uniquement un bit read/write qui peut impliquer de l'exécution... et on empêche l'opération XOR.

Mais petit à petit, le système a été amélioré au fil du temps. On a vu la mise en place de nouvelles normes et notamment la mise en place de iTLB pour les isntructions, et du dTLB pour les données.

Sous Linux, un patch a été publié sous le nom de PAGEEXEC rajoute une couche de protection.

Sur les architectures 64 bits, on a vu arriver au niveau des PTE un bit NX, qui empêche une page d'être exécutable. 

Enfin, d'autres protections ont été mises en oeuvre via cr0, à savoir la mise en place d'un write protect et d'un SMEP/SMAP qui contrôle l'acces et l'exécution.
# Composants d'un OS

# Conclusions
