Architecture des ordinateurs

Réalisation électronique d’une machine programmable manipulant des représentations numériques

Jérémy Fix

CentraleSupélec

2026-08-11

La v0 est conçue, programmons la !

Chemin de données et programmation

Conception

  • Chemin de données
  • Jeu d’instructions
  • Séquenceur

Lego Builder

Lego Builder

Programmation

  • Code machine (instructions/données)
  • Calcul des adresses “à la main”
    • adresses des branchements?
    • adresses des données ?

Lego Programmer

Lego Programmer

C’est dur et long pour le moment de programmer

Calculer la suite de Syracuse

\[\forall n \in \mathbb{N}^*, u_{n+1} = \begin{cases} u_n/2 & \mbox{si } u_n \mbox{ est pair}\\ 3u_{n+1}+1 & \mbox{ sinon} \end{cases}\] \[u_0 = 127 ; u_{n} = ?\]

Code machine

1000 007f 1c00 0024 1c00 1000 1400 0024 2000 0001 5000 7400 001b 1400 0024 2000 0003 4000 2000 0001 3000 1c00 0024 1c00 1000 7000 0006 1400 0024 4800 1c00 0024 1c00 1000 7000 0006

si si, je vous assure. Donc, c’est dur et long.

C’est dur et long pour le moment de programmer

Calculer la suite de Syracuse

\[\forall n \in \mathbb{N}^*, u_{n+1} = \begin{cases} u_n/2 & \mbox{si } u_n \mbox{ est pair}\\ 3u_{n+1}+1 & \mbox{ sinon} \end{cases}\] \[u_0 = 127 ; u_{n} = ?\]

Code machine

[LDAi 007f STA 0024 STA 1000] [LDAd 0024 LDBi 0001 ANDA JZA 001b] [LDAd 0024 LDBi 0003 MULA LDBi 0001 ADDA STA 0024 STA 1000 JMP 0006] [LDAd 0024 DIVA STA 0024 STA 1000 JMP 0006]

Comment faire pour simplifier la programmation ?

Ne plus écrire en code machine1

Code machine (arg..) Assemblage (cool!) Python (super cool!)
1000 0001
2000 0002
3000
← ??? LDAi 1
LDBi 2
ADDA
← ??? a=1
b=2
a=a+b
  • Langage d’assemblage ? LDAi, STA, ADDA, ..
  • Langages de haut-niveau (architecture indépendant) : C++, Python, .. ?

et bien sûr comment faire la conversion vers le langage machine

Comment faire pour simplifier la programmation ?

Les procédures (ou fonctions)

Définition : succession d’opérations à exécuter pour accomplir une tâche déterminée [Larousse]

Exemple de Syracuse en Python:

Sans procédures

un = 127
if(un % 2 == 0):
   un = un/2
else:
   un = 3 * un + 1
if(un % 2 == 0):
   un = un/2
else:
   un = 3 * un + 1
if(un % 2 == 0):
   un = un/2
else:
   un = 3 * un + 1

Avec procédures

def f(u):
    if(u % 2 == 0):
       return u/2
    else:
       return 3 * u + 1

u = 127
u = f(u)
u = f(u)

Les procédures : illustration

Note

Call PC

Call PC
  • programme de \(f\) : \(0\times 0014\)
  • on utilise ici explicitement le registre A pour stocker les arguments et le résultat
  • retour ?

Implémenter les procédures

Nous avons donc \(4\) problèmes à résoudre :

  • comment partir exécuter la routine ? JMP
  • comment passer les arguments à la routine ?
    • Registres dédiés : combien ?? (e.g. \(R_0-R_3\) pour ARM)
    • autre chose ?
  • comment revenir au programme appelant ?
    • sauvegarder l’adresse de retour dans un registre dédié : link register à sauvegarder lors d’appels cascadés (A appelle B qui appelle C qui appelle …), e.g. ARM
    • autre chose ?
  • comment récupérer le résultat ?
    • un registre dédié ? (e.g. \(R_0\) pour ARM)
    • autre chose ?

