Dans l’architecture de Von Neumann, une seule mémoire, la mémoire vive (RAM), reçoit à la fois les données et le programme, c’est-à-dire la suite des instructions à exécuter.
Or la mémoire ne contient que des bits. Prenons l’octet 01000001 :
A ;Rien, dans l’octet lui-même, n’indique laquelle de ces lectures est la bonne. C’est l’usage qui décide : le processeur traite comme une instruction le mot rangé à l’adresse indiquée par son compteur de programme, et comme une donnée le mot qu’une instruction lui demande de lire. Le programmeur et le système d’exploitation doivent donc ranger instructions et données à des endroits bien séparés.
Une instruction machine est un mot binaire formé de deux parties :
code de l'opération | opérandes
Remarque : l’ensemble des opérations qu’un processeur sait exécuter s’appelle son jeu d’instructions. Chaque famille de processeurs a le sien : un programme en langage machine écrit pour un processeur ne fonctionne pas sur un autre.
Par exemple, pour un petit processeur imaginaire, l’instruction :
00110010 00001010
pourrait se lire ainsi :
00110010 est le code de l’opération « ajouter une valeur au registre R2 » ;00001010, soit 10 en décimal, est l’opérande : la valeur à ajouter.Une instruction occupe un ou plusieurs mots de la mémoire. Le mot est la quantité d’information que le processeur traite d’un seul coup : 8 bits (un octet) pour un processeur 8 bits, 64 bits (huit octets) pour un processeur 64 bits comme ceux des ordinateurs actuels.
Les premiers programmeurs écrivaient directement ces suites de bits : c’est le langage machine, le seul que le processeur comprenne.
Les jeux d’instructions diffèrent d’un processeur à l’autre, mais on y retrouve presque toujours les mêmes familles d’instructions :
Les processeurs récents ajoutent des instructions plus puissantes, qui remplacent chacune une longue suite d’instructions simples : multiplication et division, calcul sur les nombres flottants (racine carrée, sinus…), copie d’un grand bloc de mémoire, ou même la même opération appliquée d’un coup à plusieurs valeurs.
Enfin, un programme en assembleur ne contient pas que des instructions. On peut y donner un nom à une ligne (une étiquette, utile comme destination d’un saut), réserver de la place pour des données ou donner un nom à une constante : l’assembleur remplace lui-même ces noms par les adresses ou les valeurs correspondantes, ce qui rend le programme plus lisible.
Une suite de 0 et de 1 est très difficile à écrire et à relire sans se tromper. On a donc donné à chaque code d’opération un nom court et facile à retenir, un mnémonique : ADD pour additionner, SUB pour soustraire, MOV pour placer une valeur, LDR pour lire en mémoire… Les registres s’écrivent R0, R1, R2… C’est le langage assembleur. L’instruction binaire de notre processeur imaginaire devient par exemple :
ADD R2, R2, #10
On lit : « ajouter 10 au contenu du registre R2, et ranger le résultat dans R2 ». Le symbole # signale une valeur écrite directement dans l’instruction, et non une adresse.
Voici un court programme en assembleur : il lit deux nombres au clavier, les additionne, puis affiche le résultat.
INP R0, 2
INP R1, 2
ADD R2, R0, R1
OUT R2, 4
HALT
Pour que le processeur puisse l’exécuter, ce texte doit être traduit en langage machine. Cette traduction est faite par un programme appelé lui aussi assembleur : chaque mnémonique est remplacé par son code binaire, chaque registre et chaque valeur par leur codage. La traduction est presque mot à mot : à une ligne d’assembleur correspond, en général, une instruction machine.
En résumé :
Remarques :
li $t0, 10 au lieu de MOV R0, #10).Écrire un programme entier en assembleur reste long et fastidieux. On a donc créé des langages de haut niveau (C, Python, JavaScript…), dont les instructions ressemblent davantage à des phrases ou à des formules mathématiques.
Un langage est dit de haut niveau quand il éloigne le programmeur des détails de la machine : il manipule des variables, des listes ou des fonctions, et non des registres ou des adresses. À l’inverse, un langage de bas niveau comme l’assembleur colle au fonctionnement du processeur. On parle de niveau d’abstraction du langage.

Figure 1 : Abstraction du langage
Le processeur, lui, ne comprend que son langage machine. Tout programme doit donc être traduit avant ou pendant son exécution.

