Introduction Ă la Virtualisation (Partie 1)
Lecture 18 min. âą
Table des matiĂšres
- Code Machine et Hardware
- Assembleur et Instructions
- ExĂ©cution dâun programme
- Multi-Processus
- Conclusion
Bonjour Ă toutes et tous đ
Bienvenue dans un article encore une fois bien trop long.
Ce devait ĂȘtre lâintroduction dâun article sur la crĂ©ation dâimage disque dans un pipeline Gitlab CI, je vais pas vous le cacher, ça a un peu dĂ©rapé⊠đ€
Je vous propose un tour dâhorizon des concepts de la virtualisation que jâutiliserai par la suite dans lâarticle prĂ©vu au dĂ©but.
Mais comme je sais pas mâarrĂȘter et que jâaime que nâimporte qui puisse comprendre quelque soit son niveau de dĂ©part, il y aura de lâextra đ
Avant de commencer.
Dâabord, un grand merci Ă Samuel Ortez, Julien Durillon et Pierre-Antoine GrĂ©goire pour la prĂ©sentation que je vais tenter de rĂ©sumer et complĂ©ter sans dĂ©former dans les lignes qui vont suivre.
Prenez une inspiration, on descends ! đ€ż
Code Machine et Hardware
Oui, comme ça direct. ^^
Ne vous inquietez pas je vais vous guider.
Code Machine
PremiĂšrement ce quâil faut savoir, câest quâun ordinateur est extrĂȘment idiot, il ne parle ni français ni anglais, il ne connais quâun langage, le Code Machine, basiquement des 0 et des 1.
Impossible de lui donner Ă manger du C, du Java ou du JavaScript, il nâest pas fait pour ça.
Dans un ordinateur, ce qui est responsable de comprendre et dâexĂ©cuter du code se nomme un CPU (Central Processing Unit).

De mĂȘme, un CPU parle un Code Machine bien spĂ©cifique et ne saura comprendre un Code Machine diffĂ©rent.

Â
Jâai fait abstraction de lâinterprĂ©tation et de la semi-compilation, plus de dĂ©tail dans cet article
CPU
Voyons un peu plus en dĂ©tail les diffĂ©rents composants dâun ordinateur.
Le premier, nous lâavons dĂ©jĂ nommĂ© est le CPU.

Il est composé de deux parties principales (je fais abstraction de ce qui nous intéresse pas ).
- Les Registres : R1, R2, ⊠et Acc
- Lâ Arithmetic Logic Unit ou ALU ou UAL en français (UnitĂ© ArithmĂ©tique et Logique)
Les Registres sont des petites mémoires qui ne permettent (en simplifiant) que de stocker une valeur.
Plus dâinfos dans cet article
Ils sont en quelques sortes lâĂ©tabli utiliser par un artisan pour poser ses planches avant la dĂ©coupe.
Dans ces registres, un est un peu spĂ©cial et se nomme lâAccumulateur, ici je le reprĂ©sente par un Acc.
Si les registres sont lâĂ©tabli, alors lâALU sont les outils de lâartisan, son travail est de manipuler les registres pour faire des opĂ©rations mathĂ©matique ou des transferts de donnĂ©es.
RAM
Mais je lâai dit plus haut, un registre ne contient quâune seule valeur, ici on a 5 registres, donc on peut stocker 5 valeurs, nos programmes vont ĂȘtre courts đ€
LâidĂ©e est alors de stocker ailleurs la donnĂ©e, la place sur le CPU Ă©tant limitĂ©e, on vient stocker dans un autre composant appelĂ© la RAM.

Le CPU communique avec la RAM au moyen de deux canaux :
- bus dâadresse : dĂ©fini ce que le CPU veut atteindre
- bus de données : transfert les données dans le sens CPU -> RAM ou RAM -> CPU
Précisions
Ici je fais abstraction du cache CPU, dâailleurs câest en partie pour ça que les Apple M1 sont trĂšs rapides, ils ont beaucoup de cache ^^
Jâassimile toute mĂ©moire de travail Ă de la RAM pour plus de facilitĂ©.