Dans notre architecture, on va répondre à ces \(3\) dernières questions en utilisant une structure particulière : la pile

Procédures, pile et pointeur de pile

Procédures, pile et pointeur de pile

LIFO

LIFO

Spécifications d’une pile

Une pile en mémoire

  • structure de données en mémoire principale (RAM)
  • empiler, dépiler une valeur : sommet de pile
  • ou est le sommet de pile : registre Stack Pointer
  • le sommet de la pile désigne la prochaine zone mémoire libre

LIFO

LIFO

Ajout de SP dans le chemin de données

Premier chemin SP

Premier chemin SP

Ajout d’instructions de manipulation de la pile

Spécifications

  • ou placer la pile en mémoire ?
  • quelle est l’adresse du sommet de la pile en mémoire ?
  • comment empiler/dépiler, écrire/lire des éléments de la pile ?

Instructions particulières

  • pour manipuler le registre SP : LDSPi, STSP, INCSP, DECSP
  • pour empiler/dépile : PUSH{A,B}, POP{A, B}
  • pour lire/écrire relativement à SP : POKE{A,B}, PEEK{A,B}

PUSH, POP, POKE, PEEK, Hum?!

Stack

Stack

→ Machines à états (b0, b4, b8, bc)?

Utilisons la pile pour passer des arguments

fonction somme(N)
  Soient i, res deux variables locales
  i ← N-1
  res ← 0
  tant que i ≠ 0 faire
    res ← res + i
    i ← i - 1
  fin tant que
  retourner res
fin fonction

fonction main()
  somme(3)
fin fonction

Comment gérer le retour d’une fonction

Somme: Une première tentative, juste JMP ?

Appel et retour de routines

Spécifications

  • Quand on part exécuter le code d’une procédure, il faut sauvegarder là où retourner (marque page)
  • Quand on termine une procédure, il faut poursuivre le programme appelant

Instructions particulières

  • CALL (0xA000): “CALL op” Empile l’adresse de la prochaine instruction et branche
  • RET (0xA800): “RET” Dépile dans PC l’adresse de retour

Attention, il faut penser à l’ajout de l’adresse de retour lorsqu’on indexe la pile (e.g. PEEK, POKE).

Réalisation de ces instructions

Réalisation de ces instructions

Réalisation de ces instructions

et le résultat au fait? La pile !

Somme : une deuxième tentative

Déroulement d’un appel de routine

Le programme appelant

  • réserve de la place pour le résultat (DECSP)
  • empile les arguments (PUSH)
  • sauvegarde la valeur PC après lecture de l’adresse de la routine et branche sur la routine (CALL)

Programme appelé

  • lit les arguments dans la pile (PEEK),
  • calcule son résultat éventuel et le sauvegarde dans la pile (POKE)
  • retourne au programme appelant (RET) : le registre SP doit être restauré!

Programme appelant

  • dépile les arguments (INCSP ou POP)
  • dépile le résultat (POP) et se poursuit

Programmation avec la pile

Qui met/enlève quoi dans la pile ?

Attention!

Dans cette version d’architecture, les accès PEEK, POKE sont relatifs au sommet de pile !
Autre possibilité : registre Base Pointer (BP) / Frame Pointer (FP)
Pour restaurer le registre SP, on pourrait aussi utiliser BP/FP

C’est parti pour le code machine de :

fonction somme(N)
  Soient i, res deux variables locales
  i ← N-1
  res ← 0
  tant que i ≠ 0 faire
    res ← res + i
    i ← i - 1
  fin tant que
  retourner res
fin fonction

fonction main()
  somme(3)
fin fonction

Les procédures permettent

  • de factoriser le code
  • d’éviter des bugs puisqu’on ne réécrit pas plusieurs fois le “même” code
  • de rendre le code plus compact

