Programmes assembleur avec AQA
L'assembleur est le langage de programmation le plus bas niveau que
sait directement executer un processeur. Le but d'un compilateur, comme
GCC pour le langage C par exemple, est de traduire du code de
haut-niveau en du code assembleur exécutable par un processeur.
Un langage d'assembleur est spécifique à chaque architecture de
processeur mais ils ont tous des caractéristiques assez similaires :
- On ne peut pas définir de variables. Les données manipulées sont
soient stockées à un endroit précis en mémoire soit dans un
registre. Il y a un nombre fixe et limité de registres
disponibles. Dit autrement, on aura un nombre fixe de variables pour
écrire un programme.
- Il n'existe pas de structure de contrôle conditionnelle comme le
"if ... then ... else" ou les boucles "while", "for", ...
- Les instructions sont exécutées en séquence et on peut marquer des
points dans une séquence. On pourra grâce à cela rompre la séquence en
faisant des sauts à une branche marquée par un point : soit un saut
direct, soit un saut en fonction d'une comparaison entre deux
valeurs.
- Dans les processeurs de type RISC, comme pour l'assembleur que
l'on utilisera ici, les calculs ne se ne font que via des
registres.
ADD R3, R2, R1
mettra dans le registre R3 le
résultat de l'addition du contenu du registre R1 et du registre R2. Si
on veut additionner deux valeurs en mémoire, il faudra d'abord faire
une lecture en mémoire qui placera le contenu lu dans un registre (une
opération d'écriture fait l'inverse).
Nous utiliserons dans ce TP le simulateur AQA
: http://www.peterhigginson.co.uk/AQA/
dont la
documentation est ici.
Jeu d'instruction
Le jeu d'instruction est précisé dans la documentation et se limite
globalement à des :
- Opérations de calculs (addition, soustraction) ou
décalage/comparaisons de valeurs.
- Opérations de lecture (
LDR
) en mémoire vers un
registre et d'écriture (STR
) d'un registre vers la
mémoire. Pour préciser une adresse, on utilisera les crochets. Par
exemple LDR R2, [R5]
place dans le registre R2 la valeur
à l'adresse précisée dans le registre R5.
- Opérations de saut direct ou en fonction de la dernière
comparaison (
B
, BEQ
...).
- Une opération de fin de programme :
HALT
- Des opérations d'entrée-sortie:
INP R1, 2
pour lire
un entier signé (type 2) dans le registre R1 ou OUT R1, 4
pour en afficher le contenu.
- On peut utiliser des constantes numériques avec # pour faire un
calcul ou faire une affectation avec l'opérateur
MOV
, par
exemple MOV R2, #5
Exemples
Si l'on voulait exécuter le programme suivant écrit en pseudo-code :
lire la valeur de A
lire la valeur de B
si A > B alors afficher A
sinon afficher B
fin
On utilisera les registres R0 et R1 pour lire A et B et le code assembleur sera le suivant :
INP R0,2
INP R1,2
// on compare R0 et R1
CMP R0,R1
// si R0 > R1 on fait
// le saut à plusgrand
BGT plusgrand
// saut non fait
// donc R0 <= R1
// on affiche R1
OUT R1,4
// on saute à la fin
B fin
plusgrand:
// branche R0 > R1
// on affiche R0
OUT R0,4
fin:
// fin du programme
HALT
La partie droite de la fenêtre est le contenu de la mémoire qui
contient à la fois le code des instructions et des données placées en
mémoire. Par défaut, les instructions sont codées à partir de
l'adresse 0. Pour placer des données traitées par un programme, le
plus simple est de les placer au début de la mémoire avec
l'instruction DAT
.
Le code ci-dessous place une séquence d'entiers (exprimés en
héxadécimal) au début de la mémoire (à partir de l'adresse 1). Cette
séquence se termine par la valeur particulière 0. La première ligne
fait un saut à la branche "code" pour ne pas interpréter
les données qui suivent comme des instructions. Le programme ensuite
fait la somme des entiers de la séquence :
// donnees
// debut = @1
B code
dat 0x12
dat 0x20
dat 0x3
dat 0x11
dat 0x0
code:
// R0: adresse à lire, initialisée à 1
// R1: valeur courante
// R2: sommme
MOV R0,#1
MOV R2,#0
boucle:
LDR R1,[R0]
ADD R2, R2, R1
ADD R0, R0, #1
CMP R1, #0
BNE boucle
OUT R2,4
HALT
Détail de l'interface
L'interface représente le chemin de données d'un pseudo-processeur
et simule de manière simplifiée l'exécution d'un programme assembleur
:
- La partie gauche permet d'écrire le code assembleur. On le valide
par "submit" puis le lance par "assemble" et "run". On peut accélérer
la vitesse d'exécution du programme avec le bouton "speed".
- La partie droite est la mémoire qui contient le code compilé du
programme (à partir de l'adresse 0) et aussi les données du
programme.
- Au milieu en haut, il y a les registres avec leur contenu. Les
registres R0 à R12 sont les registres généraux qui sont utilisés par
les programmes. Le registre PC (Program Counter) est un registre
particulier qui contient l'adresse de la prochaine instruction à aller
chercher en mémoire avant de l'exécuter. Le PC est automatiquement
incrémenté de 1 pour lire la prochaine instruction de la séquence ou
modifié par un branchement à un autre endroit du programme.
- MAR et MBR sont des registres qui servent à transférer des données
entre la mémoire et les registres : MAR contient l'adresse où l'on
veut lire ou écrire et MBR la valeur lue ou à écrire.
- En rouge au centre, l'unité de contrôle va décoder la prochaine
instruction et ensuite guider son exécution. Pour cela, le contenu de
l'instruction à exécuter, lu en mémoire dans le registre MBR, est
recopié dans CIR pour être décodé. On peut voir pendant l'exécution
que le nom de l'opération à exécuter s'affiche sur cette unité de
contrôle.
- L'unité de calcul arithmétique et logique fait les calculs et les
comparaisons. Elle possède un registre d'état qui, sous la forme d'un
bit chacun, contient des drapeaux en rapport avec la dernière
opération exécutée : Z pour Zéro (le dernier calcul a donné zéro ou
les nombres comparés sont identiques), C pour carry (une retenue a été
générée et pourra être utilisée pour complèter le résultat du calcul),
N pour Négative (permet notamment la comparaison de deux nombres) et O
pour Overflow (le calcul a débordé et le résultat n'est pas
utilisable, par exemple l'addition de deux positifs donne un
négatif).
- Un ensemble de bus relient tous ces éléments et pendant
l'exécution d'un programme, vous verrez les données transiter le long
de ces bus.
Travail à réaliser
Après avoir testé les deux programmes ci-dessus en observant les
données transitant entre les différents éléments du pseudo-processeur,
vous implémenterez trois programmes en assembleur :
- Un programme qui demande un nombre en entrée et calcule la somme
des nombres de 1 à ce nombre (ex pour 5 : 5 + 4 + 3 + 2 + 1)
- Un programme qui affiche la valeur maximum d'une série d'entiers
placés en mémoire
- Pour les plus sportifs : un programme qui trie en mémoire une
série d'entiers par un tri à bulles par exemple