On peut globalement y stocker nâimporte quoi, tant que ce sont des 0 et des 1. Et ça tombe bien notre programme est du Code Machine pouvant se reprĂ©senter par du binaire.
Chaque donnée est adressée par un nombre.
Dans cet exemple le CPU demande le contenu de lâadresse @500 Ă la RAM.
Chouette on peut stocker plus de 5 valeurs. đ
Périphériques
Mais le CPU Ă part faire des calculs, il ne sert pas Ă grand chose, il faut au moins une imprimante comme dans lâancien temps pour lire le rĂ©sultat.

Câest pour cela que lâon a inventĂ© les PĂ©riphĂ©riques. Ceux-ci peuvent ĂȘtre nâimporte quoi: un disque dur, la carte graphique, la carte rĂ©seau, lâimprimante qui marche jamais quand vous en avez besoin, etc âŠ

Assembleur et Instructions
Câest bien beau tout ça mais le binaire et lâesprit humain cela fait 2 (sans mauvais jeu de mots ^^â), câest pour cela que lâon ne manipule pas directement les 0 et les 1.
On le remplace par une sorte de langage qui est transcriptible en 0 et 1.
Assembleur
Ce langage se nomme de lâAssembleur ou ASM
Il existe autant dâassembleur quâil y a de de Code Machine diffĂ©rents et plusieurs Assembleur diffĂ©rents peuvent mener au mĂȘme Code Machine.
Cet assembleur est la premiĂšre abstraction créé par lâHumain pour lâaider Ă parler le Machine sans devenir fou!
Un exemple dâassembleur (rassurez-vous on dĂ©taille aprĂšs đ).
LOD @5Va ĂȘtre transcrit en utilisant le Code Machine de notre CPU fictif en 0101010

Notre ligne de programme en Assembleur peut alors se décomposer en deux parties
- OpCode : ce que lâon doit faire
- Opérandes : avec quoi ?
Un autre exemple dâinstruction
STD @5Qui devient

On voit ici que le OpCode change la transcription binaire, il aurait Ă©tĂ© de mĂȘme si lâopĂ©rande avait Ă©galement Ă©voluĂ©.
La langue du Code Machine est appelĂ© un set dâinstructions.
Instructions
Nous avons lâalphabet attaquons-nous aux mots.
La langue du CPU est décomposé en instructions, voici celle de notre CPU fictif.
| OpCode | Operande 1 | Operande 2 | Signification |
|---|---|---|---|
| MOV | RX | D | DĂ©place la valeur D vers le registre RX, D peut ĂȘtre un autre registre |
| LOD | RX | @A | Charge la valeur Ă lâadresse @A depuis la RAM vers le registre RX |
| STD | RX | @A | Stocke la valeur contenu dans le registre RX Ă lâadresse @A de la RAM |
| ADD | X | Ajoute la valeur X Ă Acc, X peut ĂȘtre un registre | |
| OUT | X | #VX | Ecrire X sur le périphérique #VX |
| JMP | @A | Sauter Ă lâinstruction Ă lâadresse @A | |
| JNZ | RX | @A | Sauter Ă lâinstruction Ă lâadresse @A, si la valeur dans le registre RX nâest pas 0 |
| HLT | Stop le CPU et met fin au programme |
Maintenant que nous avons des mots, nous allons pouvoir faire des phrases.
ExĂ©cution dâun programme
Ces phrases vont ĂȘtre notre programme.
Je vous propose celui-ci.

Chaque ligne du programme est stockĂ©e Ă une adresse, ici on dĂ©bute Ă lâadresse @500, biensĂ»r câest la reprĂ©sentation binaire qui est stockĂ©e et non lâassembleur.
Nous avons donc 3 questions à résoudre :
- La ligne du programme est dans la RAM, comment la transmettre au CPU ?
- Une fois dans le CPU, comment fait-il pour comprendre la ligne ?
- Que se passe-t-il Ă lâexĂ©cution de la ligne ?
Pour cela, nous allons introduire 3 notions:
- Fetch
- Decode
- Exec

Notre curseur dâexĂ©cution est Ă lâadresse @500, ce qui veut dire que la ligne dâinstruction Ă rechercher dans la RAM est Ă lâadresse @500.
Le CPU demande donc le contenu de lâadresse @500 Ă la RAM au travers du bus dâadresses, la RAM lui renvoie ce contenu.
Cette opération est appelée le Fetch.