Figure 2 : Compilation interprétation
Trois façons de faire existent :
Pour voir un processeur au travail, nous utilisons le simulateur AQA , écrit par Peter Higginson et utilisable directement dans un navigateur. Il représente une machine de Von Neumann simplifiée, que l’on programme en assembleur.
Deux règles changent tout par rapport à Python :
R0 à R12, soit dans un mot de la mémoire, désigné par son adresse ;L’écran du simulateur comporte trois colonnes.
R0 à R12, qui contiennent les valeurs en cours de calcul ;CMP (une addition ou une soustraction ne les modifie pas) : Z (le résultat de la comparaison est nul), N (il est négatif), C (une retenue est apparue) et V (le résultat dépasse la capacité d’un mot) ;Remarque : l’assembleur ne connaît ni « if… else », ni boucle « while » ou « for ». On les fabrique avec des sauts : un saut conditionnel s’exécute ou non selon le résultat de la dernière comparaison. La destination d’un saut est désignée par une étiquette, un nom placé devant la ligne visée.
Sur la capture ci-dessous, les quatre éléments de l’architecture de Von Neumann sont encadrés :
Légende des encadrements :
La liste complète des instructions se trouve dans la documentation du simulateur , en anglais. Dans une instruction, les opérandes s’écrivent de trois façons :
Rn désigne le registre numéro n ;#n désigne la valeur n elle-même, écrite dans l’instruction ;n désigne le mot de la mémoire d’adresse n.Les instructions dont nous aurons besoin :
| Instruction | Effet |
|---|---|
MOV R1, #25 |
met la valeur 25 dans R1 |
MOV R1, R4 |
recopie dans R1 le contenu de R4 |
LDR R1, 20 |
lit le mot d’adresse 20 et le copie dans R1 |
LDR R1, [R2] |
lit le mot dont l’adresse se trouve dans R2, et le copie dans R1 |
STR R1, 22 |
écrit le contenu de R1 dans le mot d’adresse 22 |
ADD R3, R1, R2 |
calcule R1 + R2 et range le résultat dans R3 ; le dernier opérande peut aussi être #n |
SUB R3, R1, #1 |
calcule R1 − 1 et range le résultat dans R3 |
| Instruction | Effet |
|---|---|
CMP R1, #0 |
compare R1 à 0 et met à jour les indicateurs, sans rien ranger |
BGT etiquette |
saute à l’étiquette si la dernière comparaison a donné « strictement plus grand » |
OUT R1, 4 |
affiche le contenu de R1 dans la zone de sortie |
INP R1, 2 |
attend qu’un nombre soit saisi au clavier, puis le range dans R1 |
HALT |
arrête le programme ; sans cette ligne, le processeur continuerait à lire la mémoire |
Ouvrir le simulateur AQA .
Saisir le programme ci-dessous, puis le valider avec Submit.
MOV R0, #25
MOV R1, #17
ADD R2, R0, R1
OUT R2, 4
HALT
Choisir l’affichage hexadécimal de la mémoire dans OPTIONS.
Déterminer la taille d’un mot de la mémoire et d’un registre.
Observer que :
Repérer à l’écran :
Exécuter le programme instruction par instruction avec Step, en vitesse lente (dans OPTIONS, choisir une vitesse lente). Recommencer autant de fois que nécessaire pour suivre chaque étape.
Décrire par écrit les opérations élémentaires réalisées pour chaque instruction.
Pour chaque ligne, décrire le rôle du PC, de l’UC, de la RAM, de l’UAL et des registres, dans l’état avant l’exécution de la ligne étudiée : pour la ligne 0, on décrit donc l’état initial.
Résumer ces observations dans le tableau suivant, à compléter :
| Ligne | Instruction en assembleur | Instruction en hexadécimal | Etat/rôle du PC | Etat/rôle de l’UC | Etat/rôle de l’UAL | Etat/rôle de la RAM | Etat/rôle du registre |
|---|---|---|---|---|---|---|---|
| 0 | MOV R0, #25 | e3a00019 | Prochaine adresse à chercher : 0 | Vide | Aucun rôle | Zone 0 en attente de chargement | Vide |
| 1 | MOV R1, #17 | e3a01011 | Prochaine adresse à chercher : 1 | A décodé l’instruction e3a00019. En attente de l’instruction e3a01011 | Aucun rôle | Zone 0 chargée. Zone 1 en attente de chargement | valeur 25 affectée au registre R0 |
| … | … | … | … | … | … | … | … |
Saisir le programme ci-dessous, puis le valider avec Submit.
LDR R0, 20
LDR R1, 21
SUB R2, R0, R1
STR R2, 22
HALT
Repasser l’affichage de la mémoire en décimal (OPTIONS, puis signed) : une valeur saisie est lue dans le format affiché, et « 50 » vaudrait 80 en hexadécimal. Avant de lancer le programme, écrire la valeur 50 dans le mot d’adresse 20 et la valeur 8 dans le mot d’adresse 21 : cliquer sur le mot dans la mémoire, taper la valeur, puis appuyer sur Entrée.
Décrire par écrit les opérations élémentaires réalisées pour chaque instruction.
Où se trouve le résultat de la soustraction, et que vaut-il ?
Le programme suivant utilise une étiquette, boucle, et un saut conditionnel BGT.
MOV R0, #5
boucle: OUT R0, 4
SUB R0, R0, #1
CMP R0, #0
BGT boucle
HALT
BGT boucle ?Dans le menu « SELECT », choisir le programme d’exemple « add » : il se charge dans la zone d’édition.
Rappel : chaque instruction
INPmet le programme en pause. L’exécution ne reprend que lorsqu’un nombre a été tapé dans la zone Input et validé avec la touche Entrée. Si rien n’est saisi, le programme reste bloqué sur cette ligne : ce n’est pas une erreur.
Aujourd’hui, presque plus personne n’écrit un logiciel entier en assembleur. Les compilateurs modernes produisent un code machine très efficace, souvent aussi rapide que celui qu’écrirait un spécialiste, et un programme en langage de haut niveau est bien plus rapide à écrire, à relire et à corriger.
L’assembleur reste pourtant utile dans quelques situations précises :
En dehors de ces cas, l’assembleur coûte trop cher : le programmeur doit tout gérer lui-même (chaque registre, chaque adresse, chaque saut), le programme est long, difficile à relire, et il ne fonctionne que sur un seul type de processeur.
MOV, ADD, LDR…) ; le programme assembleur les traduit en langage machine, presque ligne pour ligne.MOV, ADD, STR…), qui commande le travail de l’UAL ;#n, un registre Rn, une adresse en mémoire n.