Machine de Von Neumann, assembleur basique

Modèle d’architecture de Von Neumann

L’architecture de Von Neumann décompose l’ordinateur en 4 parties distinctes :

  • l’unité arithmétique et logique (UAL ou ALU en anglais) ou unité de traitement : son rôle est d’effectuer les opérations de base ;
  • l’unité de contrôle, chargée du « séquençage » des opérations ;
  • la mémoire qui contient à la fois les données et le programme qui indiquera à l’unité de contrôle quels sont les calculs à faire sur ces données ;
  • les dispositifs d’entrée-sortie, qui permettent de communiquer avec le monde extérieur.

 

 

Principaux composants

Le microprocesseur

Intel CPU Core i7 6700K

Le microprocesseur (ou unité centrale de traitement, UCT, en anglais Central Processing Unit, CPU) est un composant essentiel qui exécute les instructions machine des programmes informatiques.

Remarque : il est le plus souvent amovible, placé sur un support appelé socket, et équipé d’un radiateur et d’un ventilateur (c’est le composant de la carte mère le plus gourmand en énergie).

Il est schématiquement constitué de 3 parties :

  • l’unité arithmétique et logique est chargée de l’exécution de tous les calculs que peut réaliser le microprocesseur ;
  • les registres permettent de mémoriser de l’information (donnée ou instruction) au sein même du CPU, en très petite quantité ;
  • l’unité de contrôle permet d’exécuter les instructions (les programmes).

 

La mémoire

source : https://fr.wikipedia.org/wiki/Mémoire_(informatique)

La mémoire permet de stocker des données et des programmes.

La mémoire se divise entre mémoire volatile (programmes et données en cours de fonctionnement) et mémoire permanente (programmes et données de base de la machine)

Dans la plupart des mémoires, les informations sont classées par adresses : chaque octet est accessible par une adresse unique.

 

Pour des raisons économiques, les mémoires sont en général divisées en plusieurs familles :

Une mémoire de masse ou mémoire de stockage

HDD

SSD

sert à stocker à long terme des grandes quantités d’informations. Les technologies les plus courantes de mémoires de masse sont électromécaniques (disques durs – HDD) ou à semi-conducteurs (SSD, clefs USB, …), elles visent à obtenir une capacité de stockage élevée à faible coût et ont généralement une vitesse inférieure aux autres mémoires.

ordres de grandeur :

  • capacité : jusqu’à 10 To (HDD)
  • vitesse : jusqu’à 500 Mo/s (SSD)

 

La mémoire vive

SDRAM

SDRAM

espace principal de stockage du microprocesseur, mais dont le contenu disparaît lors de la mise hors tension de l’ordinateur.

ordres de grandeur :

  • capacité : jusqu’à 32 Go
  • vitesse : jusqu’à 2 Go/s

 

Une mémoire cache

Différents niveaux de mémoire

Différents niveaux de mémoire

sert à conserver un court instant des informations fréquemment consultées. Les technologies des mémoires caches visent à accélérer la vitesse des opérations de consultation. Elles ont une très grande vitesse, et un coût élevé pour une faible capacité de stockage.

ordres de grandeur :

  • capacité : quelques ko (L1) à quelques Mo (L2)
  • vitesse : jusqu’à 5 Go/s

 

Le registre de processeur

intégré au processeur. Ce type de mémoire est très rapide mais aussi très cher et est donc réservé à une très faible quantité de données.

ordres de grandeur :

  • capacité : quelques dizaines d’octets
  • vitesse : jusqu’à 30 Go/s

 

Les bus

Pour que les données circulent entre les différentes parties d’un ordinateur (mémoire, CPU et les entrées/sorties), il existe des systèmes de communication appelés bus. Il en existe de 3 grands types :

  • Le bus d’adresse permet de faire circuler des adresses
    par exemple l’adresse d’une donnée à aller chercher en mémoire ;
  • Le bus de données permet de faire circuler des données ;
  • Le bus de contrôle permet de spécifier le type d’action
    exemples : écriture d’une donnée en mémoire, lecture d’une donnée en mémoire.

 

 


Les circuits logiques

Dans les microprocesseurs, les opérations booléennes sont réalisées par des transistors formant ce que l’on appelle des circuits logiques.

