Tableaux, enum, pile, bytecode — un processeur en 300 lignes
Laurent Thiry — Programmation en C
« Un processeur logiciel, c'est comme un poisson rouge :
ça tient dans un bocal (300 lignes de C) et ça fait des bulles. »
Objectif : comprendre comment une machine exécute des instructions. On va la construire nous-mêmes — en C.
Un tableau = une suite contiguë de variables du même type, accessibles par indice.
int mem[256]; // déclaration mem[0] = 42; // écriture int x = mem[0]; // lecture for (int i = 0; i < 256; i++) mem[i] = 0; // initialisation
| Indice | 0 | 1 | 2 | … | 255 |
|---|---|---|---|---|---|
| Valeur | 42 | 0 | 0 | … | 0 |
mem[256] = buffer overflow. Vous êtes prévenus.Donner des noms aux entiers — plus lisible, moins d'erreurs.
enum { PUSH, // = 0 DUP, // = 1 DROP, // = 2 ADD, // = 3 HALT // = 4 };
int prog[] = {0, 42, 21, 4};
int prog[] = {PUSH, 42, PRINT, HALT};
Structure LIFO (Last In, First Out) — comme une pile d'assiettes.
int stack[STACK_SIZE]; int sp; // stack pointer : prochain indice libre void pousser(int v) { if (sp >= STACK_SIZE) exit(1); stack[sp++] = v; } int depiler(void) { if (sp <= 0) exit(1); return stack[--sp]; }
| Opération | Pile |
|---|---|
pousser(3) | [3] |
pousser(7) | [3, 7] |
depiler() | [3] → retourne 7 |
Les opérateurs viennent après leurs opérandes.
| Infixée | Postfixée | Instructions VM |
|---|---|---|
2 + 3 | 2 3 + | PUSH 2, PUSH 3, ADD |
(1+2)×3 | 1 2 + 3 × | PUSH 1, PUSH 2, ADD, PUSH 3, MUL |
1+2×3 | 1 2 3 × + | PUSH 1, PUSH 2, PUSH 3, MUL, ADD |
ip — Instruction Pointer (compteur ordinal)sp — Stack Pointer (sommet de pile)mem[256] — données + chaînesstack[128] — pile de travailcode[] — programme (tableau séparé)while (ip < taille) { int instr = code[ip++]; // FETCH switch (instr) { // DECODE case ADD: { // EXECUTE int a = depiler(); int b = depiler(); pousser(b + a); break; } } }
La VM et un vrai CPU font la même chose. L'un tient dans 300 lignes de C, l'autre dans des milliards de transistors.
| Opcode | Effet |
|---|---|
PUSH v | Empiler v |
DUP | a → a a |
DROP | a → |
SWAP | a b → b a |
OVER | a b → a b a |
| Opcode | Effet |
|---|---|
ADD | a b → b+a |
SUB | a b → b−a |
MUL | a b → b×a |
NEG | a → −a |
INC | a → a+1 |
DEC | a → a−1 |
| Opcode | Effet |
|---|---|
EQ | a b → (b==a) |
NE | a b → (b!=a) |
GT | a b → (b>a) |
LT | a b → (b<a) |
| Opcode | Effet |
|---|---|
JMP a | Saut inconditionnel |
JZ a | Saut si sommet = 0 |
JNZ a | Saut si sommet ≠ 0 |
| Opcode | Effet |
|---|---|
LOAD a | Empiler mem[a] |
STORE a | Dépiler → mem[a] |
| Opcode | Effet |
|---|---|
PRINT | Afficher le nombre dépilé |
PRTS a | Afficher chaîne en mem[a] |
HALT | Arrêt |
Les opcodes PUSH, JMP, JZ, JNZ, LOAD, STORE, PRTS consomment le mot suivant :
0: PUSH 42 2: STORE 0 4: JMP 10
Traduisons l'algorithme du cours 1 en assembleur VM :
STORE N ; mémoriser n PUSH 1 ; res = 1 PUSH 2 ; i = 2 LOOP: DUP ; i i LOAD N ; i i n GT ; i (i>n) JNZ END ; si i>n, fin SWAP ; i res OVER ; i res i MUL ; i res*i SWAP ; res*i i INC ; i+1 JMP LOOP END: DROP ; res PRTS MSG ; " =" PRINT ; afficher res HALT
Les étiquettes LOOP et END sont des adresses (6 et 19).
enum { N = 0, MSG = 200 }; enum { LOOP = 6, END = 19 }; int programme[] = { /* 0 */ STORE, N, /* 2 */ PUSH, 1, /* 4 */ PUSH, 2, /* 6 */ DUP, /* 7 */ LOAD, N, /* 9 */ GT, /* 10 */ JNZ, END, /* 12 */ SWAP, /* 13 */ OVER, /* 14 */ MUL, /* 15 */ SWAP, /* 16 */ INC, /* 17 */ JMP, LOOP, /* 19 */ DROP, /* 20 */ PRTS, MSG, /* 22 */ PRINT, /* 23 */ HALT, };
| Variable C | Bytecode VM |
|---|---|
int n = 5; | n sur la pile → STORE N |
long long res = 1; | PUSH 1 (sur la pile) |
for (i=2; i<=n; i++) res *= i; | DUP … GT … JNZ … MUL … INC … JMP |
Si GT rend 0 (faux : 2 ≤ 5) → pas de saut → on continue. Si GT rend 1 (vrai : 6 > 5) → JNZ vers END.
; Itération avec n=5, i=2, res=1 DUP ; [1 2 2] — i en double pour le test LOAD N ; [1 2 2 5] — charger n GT ; [1 2 0] — 2 > 5 ? non (0) JNZ END ; pas de saut SWAP ; [2 1] — i sous res OVER ; [2 1 2] — copier i MUL ; [2 2] — res = 1 × 2 SWAP ; [2 2] — (inchangé ici) INC ; [2 3] — i++ JMP LOOP ; retour au début
Transforme le bytecode opaque en texte lisible :
{19, 0, 16, 1, 16, 2,
1, 18, 0, 14, 17, 19,
3, 4, 7, 3, 9, 16, 6,
2, 21, 200, 20, 22}
0: STORE 0 2: PUSH 1 4: PUSH 2 6: DUP 7: LOAD 0 9: GT 10: JNZ 19 12: SWAP 13: OVER 14: MUL 15: SWAP 16: INC 17: JMP 6 19: DROP 20: PRTS 200 22: PRINT 23: HALT
Exécution ralentie, pile visible à chaque étape :
0: STORE 0 pile[5] 2: PUSH 1 pile[1] 4: PUSH 2 pile[1 2] 6: DUP pile[1 2 2] 7: LOAD 0 pile[1 2 2 5] 9: GT pile[1 2 0] 10: JNZ 19 pile[1 2] 12: SWAP pile[2 1] …
L'option --pas-a-pas active le mode interactif
(appuyez sur Entrée pour avancer).
./vm --pas-a-pas et suivez la pile pour n=3. Vérifiez que 3! = 6.La VM teste n! pour n = 0 à 12 :
=== Factorielle 0 à 12 === 0 = 1 OK 6 = 720 OK 1 = 1 OK 7 = 5040 OK 2 = 2 OK 8 = 40320 OK 3 = 6 OK 9 = 362880 OK 4 = 24 OK 10 = 3628800 OK 5 = 120 OK 11 = 39916800 OK 12 = 479001600 OK 13 passes, 0 echoues
13! dépasse la capacité d'un int 32 bits → les tests s'arrêtent à 12. On pourrait passer en long long…
make vm → compiler, make vm-run → tester, ./vm --pas-a-pas → debugger.Notre VM s'inspire du langage Forth (Chuck Moore, 1970) :
| Forth | Notre VM |
|---|---|
| Mots définis par l'utilisateur | 23 opcodes prédéfinis |
| Pile de données + pile de retour | 1 pile de données |
| Interpréteur interactif | Programme pré-compilé |
| Compilateur « threadé » | On compile à la main |
%../vm --pas-a-pas, faites n=3, et vérifiez la pile à chaque étape. Qui voit l'overflow ?| Concept | À retenir |
|---|---|
| Tableaux | Séquence contiguë, accès par indice, risque de débordement |
| enum | Noms pour les entiers, plus lisible, auto-numérotation |
| Pile | LIFO, push/pop, sp = indice du sommet |
| RPN (postfixée) | Opérateurs après les opérandes, pas de parenthèses |
| VM | Processeur logiciel : ip, sp, mem[], stack[], code[] |
| Cycle | FETCH → DECODE → EXECUTE |
| Bytecode | Programme = tableau d'entiers |
| Forth | Langage à pile qui a inspiré la VM |
while qui lit
des entiers et fait des trucs. Le matériel, c'est pareil mais
avec des transistors. »
2 registres · 256 mots mémoire · 23 opcodes · 300 lignes de C
make vm && make vm-run