20 - Assembleur
Cours associé : 10 - Langage assembleur .
QCM – vérification des prérequis
Pour chaque question, une seule réponse est correcte.
1. Quel composant exécute directement les instructions en langage machine ?
- A. La mémoire vive (RAM)
- B. Le disque dur
- C. Le processeur (CPU)
- D. La carte graphique
Correction publiée après la séance.
2. Dans le modèle de von Neumann, où sont stockés les programmes en cours d’exécution ?
- A. Dans les registres du processeur
- B. En mémoire vive (RAM)
- C. Sur le disque dur
- D. Dans la ROM
Correction publiée après la séance.
3. Dans le langage assembleur du simulateur AQA vu en cours, que signifie l’instruction MOV R0, #10 ?
- A. Additionner 10 au registre
R0 - B. Placer la valeur 10 dans le registre
R0 - C. Stocker le contenu de
R0à l’adresse mémoire 10 - D. Comparer
R0à 10
Correction publiée après la séance.
4. Quel est le rôle du registre « compteur de programme » (PC) ?
- A. Compter le nombre de programmes en cours d’exécution
- B. Stocker l’adresse de la prochaine instruction à exécuter
- C. Contenir le résultat du dernier calcul
- D. Mesurer le temps d’exécution
Correction publiée après la séance.
Rappels de cours
Du simulateur AQA au processeur MIPS
Attention, changement de langage. Le cours utilise le simulateur AQA, dont l’assembleur est propre à ce simulateur. Cette fiche utilise l’assembleur d’un vrai processeur, le MIPS, très employé pour enseigner l’architecture des ordinateurs. Chaque famille de processeurs a son propre jeu d’instructions : les idées sont les mêmes (registres, mémoire, calculs, sauts), mais les noms et l’écriture changent.
| Action | AQA (cours) | MIPS (cette fiche) |
|---|---|---|
| Registres | R0 à R12 | $t0, $t1, … (et $zero, qui vaut toujours 0) |
| Placer la valeur 10 dans un registre | MOV R0, #10 | li $t0, 10 |
| Copier un registre dans un autre | MOV R1, R0 | move $t1, $t0 |
| Additionner deux registres | ADD R2, R0, R1 | add $t2, $t0, $t1 |
| Sauter si plus grand | CMP R0, R1 puis BGT etiquette | bgt $t0, $t1, etiquette |
| Sauter sans condition | B etiquette | j etiquette |
| Lire un entier au clavier | INP R0, 2 | li $v0, 5 puis syscall (résultat dans $v0) |
| Afficher un entier | OUT R0, 4 | li $v0, 1, move $a0, $t0, puis syscall |
| Arrêter le programme | HALT | li $v0, 10 puis syscall |
Deux différences méritent l’attention : en MIPS, une instruction de saut conditionnel compare elle-même deux registres, sans CMP préalable ; et les entrées-sorties passent par l’instruction syscall, dont le service est choisi par la valeur placée dans $v0.
Le langage assembleur
Un processeur est constitué de circuits logiques, etc. Il ne prend en entrée que des 0 et des 1, et
ne ressort que des 0 et des 1. À l’aide, par exemple, de circuits comme un multiplexeur, on peut
voir qu’il peut utiliser des parties de circuits différentes suivant le code binaire reçu. Ainsi, on
peut imaginer qu’un signal 00 1100 0101 lui fasse réaliser l’addition entre 1100 et 0101, et qu’un
signal 01 1100 0101 lui fasse réaliser la soustraction entre 1100 et 0101.
Un ordinateur fonctionne donc avec des instructions passées en binaire (des 0 et des 1), toutes
codées sur le même nombre de bits (par ex : 32 bits pour un processeur 32 bits).
Par exemple :
| En binaire | En hexadécimal | En assembleur |
|---|---|---|
00100100000010000000000000001010 | 2408000a | li $t0, 10 |
00100100000010010000000000001100 | 2409000c | li $t1, 12 |
00000001000010010101000000100000 | 01095020 | add $t2, $t0, $t1 |
Pour plus de lisibilité, on peut utiliser un langage d’assemblage, ou langage assembleur, qui représente les instructions par des codes faciles à lire et à retenir pour un humain. Lors de l’assemblage, chaque code d’instruction est traduit en nombre. Chaque instruction correspond à une des tâches basiques que sait faire le CPU : charger une valeur dans une des cases du registre, additionner deux valeurs et charger le résultat dans une des cases du registre, etc.
C’est quoi le registre ? Comment fonctionne un processeur ?
Prenons l’exemple d’un processeur MIPS R2000. Il s’agit d’un processeur 32 bits (= 4 octets, c’est la taille des registres) constitué de :
- 32 registres de 32 bits (stocke les valeurs temporaires, les pointeurs d’adresses, etc.)
- un compteur de programmes PC (= Program Counter) sur 32 bits (stocke l’adresse de la prochaine instruction à exécuter)
- un registre d’instruction RI sur 32 bits (stocke l’instruction en cours d’exécution, chaque instruction est donc codée sur 32 bits.)
- une mémoire vive adressable de 2^32 octets (permet de stocker les instructions du programme, les données, etc.)
La mémoire est comme un grand tableau dont chaque case mesure 32 bits (4 octets), la mémoire mesure \(2^{32}\) octets, donc elle contient \(2^{30}\) cases de 4 octets. Pour les repérer, on utilise des adresses, également sur 32 bits, il y a donc \(2^{32}\) adresses différentes. Chaque case mémoire (qui prend 4 octets) a donc une adresse qui est un multiple de 4. Ces adresses peuvent être représentées :
- en binaire : \(00000000 00000000 00000000 00000000_2\) est la toute première adresse (
0) ; et \(11111111 11111111 11111111 11111100_2\) est l’adresse de la dernière case. - en hexadécimal : \(00 00 00 00_{16}\) est la toute première adresse (
0) ; et \(FF FF FF FC_{16}\) est l’adresse de la dernière case.
Voir schéma en bas de cette page (annexe A)
Concrètement, que se passe-t-il lorsqu’on exécute un programme ?
Le langage Python que nous utilisons en classe est un langage de « haut niveau », évolué. Lorsqu’il est interprété, il est traduit en langage « machine », de manière à pouvoir être exécuté sur un ordinateur.
À partir du langage machine, lorsqu’on fait exécuter le programme :
- le programme est stocké en mémoire :
- les lignes de code sont chargées dans les cases de la mémoire allouées au code,
- les constantes (données =
.data) du programme sont chargées dans les cases de la mémoire allouées aux données.
- Le PC s’initialise à la première ligne de code, l’exécute, il peut s’agir :
- d’affectations de variables,
- de calculs par le CPU,
- de sauts (branchements) dans le programme en modifiant la valeur du PC.
S’il n’y a pas eu de branchement, le PC passe à la ligne suivante (son adresse prend \(+4\)).
Exemple 1
Pour ce qui suit, on se servira des tables en bas de cette page (annexe B): MIPS Reference Card.
Les programmes de cette fiche s’exécutent dans un simulateur MIPS comme MARS, SPIM ou QtSpim. Chacun commence par l’étiquette main:, qui marque la première instruction à exécuter (SPIM et QtSpim l’exigent), et se termine par l’appel système 10 (li $v0, 10 puis syscall), qui arrête proprement le programme.
Exercice 1
Traduire en Python le programme en assembleur suivant.
.text
main:
li $v0, 5
syscall
move $t0, $v0
li $v0, 5
syscall
move $t1, $v0
add $t2, $t0, $t1
li $v0, 1
move $a0, $t2
syscall
li $v0,10
syscall
Indice
Le service 5 (li $v0, 5 puis syscall) lit un entier au clavier et le place dans $v0 : c’est l’équivalent de int(input()). Chaque registre $t0, $t1, $t2 peut devenir une variable Python.
Correction publiée après la séance.
Structures conditionnelle et boucles
Toutes ces structures ont pour conséquence de ne pas toujours exécuter toutes les lignes
d’instructions les unes après les autres, dans l’ordre dans lequel elles sont écrites. On réalise
donc des branchements (jump) en affectant au PC l’adresse de la prochaine instruction à réaliser.
En assembleur, les labels servent à référencer certaines parties du programme en donnant un nom à une ligne du programme. Ceci permet d’y faire référence pour faire des sauts dans le programme, pour réaliser des branchements, pour des structures conditionnelles ou des boucles. Nous ignorerons les questions de « délais de branchement ».
Les branchements conditionnels
Le résultat d’un test peut envoyer à une autre adresse d’instruction. Par exemple, pour la ligne
d’instruction : beq $t0, $t1, Label ; si la valeur contenue dans le registre $t0 est égale à celle
stockée dans le registre $t1 alors la prochaine instruction à exécuter est celle placée après
l’étiquette Label. beq vient de branch on equal (on fait le branchement si c’est égal). Il y a aussi
bne (branch on not equal), bgt (branch greater than) etc.
Exemple 2
.text
main:
li $t0, 2
li $t1, 3
blt $t0, $t1, endif # branch less than
move $t1, $t0
endif:li $v0, 1 # print_int
move $a0, $t1
syscall
li $v0, 10 # exit
syscall
Code Python
x = 2
y = 3
if x >= y : # si x < y, on va directement à endif, sinon, on exécute tout
y = x
print(y) # on affiche le max des deux
Exercice 2
Traduire le programme suivant en Python.
.text
main:
li $v0, 5 # read_int
syscall
move $t0, $v0
bge $t0, $zero, endif # greater than or equal 0
sub $t0, $zero, $t0
endif:li $v0, 1 # print_int
move $a0, $t0
syscall
li $v0, 10 # exit
syscall
Indice
bge saute à endif quand $t0 >= 0 : la ligne sub n’est exécutée que si $t0 < 0, comme dans l’exemple 2. sub $t0, $zero, $t0 calcule 0 − $t0.
Correction publiée après la séance.
Les branchements inconditionnels
Une instruction peut envoyer directement (sans condition) à une autre adresse d’instruction. Par
exemple, la ligne d’instruction : j Label renvoie à la ligne Label.
Exemple 3
.data
msg: .asciiz "gagné !" # on sauve une donnée string
.text
main:
li $t0, 5
li $t1, 0
while:beq $t0, $t1, end # si $t0=$t1, aller à end
li $v0, 5 # read_int
syscall
move $t1, $v0
j while # renvoie à la ligne while
end: li $v0, 4 # print_string
la $a0, msg # adresse de la chaîne à imprimer
syscall # imprime le message
li $v0, 10 # exit
syscall
Code Python
x = 5
y = 0
while x != y : # si x = y, on arrête la boucle
y = int(input())
print("gagné !")
Exercice 3
Traduire ce programme en Python. Que fait-il ?
.data
msg: .asciiz "reste = " # on sauve une donnée string
.text
main:
li $v0, 5 # read_int
syscall
move $t0, $v0
li $v0, 5 # read_int
syscall
move $t1, $v0
while:blt $t0, $t1, end # si $t0 < $t1, aller à end
sub $t0, $t0, $t1
j while #retour au Label while
end: li $v0, 4 # print_string
la $a0, msg # adresse de la chaîne à imprimer
syscall # on l’imprime
li $v0, 1 # print_int
move $a0, $t0 # valeur de l’entier à imprimer
syscall # on l’imprime
li $v0, 10 # exit
syscall
Indice
La boucle s’arrête quand $t0 < $t1 : en Python, la condition du while est la condition contraire. Pour comprendre ce que fait le programme, le dérouler à la main en saisissant 17 puis 5 : quelles valeurs prend $t0 ?
Correction publiée après la séance.
Exercice 4
Traduire ce programme en Python. Que fait-il ?
.data
msg: .asciiz "résultat = " # on sauve une donnée string
.text
main:
li $t0, 15
li $t1, 1
li $t2, 0
while:bgt $t1, $t0, end #si $t1 > $t0, aller à end
add $t2, $t2, $t1
addi $t1, $t1, 1
j while #retour au Label while
end: li $v0, 4 # print_string
la $a0, msg # adresse de la chaîne à imprimer
syscall # on l’imprime
li $v0, 1 # print_int
move $a0, $t2 # valeur de l’entier à imprimer
syscall # on l’imprime
li $v0, 10 # exit
syscall
Indice
Suivre les premiers tours de boucle dans un tableau, avec une colonne pour $t1 et une pour $t2 : que contient $t2 à la fin ?
Correction publiée après la séance.
Exercice 5
Traduire ce programme en Python. Que fait-il ?
.data
msg: .asciiz "résultat = " # on sauve une donnée string
.text
main:
li $t0, 10
li $t1, 1
li $t2, 2
while:bgt $t2, $t0, end #si $t2 > $t0, aller à end
mul $t1, $t1, $t2
addi $t2, $t2, 1
j while #retour au Label while
end: li $v0, 4 # print_string
la $a0, msg # adresse de la chaîne à imprimer
syscall # on l’imprime
li $v0, 1 # print_int
move $a0, $t1 # valeur de l’entier à imprimer
syscall # on l’imprime
li $v0, 10 # exit
syscall
Indice
Même démarche qu’à l’exercice 4, avec mul, qui multiplie : quel produit $t1 contient-il à la fin ?
Correction publiée après la séance.
Annexes
Annexe A

Annexe B