Un circuit logique prend en entrée et donne en sortie des signaux électriques à deux états (HAUT et BAS). Il existe deux catégories de circuit logique :

  • les circuits combinatoires (les états en sortie dépendent uniquement des états en entrée)
  • les circuits séquentiels (les états en sortie dépendent des états en entrée ainsi que du temps et des états antérieurs)

On représente les circuits logiques à l’aide de schémas appelés logigrammes (voir l’article sur les booléens pour le détail des symboles).

Voici quelques actions élémentaires que l’on peut réaliser à l’aide de circuits logiques :

Le transistor est l'élément de base des circuits logiques. Un circuit logique permet de réaliser une opération booléenne. Ces opérations booléennes sont directement liées à l'algèbre de Boole (Georges Boole, mathématicien Britanique 1815-1864). L'étude de l'algèbre de Boole dépasse le cadre de ce cours, vous devez juste savoir qu'un circuit logique prend en entrée un ou des signaux électriques (chaque entrée est dans un état "haut" (symbolisé par un "1") ou à un état "bas" (symbolisé par un "0")) et donne en sortie un ou des signaux électriques (chaque sortie est aussi dans un état "haut" ou à un état "bas"). Il existe deux catégories de circuit logique :

Le plus simple des circuits combinatoires est la porte "NON" ("NOT" en anglais) qui inverse l'état en entrée : si l'entrée de la porte est dans un état "bas" alors la sortie sera dans un état "haut" et vice versa. Si on symbolise l'état "haut" par un "1" et l'état "bas" pour un "0", on peut obtenir ce que l'on appelle la table de vérité de la porte "NON" :

E (Entrée) S (Sortie)
1 0
0 1

La porte "NON" est symbolisée par le schéma suivant :

porte NON
porte "NON"

La porte "OU" a deux entrées (E1 et E2) et une sortie S

porte OU
porte "OU"

Table de vérité porte "OU" :

E1 E2 S
0 0 0
0 1 1
1 0 1
1 1 1

La porte "ET" ("AND") a deux entrées (E1 et E2) et une sortie S

porte OU
porte "ET"

Table de vérité porte "ET" :

E1 E2 S
0 0 0
0 1 0
1 0 0
1 1 1

La porte "OU EXCLUSIF" ("XOR") a deux entrées (E1 et E2) et une sortie S

porte OU EXCLUSIF
porte "OU EXCLUSIF"

Table de vérité porte "XOR" :

E1 E2 S
0 0 0
0 1 1
1 0 1
1 1 0

En combinant les portes logiques, on obtient des circuits plus complexes. Par exemple en combinant 2 portes "OU EXCLUSIF", 2 portes "ET" et une porte "OU" on obtient un additionneur :

Additionneur

L’addition de deux bits peut être « simplement » réalisée grâce à une porte XOR :

a b aXORb
0 0 0
0 1 1
1 0 1
1 1 0

Mais cette opération ne prend pas en compte la retenue, car elle donne le résultat sur un seul bit,  :

1b+1b=10b : le 1b de 10b tient pas dans un mot de 1 bit !

Pour additionner des nombres codés sur plusieurs bits (comme les entiers naturels par exemple), il faut réaliser plusieurs additions de bits, mais en tenant compte de la retenue :

Schéma interactif

Cet additionneur permet d’additionner 2 bits (E1 et E2) en tenant compte de la retenue entrante (« Cin » , carry in en anglais). En sortie on obtient le résultat de l’addition (S) et la retenue sortante (« Cout », carry out).

  • Compléter la table de vérité de cet additionneur :
    E1 E2 Cin Cout S
    0 0 0
    0 0 1
    0 1 0
    0 1 1
    1 0 0
    1 0 1
    1 1 0
    1 1 1

Pour réaliser une addition sur des nombres de plusieurs bits, on combine plusieurs circuits logiques d’addition :

Schéma interactif: (testez le)
  • Le premier bit de retenue entrante vaut 0
  • Le dernier bit de retenue sortante est « perdu » lorsqu’il y a dépassement de la capacité (longueur du mot)

Assembleur

Le langage machine est une succession de bits qui est interprétable par le processeur d'un ordinateur.