Une fois le contenu binaire de lâadresse @500 en main, celui-ci est dĂ©codĂ©.

Ici le dĂ©codage nous annonce que lâopĂ©ration Ă rĂ©aliser de mettre la valeur 1 dans le registre Acc.
MOV ACC, 1Maintenant que le CPU, sait quoi faire, il va pouvoir lâexĂ©cuter.

Dans le cas présent, mettre un 1 dans le registre Acc.
On passe alors Ă la ligne suivante en incrĂ©mentant le curseur dâexĂ©cution.
Et du coup, fetch de lâadresse @501 cette fois-ci.

Décodage

en
MOV R1, 4Puis exécution

Nous avons maintenant un 1 dans lâaccumulateur et un 4 dans le registre R1.
On déroule le programme.
Celui-ci nous demande dâadditionner le contenu de lâaccumulateur avec le contenu du registre R1 et de mettre le rĂ©sultat dans lâaccumulateur.
ADD R1
Nous avons maintenant 5 dans lâaccumulateur.
On continue.
Plus compliquĂ©, on nous demande de stocker le contenu de lâaccumulateur dans la RAM Ă lâadresse @15.
STD ACC, @15
Le CPU va adresser la RAM pour que la valeur 5 contenue dans lâaccumulateur soit stockĂ©e Ă lâadresse @15.
Ligne suivante, on fait le chemin inverse, on demande de charger le contenu de lâadresse @15 dans le registre R1.

Nous avons maintenant 5 Ă la place de 1 dans le registre R1
On déroule.
Nous devons déplacer ce qui est dans R1 dans R2. Facile.
MOV R2, R1
Nous avons maintenant 5 dans le registre R2.
Plus difficile, nous avons utilisĂ© les registres, lâALU, la RAM, mais pas encore les pĂ©riphĂ©riques.
Nous allons demander dâĂ©crire le contenu du registre R2 sur le pĂ©riphĂ©rique #12.
Ici, nous allons dire que câest du disque dur.
Chaque périphérique est un micro ordinateur, qui possÚde également des registres.
OUT R2, #12 Vient écrire sur le périphérique en voie 1 dans le registre X2, la valeur contenue par le registre R2.

5 est maintenant écrit dans le registre, le disque viendra alors faire quelque chose avec cette valeur, mais ceci est hors scope de notre article.
La derniÚre instruction, éteint le CPU.
HLTMulti-Processus
Race condition
GĂ©nĂ©ralement, nous nâavons pas quâun seul programme qui tourne sur un ordinateur.
Chacun de ces programmes sont stockés dans la RAM à des addresses différentes.
- Notre programme 1 dĂ©bute Ă lâadresse
@126 - Tandis que le programme 2 Ă lâadresse
@500

Comme nous nâavons quâun seul CPU, et quâil ne peut pas faire deux choses en mĂȘme temps, il faut faire les choses de maniĂšre sĂ©quentielle.
Nous allons départager le temps que nous passons dans chaque programme.
Dâailleurs lorsquâun programme tourne, nous allons lâappeler un processus.

Chaque passage entre processus sera appelé un context switch, on verra par la suite de quoi il en retourne.
Voici les deux programmes en questions.
Maintenant expert en assembleur, vous devriez les comprendre. ^^

On accĂ©lĂšre la montre pour se placer Ă lâinstruction
STD ACC, @15du programme 1, celle-ci Ă©crit comme tout Ă lâheure la valeur 5 Ă lâadresse @15.
Câest alors quâintervient le context switch, le processus 1 doit laisser la main au processus 2.

Nous sommes juste avant le dĂ©but du programme 2, le switch de contexte ne peut pas faire grand chose, il est sensĂ© remettre les valeurs du contexte dâexĂ©cution du processus 2.
Mais comme le programme nâa jamais tournĂ©, câest plus du bruit qui est dĂ©fini ou bien les valeurs prĂ©cĂ©dentes restent inchangĂ©es.
En tout cas, en lâĂ©tat le processus 2, ne sait pas ce quâil y a dans les registres.

