Chapitre 1 · 4 h
Modèle de von Neumann
Unité de traitement, unité de commande, mémoire et entrées/sorties ; bus de données, d'adresses et de contrôle ; cycle fetch-decode-execute, horloge et fréquence.
Reprogrammer l'ENIAC, en 1945, prenait plusieurs jours. Non pas d'écriture — de câblage : une équipe d'opératrices rebranchait des centaines de fiches et repositionnait des milliers d'interrupteurs. La machine n'exécutait pas un programme, elle était le programme.
La même année, un rapport resté célèbre pour son statut de brouillon — le First Draft of a Report on the EDVAC, signé du seul John von Neumann alors que l'idée est collective — propose autre chose : ranger les instructions dans la mémoire, à côté des données. Changer de programme devient alors ce que c'est aujourd'hui, écrire des octets quelque part.
Ce chapitre pose cette architecture. Il est court et il est central : les chapitres 6, 7 et 8 ne feront qu'en détailler des morceaux — le jeu d'instructions, la mémoire, les entrées/sorties.
Le programme est une donnée
L'idée tient en une phrase, et ses conséquences occupent l'informatique entière.
Puisque le programme est en mémoire, un programme peut lire, écrire et fabriquer un autre programme. Un compilateur devient possible : c'est un programme dont la sortie est un programme. Un système d'exploitation devient possible : il charge en mémoire le code d'une application, puis lui donne la main. Un virus devient possible, pour la même raison.
Le revers est immédiat. Rien, dans la mémoire, ne distingue physiquement une instruction d'une donnée : ce sont des motifs de bits, dans les mêmes cases, sur le même bus. C'est le processeur qui décide, par le seul fait de savoir où il en est. Faire pointer le compteur ordinal sur des données, et la machine les exécutera consciencieusement — c'est le mécanisme d'une bonne part des attaques par débordement de tampon, et la raison pour laquelle les systèmes modernes marquent certaines zones « non exécutables ».
Les quatre organes
Le modèle décompose la machine en quatre parties, et le vocabulaire est stable depuis 1945.
L'unité arithmétique et logique (UAL) calcule : additions, soustractions, opérations booléennes, comparaisons. C'est l'additionneur du chapitre 4, entouré de quelques portes et d'un multiplexeur qui sélectionne l'opération.
L'unité de commande ne calcule rien. Elle lit l'instruction courante, la décode, et active les bons signaux au bon moment : lire la mémoire, écrire dans un registre, régler l'UAL sur « additionner ». C'est le chef d'orchestre, et son travail est intégralement combinatoire et séquentiel — décodeurs, multiplexeurs, bascules.
Ensemble, UAL et unité de commande forment le processeur. On y ajoute quelques registres, mémoires internes minuscules et très rapides :
| Registre | Rôle |
|---|---|
| Compteur ordinal (CO) | adresse de la prochaine instruction |
| Registre d'instruction (RI) | l'instruction en cours de traitement |
| Accumulateur / registres généraux | les opérandes et les résultats |
| Registre d'état | les indicateurs du chapitre 2 : retenue, débordement, zéro, signe |
La mémoire est un tableau de cases numérotées. Donner une adresse, recevoir un mot ; donner une adresse et un mot, l'écrire. Elle ne sait rien d'autre, et surtout pas ce qu'elle contient.
Les entrées/sorties relient le tout au monde. Le chapitre 8 leur est consacré.
Trois bus
Ces organes communiquent par des faisceaux de fils appelés bus, et il y en a exactement trois, chacun avec sa nature propre.
Le bus d'adresses est unidirectionnel : seul le processeur émet, la mémoire écoute. Sa largeur détermine l'espace adressable, et c'est le calcul le plus utile du chapitre : fils donnent adresses. Avec 16 fils, 64 Kio. Avec 32 fils, octets, soit exactement 4 Gio — la fameuse limite des systèmes 32 bits, qui n'est pas une décision commerciale mais un nombre de fils.
Le bus de données est bidirectionnel : un mot y circule dans un sens à la lecture, dans l'autre à l'écriture. Sa largeur — 8, 32, 64 bits — est la quantité transférée par accès.
Le bus de contrôle transporte les signaux qui disent quoi faire et quand : lecture ou écriture, horloge, demandes d'interruption du chapitre 8.
Quiz · 1 question
Un processeur dispose d'un bus d'adresses de 20 fils et d'un bus de données de 16 bits. Quelle est la quantité maximale de mémoire adressable, et pourquoi ?
- 2²⁰ × 2 octets = 2 Mio, car chaque adresse désigne un mot de 16 bits — produit des deux bus
- 2²⁰ = 1 Mio d'octets si la mémoire est adressée par octet : c'est le bus d'ADRESSES qui fixe le nombre de cases, pas le bus de données — le bus d'adresses seul
- 2¹⁶ = 64 Kio, car le bus de données limite la taille des valeurs manipulées — le bus de données
Réponse : Les deux bus répondent à deux questions différentes. Le bus d'adresses fixe COMBIEN DE CASES on peut désigner : 20 fils, donc 2²⁰ adresses distinctes, soit 1 Mio si l'unité adressable est l'octet — ce qui est le cas des machines courantes. Le bus de données fixe COMBIEN DE BITS transitent par accès : ici 16, donc deux octets par transfert, ce qui joue sur la vitesse, pas sur la capacité. La première réponse serait juste sur une machine adressée par MOT et non par octet, architecture qui existe mais qui n'est pas celle du cours. Le même raisonnement donne les 4 Gio des systèmes 32 bits.
Le cycle recherche - décodage - exécution
Le processeur ne fait qu'une chose, indéfiniment, à chaque coup d'horloge : le même cycle en trois temps.
Recherche (fetch). Le contenu du compteur ordinal part sur le bus d'adresses ; la mémoire renvoie le mot ; ce mot entre dans le registre d'instruction. Le compteur ordinal est incrémenté immédiatement, avant même qu'on sache ce que l'instruction fait. Ce détail compte : c'est ce qui permet à un branchement, plus tard dans le cycle, d'écraser proprement cette valeur.
Décodage (decode). L'unité de commande découpe le registre d'instruction : d'un côté le code opération, de l'autre les opérandes. Le chapitre 6 détaillera ce découpage. À l'issue du décodage, les signaux de commande sont positionnés ; rien n'a encore bougé dans les données.
Exécution (execute). Le travail a lieu : l'UAL calcule, ou la mémoire est lue ou écrite, ou le compteur ordinal reçoit une nouvelle valeur s'il s'agit d'un branchement.
Puis on recommence. Une machine allumée ne fait rien d'autre — et lorsqu'elle « ne fait rien », elle exécute une boucle d'attente du système d'exploitation.
Animation · 11 étapes
Le cycle recherche-décodage-exécution, instruction par instruction
- Au départ — Le compteur ordinal (CO) contient l'adresse de la prochaine instruction, ici 0. Le registre d'instruction (RI) est vide : rien n'a encore été lu. Programme et données sont dans la MÊME mémoire — c'est tout le principe de von Neumann.
- Recherche — l'instruction 0 est lue — Le CO est placé sur le bus d'adresses, la mémoire renvoie le mot, qui est copié dans le RI. Le CO est incrémenté AUSSITÔT, avant même de savoir ce que l'instruction fait : c'est ce qui permettra à un branchement de l'écraser proprement.
- Décodage — l'unité de commande lit le code opération — Le RI est découpé : le code opération dit CHARGE, le champ adresse dit 10. L'unité de commande active les signaux correspondants — lecture mémoire, écriture dans l'accumulateur. Rien n'a encore bougé dans les données.
- Exécution — l'accumulateur reçoit 7 — Second accès mémoire, cette fois à l'adresse 10, dont le contenu part vers l'accumulateur. Deux accès mémoire pour une seule instruction : c'est déjà le goulot d'étranglement de von Neumann.
- Recherche — l'instruction 1 est lue — Même geste, à l'adresse que le CO indique désormais. Le cycle est strictement identique quelle que soit l'instruction : c'est ce qui rend l'unité de commande simple.
- Décodage — c'est une addition — Le code opération désigne l'unité arithmétique et logique, et la commande la règle sur « additionner ». Le champ adresse 11 désigne le second opérande ; le premier est implicitement l'accumulateur.
- Exécution — 7 + 3 = 10 — L'UAL reçoit 7 depuis l'accumulateur et 3 depuis la mémoire, et son résultat repart dans l'accumulateur. C'est l'additionneur du chapitre 4, à la porte près.
- Recherche — l'instruction 2 est lue — Troisième tour de boucle. Le CO passe à 3.
- Décodage — c'est une écriture mémoire — Le sens du transfert s'inverse : cette fois l'unité de commande demande une ÉCRITURE, ce qui se traduit par un signal différent sur le bus de contrôle. L'adresse et la donnée circulent sur leurs bus respectifs.
- Exécution — le résultat est rangé — La case 12 contient 10. Remarquez que rien ne distingue physiquement cette case des cases 0 à 3 qui contiennent le programme : la mémoire ne sait pas ce qu'elle stocke.
- Arrêt — Sans cette instruction, le processeur continuerait à lire la case 4, puis 5, et exécuterait les données comme si c'étaient des instructions. C'est exactement ce que provoque un débordement de pile mal maîtrisé.
Horloge, fréquence, et le vrai goulot d'étranglement
L'horloge cadence tout. À chaque front, l'état bascule vers sa valeur suivante — c'est la bascule sur front du chapitre 4. La fréquence est le nombre de fronts par seconde : 3 GHz signifie trois milliards de cycles par seconde, soit une période de 0,33 nanoseconde.
Il faut résister à une tentation, que le chapitre 8 démontera pour de bon : la fréquence ne mesure pas la performance. Le temps d'exécution vaut
où le CPI est le nombre moyen de cycles par instruction. Deux processeurs de même fréquence peuvent différer d'un facteur trois selon leur CPI et selon la qualité du code compilé.
Reste le défaut structurel du modèle, connu sous le nom de goulot d'étranglement de von Neumann : programme et données partageant la même mémoire et le même bus, le processeur passe son temps à attendre la mémoire. L'animation ci-dessus le montre à nu — deux accès mémoire pour la seule instruction CHARGE 10, l'un pour lire l'instruction, l'autre pour lire la donnée.
L'écart s'est creusé pendant quarante ans : les processeurs ont accéléré bien plus vite que la mémoire. Deux réponses existent. L'architecture Harvard sépare physiquement mémoire d'instructions et mémoire de données, ce que font les microcontrôleurs et, à l'intérieur, les caches de premier niveau. Et le cache lui-même, qui est le sujet du chapitre 7 — sa raison d'être tient tout entière dans le paragraphe que vous venez de lire.
Quiz · 1 question
Pourquoi le compteur ordinal est-il incrémenté pendant la phase de recherche, avant même que l'instruction soit décodée ?
- Par optimisation : cela évite une addition supplémentaire en fin de cycle — gain d'une addition
- Parce que le cas courant est de passer à l'instruction suivante ; un branchement, décodé après, n'a plus qu'à écraser cette valeur par la sienne — cas courant puis écrasement
- Parce que le compteur ordinal doit rester synchrone avec le registre d'instruction, sans quoi le décodage lirait la mauvaise instruction — synchronisation
Réponse : La très grande majorité des instructions ne modifient pas le flot : le successeur naturel est l'instruction suivante. On l'anticipe donc systématiquement, dès la recherche, ce qui simplifie le circuit — un incrémenteur branché en permanence sur le CO, exactement le compteur du chapitre 4. Les branchements, minoritaires, écrasent ensuite cette valeur pendant l'exécution, et rien n'est perdu puisque le CO n'a pas encore servi. Ce pari sur le cas courant est le même qui fondera la prédiction de branchement du pipeline au chapitre 8 — et c'est aussi pourquoi un branchement y coûte plus cher qu'une instruction ordinaire.
À vous
L'exercice écrit la boucle du processeur : un tableau d'entiers qui contient à la fois le programme et les données, un compteur ordinal, un accumulateur, et une fonction qui répète recherche, décodage, exécution jusqu'à rencontrer l'arrêt.
Le programme fourni est déjà rangé en mémoire sous forme de nombres — pas de texte, pas de symboles. C'est le point du chapitre : à ce niveau, une instruction est un entier, et le dernier test le prouve en faisant modifier au programme sa propre instruction.
Exercice de code
Complétez le cycle recherche-décodage-exécution d'une machine à accumulateur.
Point de départ
// Une instruction tient dans un entier : opcode * 100 + adresse.
const STOP = 0, CHARGE = 1, AJOUTE = 2, RANGE = 3, SAUTE_SI_ZERO = 4, SOUSTRAIT = 5;
// Mémoire unique. Cases 0..7 : le programme. Cases 10.. : les données.
const MEM = [
CHARGE * 100 + 10, // 0 : ACC <- Mem[10]
AJOUTE * 100 + 11, // 1 : ACC <- ACC + Mem[11]
RANGE * 100 + 12, // 2 : Mem[12] <- ACC
STOP * 100 + 0, // 3 : arrêt
0, 0, 0, 0, 0, 0,
7, // 10 : donnée
3, // 11 : donnée
0, // 12 : résultat
];
function executer(mem, tracer) {
let CO = 0, ACC = 0, cycles = 0;
while (cycles++ < 100) {
// RECHERCHE : lire l'instruction pointée par le CO, puis incrémenter.
const RI = mem[CO];
CO = CO + 1;
// DÉCODAGE : séparer le code opération de l'adresse.
const op = Math.floor(RI / 100);
const adr = RI % 100;
if (tracer) console.log(" CO=" + String(CO - 1).padStart(2) +
" RI=" + String(RI).padStart(4) + " op=" + op + " adr=" + adr +
" ACC=" + ACC);
// EXÉCUTION.
if (op === STOP) return { ACC, mem, cycles };
if (op === CHARGE) ACC = mem[adr];
if (op === AJOUTE) ACC = ACC + mem[adr];
// ← il manque RANGE, SOUSTRAIT et SAUTE_SI_ZERO
}
return { ACC, mem, cycles: -1 };
}
// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Complétez l'exécution : RANGE écrit l'accumulateur en mémoire,
// SOUSTRAIT retranche, SAUTE_SI_ZERO écrit adr dans CO si ACC vaut 0.
// 2. Écrivez en mémoire un programme qui calcule 7 - 3 et le range en 12.
const r = executer([...MEM], true);
console.log("ACC =", r.ACC, "| Mem[12] =", r.mem[12], "| attendu 10 et 10");
Solution
const STOP = 0, CHARGE = 1, AJOUTE = 2, RANGE = 3, SAUTE_SI_ZERO = 4, SOUSTRAIT = 5;
const MEM = [
CHARGE * 100 + 10,
AJOUTE * 100 + 11,
RANGE * 100 + 12,
STOP * 100 + 0,
0, 0, 0, 0, 0, 0,
7, 3, 0,
];
function executer(mem, tracer) {
let CO = 0, ACC = 0, cycles = 0;
while (cycles++ < 100) {
const RI = mem[CO];
CO = CO + 1;
const op = Math.floor(RI / 100);
const adr = RI % 100;
if (tracer) console.log(" CO=" + String(CO - 1).padStart(2) +
" RI=" + String(RI).padStart(4) + " op=" + op + " adr=" + adr +
" ACC=" + ACC);
if (op === STOP) return { ACC, mem, cycles };
if (op === CHARGE) ACC = mem[adr];
if (op === AJOUTE) ACC = ACC + mem[adr];
if (op === SOUSTRAIT) ACC = ACC - mem[adr];
if (op === RANGE) mem[adr] = ACC;
// Un branchement n'est rien d'autre qu'une écriture dans le CO.
if (op === SAUTE_SI_ZERO && ACC === 0) CO = adr;
}
return { ACC, mem, cycles: -1 };
}
console.log("— addition —");
const r1 = executer([...MEM], true);
console.log("ACC =", r1.ACC, "| Mem[12] =", r1.mem[12]);
console.log("— soustraction 7 - 3 —");
const SOUSTRACTION = [...MEM];
SOUSTRACTION[1] = SOUSTRAIT * 100 + 11;
const r2 = executer(SOUSTRACTION, false);
console.log("Mem[12] =", r2.mem[12], "| attendu 4");
console.log("— le programme se modifie lui-même —");
// Rien ne distingue une case « programme » d'une case « donnée » : ranger
// une valeur à l'adresse 1 réécrit l'instruction 1. C'est tout von Neumann.
const AUTO = [
CHARGE * 100 + 10, // 0 : ACC <- 205 (= SOUSTRAIT 11, vu comme donnée)
RANGE * 100 + 2, // 1 : écrit cette valeur À LA PLACE de l'instruction 2
AJOUTE * 100 + 11, // 2 : sera écrasée avant d'être exécutée
RANGE * 100 + 12,
STOP * 100 + 0,
0, 0, 0, 0, 0,
SOUSTRAIT * 100 + 11, // 10 : une « donnée » qui est en fait une instruction
3, 0,
];
const r3 = executer(AUTO, false);
console.log("Mem[2] est devenue", r3.mem[2], "— l'instruction a été réécrite");
En travaux pratiques
Travaux pratiques 5 · 3 h
Exécuter un programme à la main
Suivre le cycle chercher–décoder–exécuter cycle par cycle, registre par registre, jusqu'à ce que le mécanisme devienne évident — puis en mesurer le goulot.
Avant de commencer
- Le TP 4 : registres, compteur, UAL
- Un simulateur de machine de von Neumann, ou une feuille et un tableau de trace
Énoncé
- Charger — Placez en mémoire un programme de quatre instructions qui calcule la somme de deux valeurs et la range. Notez le contenu de chaque adresse, instructions et données mélangées.
- La trace complète — Exécutez pas à pas et remplissez un tableau : à chaque cycle, la valeur du compteur ordinal, du registre d'instruction, de l'accumulateur, et l'adresse mémoire accédée. Ne sautez aucune ligne. Indice : Le compteur ordinal s'incrémente pendant la phase de recherche, pas à la fin — c'est ce qui rend les sauts possibles.
- Compter les accès — Sur votre trace, comptez combien de cycles accèdent à la mémoire et pour quoi faire : instruction ou donnée. Calculez la proportion.
- Le programme est une donnée — Écrivez un programme qui MODIFIE une de ses propres instructions avant de l'exécuter. Faites-le tourner et expliquez pourquoi c'est possible.
- Ce que cela coûte — Écrivez maintenant un programme qui, par une erreur d'adresse, écrit une donnée par-dessus une instruction. Observez le comportement et rapprochez-le d'une classe de failles connue.
- L'autre architecture — Dessinez le même schéma avec des mémoires séparées pour les instructions et les données. Dites ce que l'étape 4 devient, et ce que le débit devient.
- Un saut — Ajoutez un branchement conditionnel et tracez une boucle de trois tours. Identifiez le cycle exact où le compteur ordinal cesse de s'incrémenter normalement.
C'est réussi quand
- Votre trace ne comporte aucun trou : chaque cycle a ses quatre valeurs
- Vous savez dire combien d'accès mémoire coûte UNE instruction de votre jeu
- Vous savez expliquer en une phrase pourquoi une donnée peut devenir du code
Correction
adresse | contenu | rôle 0 | LOAD 10 | instruction 1 | ADD 11 | instruction 2 | STORE 12 | instruction 3 | HALT | instruction 10 | 7 | donnée 11 | 5 | donnée 12 | (résultat) | donnée
Rien dans la mémoire ne distingue l'adresse 0 de l'adresse 10. Le contenu de 10 est une instruction si le compteur ordinal y pointe, une donnée si une instruction la lit. C'est le principe du programme enregistré — l'idée qui distingue un ordinateur d'une machine à calculer.
cycle | phase | CO | RI | ACC | accès mémoire 1 | chercher | 0 | LOAD 10 | ? | lecture @0 (instruction) 2 | exécuter | 1 | LOAD 10 | 7 | lecture @10 (donnée) 3 | chercher | 1 | ADD 11 | 7 | lecture @1 (instruction) 4 | exécuter | 2 | ADD 11 | 12 | lecture @11 (donnée) 5 | chercher | 2 | STORE 12 | 12 | lecture @2 (instruction) 6 | exécuter | 3 | STORE 12 | 12 | écriture @12 (donnée) 7 | chercher | 3 | HALT | 12 | lecture @3 (instruction)
Le compteur ordinal est incrémenté en phase de recherche, AVANT l'exécution : quand une instruction de saut s'exécute, elle écrase une valeur déjà avancée. C'est ce détail de chronologie qui explique que les adresses relatives se comptent depuis l'instruction suivante, et non depuis le saut lui-même.
7 cycles, 7 accès mémoire : 4 pour lire les instructions (57 %) 3 pour les données (43 %) chaque instruction coûte AU MOINS un accès mémoire, avant même de faire quoi que ce soit d'utile
Plus de la moitié du trafic mémoire ne sert qu'à savoir quoi faire. C'est le goulot de von Neumann, et il n'a pas disparu : les processeurs modernes le contournent avec un cache d'instructions séparé, une lecture anticipée et un pipeline. Le chapitre 7 mesurera ce que coûte un accès qui rate le cache.
0 | LOAD 20 ; charge la valeur « ADD 11 » 1 | STORE 3 ; l'écrit à l'adresse 3 2 | LOAD 10 3 | (vide) ; devient « ADD 11 » avant d'être atteint 4 | HALT /* possible parce que STORE ne sait pas qu'il écrit du code */
Cette souplesse a été essentielle aux machines sans registre d'index. Son revers est direct : un débordement de tampon qui écrit au-delà d'une zone de données peut écraser du code ou une adresse de retour, et faire exécuter ce que l'attaquant a écrit. Toutes les protections modernes — pages non exécutables, W^X, signature de code — sont des restrictions ajoutées PAR-DESSUS ce principe, jamais une remise en cause.
von Neumann : une mémoire, un bus + simple, souple, code auto-modifiant possible - un seul accès à la fois : le goulot Harvard : deux mémoires, deux bus + instruction et donnée lues dans le MÊME cycle - impossible de modifier son propre code - impossible de charger un programme comme une donnée… en pratique : Harvard au niveau des CACHES, von Neumann au niveau de la mémoire centrale
Les processeurs actuels ne choisissent pas : ils sont Harvard là où le débit compte (caches L1 séparés pour instructions et données) et von Neumann là où la souplesse compte (mémoire principale unifiée, ce qui permet à un système d'exploitation de charger un programme). Le compromis n'a pas été tranché, il a été déplacé.
Ce que la suite en fait
Le chapitre 6 ouvre le registre d'instruction et regarde comment un entier de 32 bits se
découpe en code opération et opérandes ; il montrera aussi comment un pour de haut niveau
devient une suite de branchements écrivant dans le compteur ordinal.
Le chapitre 7 s'attaque au goulot d'étranglement, et le chapitre 8 aux entrées/sorties et à la performance — dont la loi d'Amdahl, qui dira combien il est vain d'accélérer une partie de la machine sans regarder le reste.
À retenir
Flashcards · 4 cartes
- Quelle est l'idée centrale du modèle de von Neumann, et sa conséquence la plus lourde ?
- Ranger le programme DANS la mémoire, à côté des données. Conséquence : un programme peut en lire, écrire et produire un autre — d'où les compilateurs, les systèmes d'exploitation, et aussi les virus. Revers : rien ne distingue physiquement une instruction d'une donnée, seul le compteur ordinal en décide, ce qui rend possibles les attaques par exécution de données.
- Quels sont les trois bus, et lequel fixe la quantité de mémoire adressable ?
- Le bus d'adresses (unidirectionnel, du processeur vers la mémoire), le bus de données (bidirectionnel) et le bus de contrôle (lecture/écriture, horloge, interruptions). C'est le bus d'ADRESSES qui fixe la capacité : n fils donnent 2ⁿ cases — 32 fils donnent 4 Gio. Le bus de données ne fixe que la quantité transférée par accès.
- Décrivez les trois phases du cycle d'exécution, et ce qui arrive au compteur ordinal.
- Recherche : le CO adresse la mémoire, le mot lu entre dans le registre d'instruction, et le CO est incrémenté AUSSITÔT. Décodage : l'unité de commande découpe le RI en code opération et opérandes, et positionne les signaux. Exécution : l'UAL calcule, ou la mémoire est lue/écrite, ou le CO reçoit une nouvelle valeur s'il s'agit d'un branchement.
- Qu'est-ce que le goulot d'étranglement de von Neumann, et quelles réponses lui apporte-t-on ?
- Programme et données partagent la même mémoire et le même bus : le processeur passe son temps à attendre la mémoire — deux accès pour une seule instruction chargeant une donnée. Les processeurs ayant accéléré bien plus vite que la mémoire, l'écart s'est creusé. Réponses : l'architecture Harvard, qui sépare les deux mémoires, et surtout le cache (chapitre 7).