Pour aller plus loin : sous le capot
Quand lire cette page, et pourquoi elle est à part
Le cours Architecture de von Neumann nomme les trois bus et les trois temps du cycle. Cette page les détaille, et ajoute les exercices qui vont avec.
Viens-y après les défis LMC, pas avant : le cycle se comprend beaucoup mieux quand on a déjà écrit et fait tourner ses propres programmes. Rien ici n'est nécessaire pour les défis.
Le programme de première borne le cours aux concepts généraux du modèle. Ce qui est exigible tient donc dans la page de cours ; ce qui suit est du détail utile, et les trois dernières sections sont franchement hors programme.
1. Les trois bus en détail
Un bus n'est pas un tuyau dans lequel les informations circulent les unes après les autres : c'est un groupe de fils parallèles qui portent chacun, en permanence, un signal électrique dans l'un de ses deux états possibles.
Chaque bus est un groupe de fils physiquement séparé des autres. À chaque instant, la mémoire "voit" l'état de tous les fils simultanément. Il n'y a pas de mélange possible : chaque information voyage sur son propre groupe de fils.
C'est le signal sur le bus de contrôle qui déclenche l'action. Sans lui, la mémoire ne fait rien, même si une adresse est présente sur le bus d'adresses.
Exemple : ajouter 2 à la valeur stockée à l'adresse 42
Supposons que la case mémoire 42 contient la valeur 10. Le CPU doit calculer 10 + 2 et réécrire le résultat en mémoire.
Les trois bus travaillent simultanément à chaque étape. C'est leur combinaison qui donne un sens à l'opération : le bus de contrôle dit quoi faire, le bus d'adresses dit où, et le bus de données transporte le contenu.
Étape 1 : Lire la valeur en mémoire
CPU ══[ LIRE ]══► Mémoire (bus de contrôle : le CPU demande une lecture)
CPU ══[ 42 ]══► Mémoire (bus d'adresses : le CPU envoie l'adresse)
CPU ◄═[ 10 ]═══ Mémoire (bus de données : la mémoire répond avec la valeur)
Le CPU envoie en même temps l'ordre LIRE et l'adresse 42. La mémoire répond en plaçant la valeur 10 sur le bus de données. Le CPU la récupère dans l'accumulateur.
Étape 2 : Le calcul
L'UAL effectue 10 + 2 = 12. Cette étape est interne au CPU, les bus ne sont pas sollicités.
Étape 3 : Écrire le résultat en mémoire
CPU ══[ ÉCRIRE ]══► Mémoire (bus de contrôle : le CPU demande une écriture)
CPU ══[ 42 ]══► Mémoire (bus d'adresses : le CPU envoie l'adresse)
CPU ══[ 12 ]══► Mémoire (bus de données : le CPU envoie la valeur)
Le CPU envoie en même temps l'ordre ÉCRIRE, l'adresse 42 et la valeur 12. La mémoire reçoit les trois informations et écrit 12 à l'adresse 42.
À retenir : À l'étape 1, le bus de données va de la mémoire vers le CPU (la mémoire répond). À l'étape 3, il va du CPU vers la mémoire (le CPU envoie). Les bus d'adresses et de contrôle vont toujours du CPU vers la mémoire : c'est le CPU qui commande.
La case mémoire 42 contient maintenant 12.
Exercices sur les bus
Le CPU veut lire la valeur stockée à l'adresse 7
Complète le tableau :
| Bus | Valeur transportée | Sens |
|---|---|---|
| Bus d'adresses | ? | ? → ? |
| Bus de contrôle | ? | ? → ? |
| Bus de données | ? | ? → ? |
Réponse
| Bus | Valeur transportée | Sens |
|---|---|---|
| Bus d'adresses | 7 | CPU → Mémoire |
| Bus de contrôle | LIRE | CPU → Mémoire |
| Bus de données | la valeur à l'adresse 7 | Mémoire → CPU |
Le bus de données va de la mémoire vers le CPU car c'est une lecture : la mémoire répond.
Le CPU veut écrire la valeur 25 à l'adresse 3
Complète le tableau :
| Bus | Valeur transportée | Sens |
|---|---|---|
| Bus d'adresses | ? | ? → ? |
| Bus de contrôle | ? | ? → ? |
| Bus de données | ? | ? → ? |
Réponse
| Bus | Valeur transportée | Sens |
|---|---|---|
| Bus d'adresses | 3 | CPU → Mémoire |
| Bus de contrôle | ÉCRIRE | CPU → Mémoire |
| Bus de données | 25 | CPU → Mémoire |
Cette fois, les trois bus vont dans le même sens : le CPU envoie tout. C'est la différence avec la lecture.
Qu'est-ce qui changerait si on supprimait le bus de contrôle ?
On garde le bus d'adresses et le bus de données. Le CPU envoie l'adresse 3 et la valeur 25.
La mémoire doit-elle lire ou écrire ? Peut-elle le savoir ?
Réponse
Non, elle ne peut pas le savoir. Sans bus de contrôle, la mémoire voit une adresse (3) et une donnée (25) mais ne sait pas si elle doit :
- lire la valeur à l'adresse 3 et la mettre sur le bus de données, ou
- écrire 25 à l'adresse 3
Le bus de contrôle est indispensable pour lever cette ambiguïté.
2. Le cycle fetch-decode-execute en détail
Le cours dit que le processeur répète un cycle en trois temps. Voici ce que chaque temps recouvre exactement.
2.1 Fetch (recherche)
- L'UC lit l'adresse de la prochaine instruction dans le compteur ordinal (ou Program Counter)
- L'instruction est chargée depuis la mémoire vers le registre d'instruction
- Le compteur ordinal est incrémenté pour pointer vers l'instruction suivante
2.2 Decode (décodage)
L'UC analyse l'instruction pour déterminer :
- Quelle opération effectuer
- Quelles données utiliser (adresses, registres)
2.3 Execute (exécution)
L'UC commande l'UAL ou les autres composants pour réaliser l'opération.
Le cycle complet, sur une instruction LMC
Programme : additionner 3 + 5
00: LDA 10 // Charger la valeur à l'adresse 10
01: ADD 11 // Ajouter la valeur à l'adresse 11
02: STA 12 // Stocker le résultat à l'adresse 12
03: HLT // Arrêter
...
10: DAT 3 // Première valeur
11: DAT 5 // Deuxième valeur
12: DAT 0 // Résultat (initialement 0)
Déroulement du cycle pour l'instruction ADD 11 (adresse 01) :
-
Fetch :
- Le PC contient 01
- L'UC lit l'instruction à l'adresse 01 :
ADD 11(code machine :1011) - L'instruction est chargée dans le registre d'instruction
- Le PC est incrémenté : PC = 02
-
Decode :
- L'UC décode
1011: le premier chiffre,1, désigne l'instructionADD - Opérande : adresse 11
- Opération à effectuer : ajouter le contenu de l'adresse 11 à l'accumulateur
- L'UC décode
-
Execute :
- L'UC lit la valeur à l'adresse 11 : 5
- L'UAL additionne : accumulateur (3) + 5 = 8
- Le résultat 8 est stocké dans l'accumulateur
Puis le cycle recommence avec l'instruction suivante à l'adresse 02 (STA 12).
Exercices sur le cycle
Dérouler un programme, phase par phase
Reprends le programme du cours :
Adresse | Instruction Adresse | Valeur
--------|------------ --------|-------
0 | LDA 10 10 | 5
1 | ADD 11 11 | 3
2 | STA 12 12 | ?
3 | HLT
Recopie et remplis ce tableau. Cette fois, détaille les trois phases pour chaque instruction :
| Instruction | PC avant le fetch | Fetch | Decode | Execute | ACC après |
|---|---|---|---|---|---|
LDA 10 |
|||||
ADD 11 |
|||||
STA 12 |
|||||
HLT |
Une fois ton tableau rempli, saisis ce programme dans le simulateur, lance-le en mode pas à pas, et vérifie ta trace.
Avant d'ouvrir la correction
Explique en une phrase ce que tu as prédit pour l'accumulateur après ADD 11, et pourquoi. Puis compare.
Réponse
Instruction 0 : LDA 10
- PC = 0 avant le fetch
- Fetch : charge l'instruction à l'adresse 0 dans le registre d'instruction. PC passe à 1.
- Decode : identifie
LDA(code opération5), adresse source = 10 - Execute : lit la valeur à l'adresse 10 (= 5), la place dans l'accumulateur
- ACC = 5
Instruction 1 : ADD 11
- PC = 1 avant le fetch
- Fetch : charge l'instruction à l'adresse 1. PC passe à 2.
- Decode : identifie
ADD(code opération1), adresse source = 11 - Execute : lit la valeur à l'adresse 11 (= 3), l'additionne à l'accumulateur (5 + 3)
- ACC = 8
Instruction 2 : STA 12
- PC = 2 avant le fetch
- Fetch : charge l'instruction à l'adresse 2. PC passe à 3.
- Decode : identifie
STA(code opération3), adresse destination = 12 - Execute : écrit la valeur de l'accumulateur (8) à l'adresse 12
- La case mémoire 12 contient maintenant 8
Instruction 3 : HLT. Le programme s'arrête.
Les bus pendant le fetch
Toujours sur le même programme. Lors de la phase fetch de l'instruction rangée en 1 (ADD 11) :
- Que transporte le bus d'adresses ? Dans quel sens ?
- Que transporte le bus de contrôle ?
- Que transporte le bus de données ? Dans quel sens ?
Réponse
- Le bus d'adresses transporte 1 (l'adresse de l'instruction à charger, celle contenue dans le PC). Sens : CPU → Mémoire.
- Le bus de contrôle transporte LIRE (le CPU veut récupérer l'instruction). Sens : CPU → Mémoire.
- Le bus de données transporte l'instruction
ADD 11, c'est-à-dire le nombre1011. Sens : Mémoire → CPU.
Le fetch est toujours une lecture en mémoire : le CPU va chercher l'instruction.
3. Les registres
Les registres sont de petites mémoires ultra-rapides intégrées au processeur. Le cours n'en nomme que deux, le compteur ordinal et l'accumulateur. Voici la liste complète de ceux qui interviennent dans le cycle :
- Compteur ordinal (PC) : contient l'adresse de la prochaine instruction
- Registre d'instruction (RI) : contient l'instruction en cours d'exécution
- Accumulateur : contient la valeur sur laquelle l'UAL travaille, et le résultat de son calcul
- Registres généraux : stockent temporairement des données pour les calculs
Le LMC n'a qu'un seul registre de travail, l'accumulateur : c'est ce qui rend ses programmes si contraignants à écrire, et c'est voulu. Un processeur réel en possède plusieurs dizaines.
4. Le goulot de von Neumann
Hors programme.
Les instructions et les données partagent la même mémoire et les mêmes bus. Le processeur passe donc une partie de son temps à attendre la mémoire, et cette attente devient le facteur limitant : c'est le goulot de von Neumann.
Trois réponses courantes, qui ne suppriment pas le problème mais le réduisent :
- la mémoire cache, petite et très rapide, placée entre le processeur et la mémoire vive ;
- l'architecture Harvard, qui sépare la mémoire des instructions de celle des données ;
- le pipeline, qui commence le fetch de l'instruction suivante pendant qu'on exécute la courante.
5. De ton fichier à l'exécution, sur une vraie machine
Hors programme.
Sur le LMC, tu as écrit un fichier, cliqué sur « Assembler », puis sur « Charger ». Ces deux boutons ne sont pas une commodité du simulateur : ils sont la chaîne que suit tout programme, sur n'importe quelle machine.
Tu n'écris jamais des nombres. Tu écris du texte, dans un fichier, sur le disque. Ce texte n'est pas exécutable : le processeur ne lit pas des lettres.
Il se passe donc trois choses, dans cet ordre, et ce sont les mêmes pour Python, pour un jeu vidéo ou pour le système d'exploitation.
graph LR
S["Fichier source<br/>du texte, sur le disque"] -->|assemblage| O["Fichier objet<br/>des nombres, sur le disque"]
O -->|chargement| R["Mémoire vive<br/>les mêmes nombres, en RAM"]
R -->|cycle fetch-decode-execute| P["Processeur"]
- Tu écris le source. Du texte lisible par toi : des mots, des noms que tu choisis, des commentaires. Il est rangé sur le disque, comme n'importe quel fichier.
- Un programme le traduit : l'assembleur. Il remplace chaque mot par le nombre que le jeu d'instructions de la machine lui associe, et il produit un second fichier, le fichier objet, qui ne contient plus que des nombres.
- Le fichier objet est chargé en mémoire vive. Le disque est un périphérique d'entrée-sortie : le processeur ne peut pas y exécuter quoi que ce soit. Il faut d'abord recopier les nombres en RAM. C'est seulement là que le compteur ordinal peut s'y promener.
Ce que l'assembleur enlève, et c'est le point important
Le fichier objet ne contient ni mnémonique, ni nom de variable, ni commentaire. Tout cela a disparu à la traduction : c'était pour toi, pas pour la machine.
Un assembleur n'est donc pas un programme intelligent. C'est un programme qui applique une table, exactement celle que ton groupe a fabriquée à la première séance. Un compilateur, que tu rencontreras plus tard, fait un travail beaucoup plus difficile : il traduit un langage où une seule ligne peut valoir des dizaines d'instructions machine.
Exemple : ce qu'un simple print demande à un vrai processeur
Voici ce qu'il faut écrire, pour un processeur de ton ordinateur, afin d'obtenir ce que Python écrit en une seule ligne, print("truc"). Rien ici n'est au programme, et tu n'as pas à le comprendre : regarde seulement la longueur.
; truc.asm
; Assemblage : nasm -f elf64 truc.asm -o truc.o
; Édition de liens : ld truc.o -o truc
; Exécution : ./truc
section .data
msg db "truc", 10 ; le message, avec un saut de ligne (10 = '\n')
msg_len equ $ - msg ; longueur calculée automatiquement
section .text
global _start
_start:
; appel système write(1, msg, msg_len)
mov rax, 1 ; numéro de syscall pour write
mov rdi, 1 ; descripteur de fichier 1 = stdout
mov rsi, msg ; adresse du message
mov rdx, msg_len ; nombre d'octets à écrire
syscall
; appel système exit(0)
mov rax, 60 ; numéro de syscall pour exit
xor rdi, rdi ; code de retour 0
syscall
Une fois assemblé, cela fait huit instructions machine pour afficher quatre lettres. Et c'est le minimum : ce programme se contente de demander au système d'exploitation d'écrire cinq octets, là où le print de Python fait beaucoup plus de choses avant d'en arriver là.
Tu y reconnais des mots de ce cours : une adresse (msg), des registres (rax, rdi, rsi, rdx), un fichier source en texte, et un assembleur qui le traduit en nombres. Le reste, les numéros d'appel système et le rôle de chaque registre, s'apprend ailleurs.
Et ces huit instructions ne sont pas le bout du chemin. syscall ne fait qu'une chose : passer la main au système d'exploitation, qui ira écrire, lui, dans le terminal. Ce que la machine exécute ensuite est encore du code, écrit par d'autres, et il est de deux natures.
- La porte d'entrée du système, elle, est bien écrite en assembleur, à la main (dans Linux, le fichier
arch/x86/entry/entry_64.S). Elle doit sauvegarder les registres, changer de pile et basculer le processeur en mode noyau : des gestes qu'aucun langage de haut niveau ne sait exprimer. - Mais le
writelui-même, celui qui écrit vraiment tes cinq octets, est écrit en C (fs/read_write.c), comme l'immense majorité du noyau. La porte est en assembleur, la pièce derrière ne l'est pas.
Ce qui reste vrai des deux côtés : assembleur ou C, tout finit en instructions machine, parce que le processeur ne sait rien lire d'autre. Et cela se mesure : sur une machine Linux, afficher ces quatre lettres demande environ dix mille instructions exécutées dans le noyau, contre les huit que tu as écrites. Tu n'as écrit ici que celles qui frappent à la porte.
Si tu veux l'essayer : nsi install nasm, puis les trois commandes écrites en tête du fichier.
6. Jeux d'instructions et architectures
Hors programme.
- Jeu d'instructions : l'ensemble des opérations qu'un processeur sait exécuter. Les familles CISC (beaucoup d'instructions, complexes) et RISC (peu d'instructions, simples et rapides) reposent sur deux paris opposés.
- Architectures alternatives : Harvard, et les variantes dites de von Neumann modifiée.
7. Ce que la machine voit vraiment : des nombres
À faire après les défis du langage machine.
7.1 Chaque instruction est un nombre
LDA, STA, les noms de cases : tout cela est pour toi. Dans la mémoire, il n'y a que des nombres de quatre chiffres. Les deux premiers disent quoi faire, les deux derniers sur quelle case. Ainsi 5042, c'est LDA 42.
| Instruction | Nombre |
|---|---|
INP |
9001 |
OUT |
9002 |
HLT |
0000 |
LDA 42 |
5042 |
STA 42 |
3042 |
ADD 42 |
1042 |
SUB 42 |
2042 |
BRA 42 |
6042 |
BRZ 42 |
7042 |
BRP 42 |
8042 |
Traduire à la main, une fois
Chaque ligne occupe une case, à partir de la case n°0.
| Case n° | Instruction |
|---|---|
| 0 | INP |
| 1 | STA 06 |
| 2 | INP |
| 3 | ADD 06 |
| 4 | OUT |
| 5 | HLT |
| 6 | DAT |
Sur ton cahier : écris le nombre que contient chaque case.
Ensuite, dans le simulateur : assemble ce programme, ouvre le fichier .lmcobj qui apparaît et compare. Remplace 1006 par 2006, enregistre, clique Charger sans réassembler, puis Run : le programme fait maintenant une soustraction. C'est bien le fichier de nombres qui tourne.
Correction
9001 3006 9001 1006 9002 0000 0000. Il lit deux nombres et affiche leur somme.
7.2 Le saut peut tomber sur une donnée
Prédire l'impossible
| Case n° | Instruction |
|---|---|
| 0 | LDA 04 |
| 1 | OUT |
| 2 | BRA 04 |
| 3 | HLT |
| 4 | DAT 9002 |
Sur ton cahier : la liste des nombres affichés, et pourquoi le programme s'arrête alors qu'il ne passe jamais par la case n°3.
N'ouvre pas le simulateur tant que tu n'as pas écrit tes réponses.
Indice
Compare le nombre de la case n°1 et celui de la case n°4.
Correction
Il affiche −998, puis −998 encore.
- Case n°0 : l'accumulateur reçoit le mot 9002. Case n°1 : premier affichage. La machine l'écrit −998, parce qu'elle lit tout mot à partir de 5000 comme un négatif ; le mot rangé, lui, vaut bien 9002.
- Case n°2 : la machine va à la case n°4 et la lit comme une instruction. Or 9002, c'est
OUT: second affichage. - Case n°5 : jamais écrite, elle vaut
0000, c'est-à-direHLT.
Rien, dans la mémoire, ne distingue une instruction d'une donnée. C'est le compteur ordinal qui décide.
Le même mot a même été lu de trois façons ici : l'instruction OUT, le nombre −998, et les quatre chiffres 9002 que montre l'inspecteur mémoire. Une seule case, trois lectures.
7.3 Cybersécurité : quand une donnée devient du code
Un éditeur vend un logiciel qui demande une clé de licence au démarrage. Camille en a une copie, sans clé. Le programme a deux maladresses, comme en ont tous les logiciels, souvent nées d'un copier-coller.
| Case n° | Programme | Ce que ça fait |
|---|---|---|
| 00 | INP |
lire la clé tapée |
| 01 | STA cle |
la ranger dans la case cle |
| 02 | SUB licence |
clé tapée moins clé enregistrée |
| 03 | BRZ demarre |
si ça vaut 0, la clé est bonne |
| 04 | LDA zero |
|
| 05 | OUT |
afficher 0 : refusé |
| 06 | cle: DAT 0 |
la case où atterrit la clé tapée |
| 07 | OUT |
afficher la clé tapée, pour la relire |
| 08 | HLT |
|
| 09 | demarre: LDA un |
|
| 10 | OUT |
afficher 1 : le logiciel démarre |
| 11 | HLT |
|
| 12 | licence: DAT 4242 |
la clé enregistrée |
| 13 | zero: DAT 0 |
|
| 14 | un: DAT 1 |
Les deux maladresses
- La case
cleest au milieu du chemin : la machine va passer dessus et exécuter ce qu'elle contient. - La case n°07 ne fait pas ce que l'auteur croit :
OUTaffiche l'accumulateur, pas la casecle.
Attaque 1 : faire afficher la clé
Tu ne peux rien changer au programme, seulement le nombre que tu tapes. Quel nombre taper pour que le logiciel affiche 4242 ?
Indice
Ce que tu tapes atterrit en case n°06, et la machine va l'exécuter. Il faudrait que ce soit LDA licence, c'est-à-dire LDA 12.
Correction
5012, le nombre de LDA 12. Le refus s'affiche (0), puis la case n°06 met 4242 dans l'accumulateur, et la case n°07 l'affiche. Le logiciel a donné sa propre clé.
Attaque 2 : démarrer sans clé
Quel nombre taper pour que le logiciel démarre ?
Correction
6009, le nombre de BRA 09. Après le refus, la case n°06 fait sauter la machine en case n°09, derrière le contrôle. C'est un crack.
Ce qu'il faut en retenir
Une vérification faite sur la machine de celui qu'elle doit arrêter ne protège rien. D'où ce que font les éditeurs : vérifier la clé sur un serveur, ne jamais ranger un secret en clair dans un programme, marquer des zones de mémoire non exécutables, et ne jamais ranger une saisie sans la vérifier. Le même mécanisme porte des noms que tu entendras : injection de code, débordement de tampon.
Utiliser un logiciel payant sans licence est illégal. On étudie le mécanisme, pas la pratique.
Sources :
- von Neumann, J. (1945). First Draft of a Report on the EDVAC. University of Pennsylvania.
- Tanenbaum, A. S. (2013). Structured Computer Organization (6e éd.). Pearson.