Le programme 2 se déroule et atteint une instruction étrangement familiÚre.
STD R1, @15Comme un ordinateur est dĂ©terministe, une mĂȘme action, provoque le mĂȘme rĂ©sultat.
A ceci prĂȘt que lâopĂ©ration prĂ©cĂ©dente avait dĂ©fini la valeur de R1 comme Ă©tant 0

Donc maintenant lâadresse @15 vaut 0 !!!
Nous venons de crĂ©er une race condition : deux acteurs qui utilisent la mĂȘme ressources sans se synchroniser sur lâĂ©criture.
Temps écoulé pour le processus 2, il passe la main.
Le context switch restaure les états des registres pour que le processus 1 puisse de nouveau travailler.
On remet :
- 4 dans le registre R1
- 5 dans lâaccumulateur
On restaure Ă©galement le pointeur dâexĂ©cution.

On incrĂ©mente le curseur dâexĂ©cution.
LOD R1, @15
Ah bah oui, mais non âŠ
Le 5 nâest plus lĂ . Il a Ă©tĂ© prĂ©cĂ©demment remplacĂ©.
Le programme 1, nâest dĂ©sormais plus dĂ©terministe, il est capable de faire nâimporte quoi.
Echec et mat !
Sommes-nous condamner Ă nâexĂ©cuter quâun seul programme par CPU.
Non, biensûr que non.
Mais pour ça, nous allons avoir besoin de plus de choses.
Kernel et MMU
Il va nous falloir un chef de gare pour gérer un peu la discipline dans les rangs.
Je vous le prĂ©sente, il sâagit du Kernel.

Son rĂŽle va ĂȘtre de gĂ©rer les processus et de les empĂȘcher de faire nâimporte quoi.

Et par nâimporte quoi, je veux dire Ă©crire nâimporte oĂč dans la mĂ©moire.
Le but Ă©tant dâĂ©viter la race condition, mais plus gĂ©nĂ©ralement que chaque processus reste chez lui et ne puisse pas aller taper de la mĂ©moire qui ne lui appartient pas ou qui est dĂ©jĂ utilisĂ©e.
Il a encore un milliard de rĂŽles diffĂ©rents supplĂ©mentaires, mais on reste simple, câest de la vulgarisation, pas un cours.
Pour lâaider dans sa tĂąche de cloisonnement, le Kernel peut faire appel Ă un outil.
Celui-ci se nomme la Memory Management Unit.

Il sâagit globalement dâune liste dâadresse virtuelles qui pointent sur des adresses physique.
Pourquoi virtuelle ? Parce quâelles nâont aucune existence physique, elle nâexistent pas dans la RAM, au contraire de lâadresse physique, qui est et bien physiquement prĂ©sente dans le silicium.
Pour permettre de sây retrouver et aussi dâavoir plusieurs fois la mĂȘme adresse virtuelle pour deux adresses physiques diffĂ©rentes, on dĂ©coupe la MMU en tables.
Ici par exemple on a deux tables, dans ces deux tables lâadresses virtuelle 15 existe, mais ne pointe pas vers la mĂȘme adresse physique.

Dans la table 1
$$15 \Rightarrow 1005$$
Dans la table 2
$$15 \Rightarrow 22$$
La MMU est un composant physique à la différence du Kernel qui est du logicielle.
La MMU en action
Nous allons utiliser cette propriĂ©tĂ© pour rĂ©soudre notre problĂšme de tout Ă lâheure.
Revenons dans le temps.
Nous sommes dans le processus 1, juste avant le stockage de la valeur en RAM.
Nous allons interposer la MMU sur le chemin du bus dâadressage.
Le CPU fait ce quâil faisait avant, demander de stocker Ă lâadresse @15, mais la MMU est configurĂ©e par le Kernel pour utiliser la table p1.
Ainsi, lâadresse subit la modification suivante:
$$15 \Rightarrow 1013$$

Notre 5 est alors stockĂ© Ă lâadresse physique @1013 et non @15.
Cette fois-ci, câest au processus 2 de vouloir stocker son 0 Ă lâadresse @15.
Mais mĂȘme principe, le Kernel dĂ©fini la table de la MMU sur p2.
Ainsi, lâadresse rĂ©ellement utilisĂ©e devient
$$15 \Rightarrow 2862$$