mais avec un petit surcoût à l’exécution (parfois inévitable)

La pile permet

  • de passer des arguments à une procédure
  • de récupérer le résultat d’une procédure
  • de sauvegarder l’adresse de retour d’une procédure
  • de sauvegarder des variables locales à la procédure

Des fonctions récursives : Hanoï

Problème

Calculer le nombre de déplacement minimum nécessaires : \[h(n) = \begin{cases} 1 & \mbox{si } n=1 \\ 2h(n-1) + 1 & \mbox{sinon} \end{cases}\] \(h(4) = ?\)

Exercice : Définir le code assembleur pour calculer \(h(4)\).

Exécutons (à 8 Hz) et regardons le contenu de la pile pour visualiser les contextes.

Des fonctions récursives : Hanoï

La pile pourrait “déborder” \(\rightarrow StackOverflow\)

Au fait ..

Comment passer de la formalisation du problème à un algorithme efficace permettant de le résoudre ??

Cours SDA (1A - S06): Structures de Données et Algorithmes

Cours C++ (1A - S06): Programmation C++, structures de données, etc…

Cours Génie logiciel (2A - S07): Etude des méthodes et bonnes pratiques pour le développement logiciel

Simplifions la programmation de la machine

Souhaits

  • un programme moins long, moins répétitif, moins sujet aux bugs : procédures et pile
  • mais on programme toujours en code machine ?!?!

8000 0FFF
1000 0001
2000 0002

LDSPi 0x0FFF
LDAi 0x0001
LDBi 0x0002

La couche d’assemblage

La couche d’assemblage

Couches architecture

Couches architecture

Couches architecture specif

Couches architecture specif

Langage d’assemblage et assembleur

Traduction asm machine call

Traduction asm machine call

Assembleur : programme traduisant langage d’assemblage → code machine.
Abus : programme assembleur = programme en langage d’assemblage

Quelques éléments de syntaxe de notre assembleur

Spécifications

  • On utilise les mnémoniques LDAi, LDBi, STA, CALL, …
  • les valeurs en hexadécimal sans préfixe 0x
  • “;;” commentaire
  • étiquettes : pour les branchements, pour les variables; spécial @stack@
  • pseudo-instructions : e.g. 
    • DSW : allocation de variables globales; par convention en début de programme!

Les variables et les étiquettes ne doivent pas être interprétables en hexadécimal.

Disposition du programme et des données en mémoire (chez nous)

Traduction de quelques structures de contrôle

La boucle tant que

En pseudo-code

tant que i ≠ 0 faire
  res ← res + i
  i ← i - 1
fin tant que

En assembleur

loop: LDAd i
      JZA  end
      LDBd res
      ADDB
      STB  res
      LDBi 1
      SUBA
      STA  i
      JMP  loop
end:  ...

Traduction de quelques structures de contrôle

La boucle for est équivalente à la boucle tant que

for ⇔ while

pour (i = 0 ; i < N ; i = i + 1) faire
  res ← res + i
fin pour

i ← 0
tant que i < N faire
  res ← res + i
  i ← i + 1
fin tant que

for ⇔ while

pour (init; condition ; incrément) faire
  action
fin pour
init
tant que condition faire
  action
  incrément
fin tant que

Traduction de quelques structures de contrôle

Les tests conditionnels :

Pseudo-code

si x ≠ 0 alors
  x ← 1
fin si
x ← x + 1

Assembleur

      LDAd x
      JZA  end
      LDAi 1
      STA  x
end:  LDBi 1
      ADDA
      STA  x

Traduction de quelques structures de contrôle

Pseudo-code

si x == 10 alors
  x ← 0
sinon
  x ← x + 1
fin si

Assembleur

       LDAd x
       LDBi A
       SUBB
       JZB  if 
else:  LDBi 1
       ADDA
       STA  x
       JMP  end
if:    LDAi 0
       STA  x
end: ....

L’assembleur : traduction en code machine

