Cours 2

Du tableau à la

Machine Virtuelle

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. »

1

🗺️ Plan du cours

Au cours 1 on codait la factorielle. Au cours 2, on construit la machine qui la calcule. C'est méta.
2

1. Du problème au processeur logiciel

📘 Cours 1
factorielle.c
💡 Tableaux & enum
nouvelles briques
🧱 Pile & opcodes
architecture VM
⚙️ Factorielle en bytecode
programme exécutable

Objectif : comprendre comment une machine exécute des instructions. On va la construire nous-mêmes — en C.

Passer de conducteur à mécano. Sauf qu'ici, on ne se salit pas les mains (sauf segmentation fault).
3

2.1 Les tableaux

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
Indice012255
Valeur42000
🐛 Attention : C ne vérifie pas les limites. mem[256] = buffer overflow. Vous êtes prévenus.
4

2.2 Les énumérations

Donner des noms aux entiers — plus lisible, moins d'erreurs.

enum {
    PUSH,   // = 0
    DUP,    // = 1
    DROP,   // = 2
    ADD,    // = 3
    HALT    // = 4
};

Sans enum

int prog[] = {0, 42, 21, 4};

Avec enum

int prog[] = {PUSH, 42, PRINT, HALT};
12 (le nombre) ? Non, c'est MUL. Sauf si vous avez changé l'ordre. D'où l'intérêt des enum.
5

3. La pile (stack)

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érationPile
pousser(3)[3]
pousser(7)[3, 7]
depiler()[3] → retourne 7
6

4. Notation postfixée (RPN)

Les opérateurs viennent après leurs opérandes.

InfixéePostfixéeInstructions VM
2 + 32 3 +PUSH 2, PUSH 3, ADD
(1+2)×31 2 + 3 ×PUSH 1, PUSH 2, ADD, PUSH 3, MUL
1+2×31 2 3 × +PUSH 1, PUSH 2, PUSH 3, MUL, ADD
Avantage : Pas de parenthèses, pas de priorité. Simple pour la machine.
Les calculatrices HP des années 70 utilisaient la RPN. Les ingénieurs adoraient. Les comptables pleuraient. Les deux avaient raison.
7

5. Architecture de la VM

Registres

  • ip — Instruction Pointer (compteur ordinal)
  • sp — Stack Pointer (sommet de pile)

Mémoire

  • mem[256] — données + chaînes
  • stack[128] — pile de travail
  • code[] — programme (tableau séparé)

Cycle FETCH → DECODE → EXECUTE

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.

8

6. Les 23 opcodes (1/2)

📦 Poussée / Duplication

OpcodeEffet
PUSH vEmpiler v
DUPa → a a
DROPa →
SWAPa b → b a
OVERa b → a b a

🔢 Arithmétique

OpcodeEffet
ADDa b → b+a
SUBa b → b−a
MULa b → b×a
NEGa → −a
INCa → a+1
DECa → a−1

⚖️ Comparaisons

OpcodeEffet
EQa b → (b==a)
NEa b → (b!=a)
GTa b → (b>a)
LTa b → (b<a)

🎯 Sauts

OpcodeEffet
JMP aSaut inconditionnel
JZ aSaut si sommet = 0
JNZ aSaut si sommet ≠ 0
9

6. Les 23 opcodes (2/2)

💾 Mémoire

OpcodeEffet
LOAD aEmpiler mem[a]
STORE aDépiler → mem[a]

📤 E/S

OpcodeEffet
PRINTAfficher le nombre dépilé
PRTS aAfficher chaîne en mem[a]
HALTArrêt

Opcode avec argument

Les opcodes PUSH, JMP, JZ, JNZ, LOAD, STORE, PRTS consomment le mot suivant :

   0:  PUSH  42
   2:  STORE 0
   4:  JMP   10
23 opcodes. Moins que votre grille-pain. Mais avec ça, on calcule n'importe quoi. En théorie.
10

7.1 Factorielle — algorithme VM

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).

11

7.2 Factorielle — bytecode

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 CBytecode 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
12

7.3 Visualisons la boucle

Pile:
[1 2]
DUP
[1 2 2]
LOAD N
[1 2 2 5]
GT
[1 2 0]

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
13

8.1 Désassembleur

Transforme le bytecode opaque en texte lisible :

Bytecode (entiers)

{19, 0, 16, 1, 16, 2,
 1, 18, 0, 14, 17, 19,
 3, 4, 7, 3, 9, 16, 6,
 2, 21, 200, 20, 22}

Assembleur (lisible)

   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
23 entiers qui veulent dire quelque chose. Sans le désassembleur, on devinerait au feeling. Avec, on lit le programme comme le journal.
14

8.2 Mode pas-à-pas

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).

✏️ Exercice : Lancez ./vm --pas-a-pas et suivez la pile pour n=3. Vérifiez que 3! = 6.
15

Tests et résultats

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

💡 Commandes : make vm → compiler, make vm-run → tester, ./vm --pas-a-pas → debugger.
16

9.1 L'héritage Forth

Notre VM s'inspire du langage Forth (Chuck Moore, 1970) :

ForthNotre VM
Mots définis par l'utilisateur23 opcodes prédéfinis
Pile de données + pile de retour1 pile de données
Interpréteur interactifProgramme pré-compilé
Compilateur « threadé »On compile à la main
Forth (1970)
Chuck Moore
Notre VM (2026)
300 lignes de C
Cours 3
Parseur + compilateur
Forth, c'est le punk du langage : minimaliste, rapide, incompréhensible pour les non-initiés. Notre VM, c'est le punk adouci.
17

9.2 Exercices

★☆☆ Fibonacci : Programme VM qui calcule F(n). Indice : deux variables en mémoire (a, b).
★☆☆ PGCD : Programme VM (Euclide). Deux entrées sur la pile.
★★☆ MOD : Ajouter l'opcode MOD → 4 étapes : enum + nom + case + %.
★★☆ AND/OR/XOR : Opcodes bit-à-bit.
★★★ Pile de retour : CALL/RET avec une deuxième pile.
★★★ Compilateur : Lire "(1+2)*3" en entrée → produire du bytecode. (C'est le cours 3 !)
✏️ Défi express : Lancez ./vm --pas-a-pas, faites n=3, et vérifiez la pile à chaque étape. Qui voit l'overflow ?
18

Résumé du cours

ConceptÀ retenir
TableauxSéquence contiguë, accès par indice, risque de débordement
enumNoms pour les entiers, plus lisible, auto-numérotation
PileLIFO, push/pop, sp = indice du sommet
RPN (postfixée)Opérateurs après les opérandes, pas de parenthèses
VMProcesseur logiciel : ip, sp, mem[], stack[], code[]
CycleFETCH → DECODE → EXECUTE
BytecodeProgramme = tableau d'entiers
ForthLangage à pile qui a inspiré la VM
« Un processeur, c'est juste une boucle while qui lit des entiers et fait des trucs. Le matériel, c'est pareil mais avec des transistors. »
19

De la variable au processeur

2 registres · 256 mots mémoire · 23 opcodes · 300 lignes de C

📄 Fichier clé
vm.c (326 lignes)
📚 Prochain cours
Cours 3 — S-expressions & parseur
🛠️ Commandes
make vm && make vm-run
Le prochain cours ? On écrit un compilateur. Parce qu'encoder le bytecode à la main, c'est pour les mages.
« On n'arrête pas le progrès. Surtout quand il tient dans un Makefile. »
20