Son 0 est alors stockĂ© Ă lâadresse physique @2862 et non @15.
Lorsque le processus 1 revient Ă la vie.
Il va lors du chargement de lâadresse @15, lĂ aussi passer par le MMU.
Ainsi ce nâest pas @15 mais Ă nouveau @1013 qui est utilisĂ© pour adresser la RAM.

Et donc câest bien un 5 et non un 0 qui est dĂ©sormais stockĂ© dans le registre R1.
Le programme peut de nouveau continuer. đ
La MMU est un composant physique.
La MMU est une partie essentielle de la sĂ©curitĂ© dâun ordinateur, sans lui un programme malveillant ou mal conçu pourrait réécrire la mĂ©moire dâun autre programme ou mĂȘme le programme lui-mĂȘme.
Du fait que lâisolation soit matĂ©rielle, cela rend trĂšs difficile le contournement de la sĂ©curitĂ©.
Syscall
De la mĂȘme maniĂšre que nous ne voulons pas quâun processus prenne la main sur la mĂ©moire de ses petits camarades, nous allons isoler nos processus de lâaccĂšs direct aux ressources physiques.
Pour 2 raisons:
- Un programme ne sait pas forcĂ©ment parler âDisque durâ
- On veut pas quâil puisse effacer ce quâil veut le Kernel compris (oui les programmes sont sur disque avant dâarriver dans la RAM ^^)

Lorsque quâun processus dĂ©sire rĂ©aliser une manipulation qui nĂ©cessite autre chose que de la RAM, ce processus va rĂ©aliser une dolĂ©ance au Kernel, cette demande est appelĂ©e un Syscall.
Le syscall que nous allons utiliser se nomme read. Son boulot est demander la lecture dâune ressource un fichier par exemple qui serait sur le disque dur.
Mais de la mĂȘme maniĂšre que le processus ne savait pas parler âDisque Durâ, le Kernel ne sait pas non plus.
Heureusement, le Driver lui parle Ă la fois Kernel et Disque Dur, se sera donc notre interprĂšte dans la transaction.
Pour les besoins du dessin je simplifie tout, mais il y a bien plus de chose Ă dire ^^
DĂ©taillons ce quâil se passe lors de la lecture de ce fichier.

Le processus vient dĂ©finir quâil a besoin en mettant lâinformation dans un registre (ici pour lâexemple R2 ), le nombre est ce quâon appelle un file descriptor, câest un identifiant qui est connu du Kernel et quâil associe Ă une ressource, ici un fichier dĂ©jĂ ouvert par un prĂ©cĂ©dent syscall open.

Un JMP est une instruction appelĂ©e un Jump, celui-ci vient dĂ©placer le curseur dâexĂ©cution Ă lâadresse dĂ©finie ici @809 qui correspond au dĂ©but de notre syscall imaginaire et simplifiĂ© read_fd.
Le syscall fait des trucs, dont stocker de la donnĂ©e Ă lâadresse @10005.
Câest alors que des mĂ©canismes un peu complexes et que je nâai pas totalement saisis vont aller rĂ©veiller le Kernel qui piquait une sieste en attendant quâil se passe quelque chose dâintĂ©ressant.

Cette magie noire se nomme une interruption.
Les instructions qui vont suivre sont appelĂ©es privilĂ©giĂ©es car elle ne peuvent ĂȘtre exĂ©cutĂ© que dans un mode spĂ©cifique du CPU, mode qui ne peut ĂȘtre atteint que par le Kernel et ses modules dont les drivers.
Il va alors faire ce que le syscall lui demande, lire le fichier, mais comme il ne sait toujours pas parler Disque dur, il fait appel au Driver qui lui sait.

Finalement le Disque dur est lu, au travers de lâopĂ©ration
OUT R2, #12Le trajet de retour vers le processus est une autre histoire, peut-ĂȘtre un jour dans un autre article ^^â
Petit rĂ©capitulatif de ce qui sâest dĂ©roulĂ©.

Conclusion
Ainsi sâachĂšve la premiĂšre partie de notre Ă©popĂ©e dans la Virtualisation, vous avez lâintuition de ce qui se passe dans un ordinateur physique.
Dans la seconde partie on commencera Ă esquisser le fonctionnement de la virtualisation elle mĂȘme.
Merci de votre lecture â„ïž