Comme ces successions de bits sont très peu gérable par un humain, un langage de programmation de bas niveau est nécessaire :

Un langage assembleur est le langage machine machine ou des combinaisons de bits sont représentées par des "symboles" qu'un être humain peut mémoriser.

Un programme assembleur convertit ces "symboles" en la combinaison de bits correspondante pour que le processeur puisse traiter l'information.

Un langage assembleur est souvent spécifique un type de processeur.

On considère ici un processeur de type x86.

On veut faire comprendre à un processeur de ce type l'instruction suivante : écrire le nombre 97 dans le registre AL.

Voici comment écrire cette instruction en langage assembleur : movb 0x61,%al.

Pour comprendre :

  1. la partie movb %al signifie écrire dans le registre AL,

  2. la partie $0x61 correspond au nombre 97 écrit en hexadécimal car 97=61(16).

Voici comment un programme assembleur va convertir en binaire : 10110000 01100001.

Pour comprendre :

  1. la partie movb %al est codée en 10110000,

  2. le nombre 97, donc le code hexadécimal $0x61, s'écrit en binaire 01100001 car 97=64+32+1.

En langage assembleur, on peut rencontrer :

  1. des calculs simples (addition, soustraction, multiplication, ...),

  2. des affectations (placer telle valeur de la mémoire vive dans tel registre et vice versa)

  3. des instructions de saut qui permettent de coder des instructions conditionnelles par exemple.

De plus, il y a trois possibilités pour accéder à un opérande (valeur sur laquelle va être opérée une opération) :

  • Il est directement saisi
  • Il est dans un registre : il faut alors indiquer le nom de ce registre
  • Il est dans la mémoire vive : il faut indiquer son adresse.

Exemples d'utilisation de l'assembleur

Dans les années 40 et 50, les programmes étaient surtout écrits en langage assembleur. Cependant, c'était long, fastidieux et les erreurs étaient fréquentes.

Ensuite, des langages de plus haut niveau ont permis des codes plus simples à écrire et à comprendre par des humains tandis que les progrès des puissances des ordinateurs permettait de compiler ces langages de haut niveau en langage machine en assez peu de temps.

Des programmes tel que le système d'exploitation DOS (le plus utilisé sur PC jusque vers 1990) ou les jeux vidéos pour console comme Super Nintendo ou Mega Drive ont été écrits en langage assembleur.

Source : "INTERROS des LYCEES-Les VRAIS EXOS donnés dans les lycées"-"numérique et sciences informatiques"-Stéphane PASQUET-Nathan

On imagine un ordinateur constitué d'une mémoire centrale et d'un jeu d'instructions.

  • Mémoire : tableau de 32 cellules. Chaque cellule constituée d'un octet.
  • Instructions : code opération sur 3 bits et une partie adresse sur 5 bits

Voici le tableau d'instructions :