Exercice : Assembler le programme

u ← 127
tant que vrai faire
  u ← next(u)
  print(u)
fin tant que

fonction next(u)
  si u pair alors
    retourner u/2
  sinon
    retourner 3*u + 1
  fin si
fin fonction

Compteur d’emplacement, table des symboles, variables globals; cf syr.asm

Démo : python assemble.py

Langages de haut niveau

Langage de haut niveau (Python, C, C++, Scala, ..)

Mais pourquoi ?

Assembleur

  • spécifique à une architecture
  • encore dur à programmer (LDAd b; LDBd c; ADDA; STA a)
  • peu de vérification syntaxique: on peut ajouter des choux et des carottes

Langage de haut niveau

  • indépendant de l’architecture
  • langage plus intuitif, e.g. \(a = b + c\), structures de contrôle, définition de fonctions
  • vérification syntaxique

Langage : Interprété ou compilé

Langage compilé

Un programme (compilateur) convertit le code source en code machine qui peut ensuite être exécuté
Ex : C++/g++

g++ -S main.cc  
g++ -o main main.cc ; hexdump -C main  
./main

Langage interprété

Un programme (interpréteur) interprète “à la volée” le code source

Ex : Python/ python

python main.py

⇒ Machine virtuelle python

Brève Anatomie d’un compilateur

Références :

  • “The dragon book” Aho, Lam, Sethi, Ullman
  • “Modern Compiler Implementation”, A. Appel

La phase d’analyse (frontend)

Analyse lexicale

Ségmentation et identification des lexèmes

Lexing

Lexing

Analyse syntaxique

Construction d’un arbre syntaxique à partir des lexèmes et d’une grammaire du langage.

La phase d’analyse (frontend)

Génération et optimisation d’une représentation intermédiaire

Représentation intermédiaire

  • indépendante du langage source (C, C++, ..) et de l’architecture (x86, ARM, CentraleSupelec)
  • facile à produire, facile à convertir en code machine
  • optimisable

Ex : register transfer language, gimple, generic, three adress code, single static assignment, control flow graph, …

Example de représentation : Three Address Code

  • opérations binaires : x := y op z
  • opérations unaires : x:= op y
  • copies : x := y
  • sauts (in)conditionnels : goto L; If x relop y goto L
  • procédures : param x1,.. call p, return y

Références :

  • “The dragon book” Aho, Lam, Sethi, Ullman
  • “Modern Compiler Implementation”, A. Appel

Exemple de représentation intermédiaire : Three Adress code

En C++ :

int x = 3;
int y = 2 + 7 + x;
int z = 2*y;
if(x < y) {
  z = x/2 + y/3;
}
else {
  z = x * y + y;
}

Représentation 3-Address Code

     x = 3;
     _t1 = 2 + 7;
     y = _t1 + x;
     z = 2 * y;
     _t2 = x < y;
     IfZ _t2 Goto _L0;
     _t3 = x / 2;
     _t4 = y / 3;
     z = _t3 + _t4
     Goto _L1
_L0: _t5 = x * y;
     z = _t5 + z;
_L1:

Three adress Control Flow Graph (CFG)

Optimisation d’un CFG

Note

Application répétée de quelques règles de simplification

  • supprimer des affectations inutiles
  • remplacer des constantes : int x = 3; int y = x + 2 ⇒ int x = 3 ; int y = 3 + 2;
  • calculer des expressions constantes : int y = 3 + 2; ⇒ int y = 5

jusqu’à ce que plus aucune des règles ne soit applicable

La phase de synthèse (backend)

Représentation intermédiaire ⇒ Code machine

  • génération du code machine de chacun des blocs
  • disposition en mémoire des blocs
  • optimisations éventuelles

Et voila

Compilation frontend backend

Compilation frontend backend

Références :

  • “The dragon book” Aho, Lam, Sethi, Ullman
  • “Modern Compiler Implementation”, A. Appel