Instruction Effet
LD X Lit le contenu de la cellule X (ACC ← #X)
STO X Enregistre dans la cellule X (#X ← ACC)
ADD X Ajoute le contenu de la cellule X (ACC ← ACC + #X)
SUB X Soustrait le contenu de la cellule X (ACC ← ACC - #X)
JMP ADR Saute à l'adresse ADR (CO ← A)
JMPZ ADR Saute à l'adresse ADR si nul
JMPP ADR Saute à l'adresse ADR si positif
JMPN ADR Saute à l'adresse ADR si négatif

On considérera qu'un programme commence à l'adresse 8, les adresses 0 à 7 étant réservées au stockage des données.

  1. Voici un exemple de programme :

    Adresse Contenu
    0 25
    1 14
    ..... ......
    8 LD 0
    9 SUB 1
    10 STO 2
    11 END

    Que fait ce programme ?

  2. Écrire un programme qui traduit l'expression x=4;x=x+5.

    On suppose que les variables x et y sont liées aux adresses 0 et 1.


Simulateur de CPU

Utilisation

Il existe autant de langages machines que de types de processeurs. Les commandes de base sont assez faciles à transposer d'un langage à l'autre.

Par exemple, l'instruction qui permet de copier le contenu d'un registre dans une mémoire donnée par son adresse peut s'appeler suivant les processeurs STO, STR ou STA.

Afin de pouvoir mieux visualiser le fonctionnement d'un processeur et de pouvoir tester ses lignes de code en langage assembleur, nous allons désormais utiliser le simulateur accessible avec ce lien.

Voici un visuel de ce simulateur :

simulateur

Voici une présentation en vidéo de ce simulateur.

Voici un nouvel exercice de programmation en assembleur. Cependant, le jeu d'instructions n'est pas le même que celui de l'exercice 1 car ce jeu correspond désormais à celui du simulateur précédemment présenté ; dans toute la suite du chapitre, c'est ce jeu d'instructions qui sera utilisé.

Voici le tableau d'instructions de ce simulateur :

Instruction Effet
INP Lire une valeur à partir de la console d'entrée
OUT Ecrire une valeur sur la console d'affichage
ADD Addition
SUB Soustraction
MOV Affectation
LDR Affectation
STR Affectation
B Aller à
HALT Fin du programme
CMP Comparaison
BEQ Comparaison (égalité) puis saut à une adresse
BNE Comparaison (égalité) puis saut à une adresse
BGT Comparaison (supérieur) puis saut à une adresse
BLT Comparaison (inférieur) puis saut à une adresse
  • pour les instructions de saut, on donnera un label à la place d'une adresse
  • un nombre est précédé d'un # pour le différencier d'une adresse mémoire
    Quelques exemples :
  • MOV R0 , #25 : affecte la valeur 25 au registre R0

    Le nombre 25 est précédé d'un # pour le différencier d'une adresse mémoire.

  • CMP R0 , #15 : compare 15 et la valeur du registre R0
  • BNE test : saute les lignes pour aller à test si la condition (précédente R0=15) est vérifiée.

    Pour les instructions de saut, on donne un label, ici test, à la place d'une adresse.

  • ADD R1, R0, #10 : ajoute 10 à la valeur du registre R0 et affecte le résultat dans le registre R1
  • LDR R0, 12 : affecte la valeur stockée à l'adresse 12 dans le registre R0
  • STR R0, 12 : affecte la valeur stockée dans le registre R0 à l'adresse 12
  • INP R0, 2 : Lit un nombre et l'affecte à R0. Le 2 est un paramètre pour typer le nombre
  • INP R0, 4 : écrit le contenu de R0. Le paramètre 4 correspond au nombre signé

Vous pouvez obtenir un détail sur le jeu d'instructions de ce simulateur à cette endroit de la page .

  1. Observer le programme écrit en langage machine :

    premier programme
  2. Indiquer la signification de cette suite d’instructions.

  3. Quel est le contenu de R0,R1,R2 à la fin de ces instructions ?

  4. Que signifie la commande OUT R2,4

Voici une image plus complète du programme dans le simulateur :

premier programme
  1. Écrire en langage assembleur du simulateur le programme suivant :

    
    x=15
    y=12
    z=x-y
    print(z)
                
                

Voici un script en langage assembleur :

INP R0,2
INP R1,2
CMP R1,R0
BGT VRAI
OUT R0,4
B DONE
VRAI:
OUT R1,4
DONE:
HALT
                        
  1. Essayer de deviner ce que fait ce programme sans lancer le simulateur

    Vous pouvez vous aider du détail sur le jeu d'instructions du simulateur présent à cet endroit de la page .

  2. Tester le programme en langage assembleur en utilisant le simulateur accessible à cette adresse avec comme saisie 4 puis 8

  3. Tester le programme en langage assembleur en utilisant le simulateur accessible à cette adresse avec comme saisie 7 puis 2

  4. Que fait ce programme écrit en langage assembleur ?

Voici un programme Python très simple :

x = 4
y = 8
if x == 10:
	y = 9
else :
	x=x+1
z=6


                                    

et voici maintenant voici son équivalent en assembleur :

   MOV R0, #4
   STR R0,30
   MOV R0, #8
   STR R0,75
   LDR R0,30
   CMP R0, #10
   BNE else
   MOV R0, #9
   STR R0,75
   B endif
else:
   LDR R0,30
   ADD R0, R0, #1
   STR R0,30
endif:
   MOV R0, #6
   STR R0,23
   HALT

                                    

Après avoir analysé très attentivement le programme en assembleur ci-dessus, vous essaierez d'établir une correspondance entre les lignes du programme en Python et les lignes du programme en assembleur. À quoi sert la ligne "B endif" ? À quoi correspondent les adresses mémoires 23, 75 et 30 ?

Jeu d'instructions détaillé

Voici la liste des instructions de ce simulateur, instructions traduites en français :

  • LDR Rd, <adresse mémoire> Charge la valeur enregistrée dans l'<adresse mémoire> dans le registre d
  • STR Rd, <adresse mémoire> Enregistre la valeur du registre d dans la mémoire spécifiée par <adresse mémoire>
  • ADD Rd, Rn, < opérande 2 > ajoute la valeur spécifiée par < opérande 2 > à la valeur du registre d et enregistre le résultat dans le registre d
  • SUB Rd, Rn, < opérande 2 > Soustrait la valeur de < opérande 2 > à la valeur du registre n et enregistre le résultat dans le registre d
  • MOV Rd, < opérande 2 > Copie la valeur < opérande 2 > dans le registre d
  • CMP Rn, < opérande 2 > Compare la valeur de registre n avec la valeur de < opérande 2 >.
  • B <label> Branchement inconditionnel jusqu'à la positon <label> dans le programme.
  • B <condition> <label> Branchement conditionnel vers la position <label> dans le programme si la dernière comparaison rempli le critère spécifié par <condition>. Les valeurs possibles sont : EQ:égal à ; NE: n'est pas égal à; GT:Plus grand que; LT: Moins grand que.
  • AND Rd, Rn, < opérande 2 > Effectue l'opération bit à bit logique AND (ET) entre la valeur du registre n et la valeur < opérande 2 > et enregistre le résultat dans le registre d.
  • ORR Rd, Rn, < opérande 2 > Effectue l'opération bit à bit logique OR (OU) entre la valeur du registre n et la valeur < opérande 2 > et enregistre le résultat dans le registre d.
  • EOR Rd, Rn, < opérande 2 > Effectue l'opération bit à bit logique XOR(OU exclusif) entre la valeur du registre n et la valeur < opérande 2 > et enregistre le résultat dans le registre d.
  • MVN Rd, < opérande 2 > Effectue l'opération bit à bit logique NOT (NON) sur la valeur < opérande 2 > et enregistre le résultat dans le registre d.
  • LSL Rd, Rn, < opérande 2 > Décale de < opérande 2 > bit(s) vers la gauche la valeur du registre n et enregistre le résultat dans le registre d
  • LSR Rd, Rn, < opérande 2 > Décale de < opérande 2 > bit(s) vers la droite la valeur du registre n et enregistre le résultat dans le registre d
  • HALT Arrête l'exécution du programme.
    • < opérande 2 > peut être #nnn (c'est à dire un nombre, exemple #42) ou bien Rm (c'est à dire le registre m , par exemple R1 est le registre numéro 1)
    • Les registres vont de R0 à R12.
    • Compléments pour les instructions DAT, INP et OUT :

    • la pseudo-instruction DAT vous permet de mettre un nombre dans la mémoire en utilisant l'assembleur. Un label peut aussi être pris comme donnée.
    • INP Rd,2 lis un nombre dans le registre d.
    • OUT Rd,4 retourne en sortie le nombre du registre d
    • Pour OUT, vous pouvez retourner des nombres signés (paramètre 4), des nombres non signés (paramètre 5), des hexadécimaux (paramètre 6) ou des caractères (paramètre 7). Vous pouvez entrer des hexadécimaux comme 0xnnn partout où un nombre est attendu.

La liste d'instructions en anglais :

The AQA Instruction Set.

    LDR Rd, <memory ref> Load the value stored in the memory location specified by <memory ref> into register d.
    STR Rd, <memory ref> Store the value that is in register d into the memory location specified by <memory ref>.
    ADD Rd, Rn, <operand2> Add the value specified in <operand2> to the value in register n and store the result in register d.
    SUB Rd, Rn, <operand2> Subtract the value specified by <operand2> from the value in register n and store the result in register d.
    MOV Rd, <operand2> Copy the value specified by <operand2> into register d.
    CMP Rn, <operand2> Compare the value stored in register n with the value specified by <operand2>.
    B <label> Always branch to the instruction at position <label> in the program.
    B<condition> <label> Conditionally branch to the instruction at position <label> in the program if the last comparison met the criteria specified by the <condition>. Possible values for <condition> and their meaning are: EQ:Equal to, NE:Not equal to, GT:Greater than, LT:Less than.
    AND Rd, Rn, <operand2> Perform a bitwise logical AND operation between the value in register n and the value specified by <operand2> and store the result in register d.
    ORR Rd, Rn, <operand2> Perform a bitwise logical OR operation between the value in register n and the value specified by <operand2> and store the result in register d.
    EOR Rd, Rn, <operand2> Perform a bitwise logical exclusive or (XOR) operation between the value in register n and the value specified by <operand2> and store the result in register d.
    MVN Rd, <operand2> Perform a bitwise logical NOT operation on the value specified by <operand2> and store the result in register d.
    LSL Rd, Rn, <operand2> Logically shift left the value stored in register n by the number of bits specified by <operand2> and store the result in register d.
    LSR Rd, Rn, <operand2> Logically shift right the value stored in register n by the number of bits specified by <operand2> and store the result in register d.
    HALT Stop the execution of the program.


Pour aller plus loin...

Bascule d'un programme en langage Python en un langage proche de l'assembleur

Un langage compilé est un langage de programmation qui lors de son exécution utilise un compilateur, c'est-à-dire un programme qui traduit le code source écrit dans ce langage de programmation en langage assembleur ou en langage machine afin de créer un programme directement exécutable sur la machine.

Ensuite, c'est ce fichier intermédiaire créé qui est exécuté sur la machine.

Par contre, un langage interprété est un langage de programmation qui lors de l'exécution fait appel à un interpréteur qui va exécuter le programme instruction par instruction ainsi :

  1. lire et analyser l'instruction,

  2. si l'instruction est syntaxiquement correcte dans la langage de programmation, l'exécuter,

  3. passer à l'instruction suivante.

Ici, pas de fichier intermédiaire.

Le langage Python est un langage interprété un peu hybride, c'est-à-dire que le code en Python est déjà compilé en un bytecode, un code intermédiaire qui n'est pas directement exécutable par un processeur mais qui est ensuite interprété par un interpréteur.

Pour comprendre comment un programme (simple) écrit dans le langage de haut-niveau Python va être traduit en langage machine, il est intéressant d'utiliser le module dis qui permet d'examiner le bytecode intermédiaire.

Voici le code en Python que l'on va étudier :

x = 4
x = x + 5            
                                                

Pour étudier ce code, il suffit d'excuter le code suivant

import dis   # importation du module permettant l'étude
dis.dis('x = 4; x = x + 5; print(x)')
                                                

Voici ce qui est affiché ; c'est le bytecode désassemblé :

1         0 LOAD_CONST               0 (4)
2 STORE_NAME               0 (x)
4 LOAD_NAME                0 (x)
6 LOAD_CONST               1 (5)
8 BINARY_ADD
10 STORE_NAME               0 (x)
12 LOAD_CONST               2 (None)
14 RETURN_VALUE

Les lignes affichées ont respectivement la signification suivante :

  1. 0 :Le nombre 4 est stocké dans un registre,

  2. 2 : le contenu de ce registre est copié dans la mémoire au niveau de la case dont l'adresse est x,

  3. 4 : la valeur de x est copiée dans un registre,

  4. 6 : le nombre 5 est copié dans un autre registre,

  5. 8 : l'addition des deux nombres (ceux en haut de la "pile" c'est-à-dire repéés par les nombres 0 et 1 de la troisième colonne),

  6. 10 : le résultat de l'addition est copié dans la mémoire au niveau de la mémoire dont l'adresse est x,

  7. 12 : la valeur None est copiée dans un troisième registre,

  8. 14 : cette valeur None est renvoyée.

Ce code est proche de ce que l'on pourrait écrire dans le langage assembleur du simulateur :

        
MOV R0, #4
STR R0, x 
LDR R0, x
MOV R1, #5
ADD R0, R0, R1
STR R0,x
HALT
0
0
0
x:    0