Assembleur

publicité
Assembleur
Rappels d’architecture
Un ordinateur se compose principalement
d’un processeur,
de mémoire.
On y attache ensuite des périphériques, mais ils sont optionnels.
données : disque dur, etc
entrée utilisateur : clavier, souris
sortie utilisateur : écran, imprimante
processeur supplémentaire : GPU
Mémoire
Le processeur peut regarder trois zones de mémoire :
la zone de code,
la zone de données,
la pile.
En théorie, toutes les zones sont accessibles de la même façon : on
peut donc enregistrer des variables au milieu du code.
En pratique, non !
Les registres
En plus de cette mémoire, le processeur dispose de registres.
en nombre limité : pour MIPS, 32 registres
en taille limitée : 32 ou 64 bits pour la plupart des processeurs
L’accès est très rapide. De ce fait, le processeur ne réalise des
opérations que sur les registres.
Il faut donc continuellement faire des transferts entre la mémoire et
les registres.
Langage machine
Une instruction de langage machine correspond à une instruction
possible du processeur. Elle contient donc
un code correspondant à opération à réaliser,
les arguments de l’opération : valeurs directes, numéros de
registres, adresses mémoire.
code op
opérandes
Langage machine
Si on ouvre un fichier exécutable avec un éditeur (hexadécimal), on
obtient
...
01ebe814063727473747566662e6305f5f43544f525f4c
5f05f5f44544f525f4c4953545f5f05f5f4a43525f4c49
53545f5f05f5f646f5f676c6f62616c5f64746f72735f6
75780636f6d706c657465642e36353331064746f725f69
...
Langage machine lisible
Si on ouvre un fichier exécutable avec un éditeur (hexadécimal), on
obtient
...
01ebe814063727473747566662e6305f5f43544f525f4c
5f05f5f44544f525f4c4953545f5f05f5f4a43525f4c49
53545f5f05f5f646f5f676c6f62616c5f64746f72735f6
75780636f6d706c657465642e36353331064746f725f69
...
C’est une suite d’instructions comme 01ebe814, que l’on peut
traduire directement de façon plus lisible :
add
$t7, $t3 , $sp
C’est ce qu’on appelle l’assembleur.
Assembleur
L’assembleur est donc une représentation du langage machine.
Il y a autant d’assembleurs que de type de processeurs différents.
En projet, on utilisera l’architecture du processeur (et donc
l’assembleur) MIPS.
de type RISC (Reduced Instruction Set Computer), c’est-à-dire avec
peu d’instructions disponibles
sorti dans les années 1980, utilisé jusqu’au début 2000
(PlayStation 2)
utilisé en informatique embarquée
émulateurs :
spim en ligne de commande,
qtspim en version graphique (préférable).
mars implémentation en JAVA.
Registres
Un processeur MIPS dispose de 32 registres de 4 octets chacun. On les
distingue des adresses par le signe ’$’.
On travaillera principalement avec les suivants :
numéro
8-15, 24, 25
31
29
0
2, 3
4-7
nom
$t0-$t9
$ra
$sp
$0
$v0,$v1
$a0-$a3
utilité
registres courants
adresse de retour (après un saut)
pointeur de haut de pile
la valeur zéro
appel et résultat de routines
arguments des routines
Espace mémoire
La zone de mémoire de données se déclare avec le mot-clef .data.
C’est là que l’on stocke les “variables”.
Le “type” des variables renseigne uniquement sur la place en
mémoire de chaque variable : ce ne sont en réalité que des
adresses.
Le “type” .space indique simplement que l’on réserve le
nombre d’octet indiqués (sans les mettre à zéro).
c
n1
n2
tab
.data
.byte
.halfword
.word
.space
’a’
26
353
40
; octet
; 2 octets
; 4 octets
Programme
La zone mémoire du programme est signalée par le mot-clef .text.
.text
li
li
saut: add
addi
bgt
$t1,
$t2,
$t1,
$t2,
$t2,
0
100
$t1, t2
$t2, -1
$0, saut
On y lit :
des instructions,
des points d’accroche (saut), c’est-à-dire des adresses dans le code.
Chargement, sauvegarde
Les instructions commençant par
l chargent (load) dans les registres,
s sauvegardent (save) un registre en mémoire.
Le caractère suivant donne la taille (word ou byte), ou bien
immediate pour une valeur directe.
lw
lb
sw
li
$t0,
$t1,
$t0,
$t0,
a
b
a
5
;
;
;
;
charge a dans t0
charge le premier octet de b dans t1
enregistre t0 dans a
charge 5 dans t0
L’instruction move se contente de copier les registres.
move
$t0, $t1
; t0 := t1
Opérations
La forme générale des instructions binaires est OP $d $s $t qui veut
dire “le registre $d prend la valeur $s OP $t”.
OP peut prendre les valeurs suivantes :
add, sub,
and, or, xor, opérations boolénnes bit-à-bit.
Certains ont également la variante OPi, où $t est remplacé par une
valeur directe.
add
ori
$t0, $t1, $t2
$t0, $t1, 1
; t0 = t1 + t2
; met le bit de poids faible de
; t1 a 1, puis le stocke dans t0
Opérations multiplicatives
Une multiplication de deux entiers à 32 bits peut prendre 64 bits.
L’opération mult sépare le résultat dans deux registres cachés Lo
et Hi. On récupère la valeur avec les opérations mflo, mfhi
(move from lo, hi).
L’opération de division utilise ces mêmes registres pour
enregistrer le quotient (dans Lo) et le reste (dans Hi).
div
mflo
mfhi
$t4, $t5
$t2
$t3
; t2 = t4 / t5
; t3 = t4 % t5
mult
mflo
$t4, $t5
$t0
; t0 = t4 * t5 si ce n’est pas trop grand
J-commandes : sauts
Les sauts inconditionnels commencent par j comme jump :
j
jal
adresse
adresse
jr
$t0
;
;
;
;
va au label "adresse:"
enregistre en plus la ligne du programme
dans ra
va a l’adresse de code t0
B-commandes : branchements
Les saut conditionnels s’appellent branchements et commencent donc
par la lettre b.
beq
bne
bgt
bge
blt, ble
li
li
saut: add
addi
bgt
$t1,
$t2,
$t1,
$t2,
$t2,
branche (saute) si égal
branche si n’est pas égal
branche si supérieur (greater than)
branche si supérieur ou égal
... inférieur (less than)
0
100
$t1, t2
$t2, -1
$0, saut
Que vaut $t1 à la fin de ce morceau de code ?
Appels système
MIPS permet de communiquer avec le système de façon simple par la
commande syscall. La fonction utilisée est déterminée selon la
valeur de $v0.
$v0
1
4
5
8
commande
print int
print string
read int
read string
10
exit
argument
$a0 = entier à lire
$a0 = adresse de chaı̂ne
résultat
$v0 = entier lu
$a0 = adresse de chaı̂ne,
$a1 = longueur max
Chargement d’adresses
Pour accéder à une adresse (un tableau, par exemple), on utilise
l’instruction la (load adress).
Pour accéder à la valeur adressée par $t0, on écrit 0($t0).
On peut également accéder aux valeurs environnantes en variant
le numéro, comme 4($t0).
On ne peut pas écrire i($t0), ni $t1($t0), ni 0(tab).
Pour accéder à l’indice i d’un tableau tab d’entiers de taille 10 :
i:
tab:
.data
.word 5
.space 40
.text
la
$t0,
li
$t1,
add $t1,
add $t1,
add $t0,
lw
$t2,
tab
i
$t1, $t1
$t1, $t1
$t1, $t0
0($t0)
Chargement d’adresses
La mauvaise nouvelle : on ne peut pas écrire
a:
b:
.data
.word
.word
8
0
On se retrouve à factoriser toutes les variables.
.data
vars: .word
.word
8
0
Chargement d’adresses
La mauvaise nouvelle : on ne peut pas écrire
a:
b:
.data
.word
.word
8
0
On se retrouve à factoriser toutes les variables.
.data
vars: .word
.word
8
0
Ce n’est pas grave, on a justement une table des symboles !
Chargement d’adresses
nom
a
b
adresse
0
4
...
On veut calculer a+b, avec a initialisé à 8, b à 0.
.data
vars: .word
.word
.text
la
lw
lw
add
8
0
$t0,
$t1,
$t2,
$t3,
vars
0($t0)
4($t0)
$t1, $t2
Et plus encore ...
Il reste des zones de MIPS que l’on n’utilisera pas :
coprocesseur pour flottants
exceptions (retenue de l’addition, etc ...)
Voir la table de référence sur la page web du cours.
Par exemple, on pourra utiliser l’opération sll (shift left logical) pour
multiplier les indices de tableau par 4 :
add
add
$t1, $t1, $t1
$t1, $t1, $t1
sll
$t1, $t1, 2
Compilation directe
L’assembleur est une version “lisible” du langage machine, mais on
pourrait en écrire directement.
add
$t7, $t3, $sp
Le registre $t0 est en fait le numéro 8, et $sp le numéro 29.
Le code de l’opération est ici coupé en deux : le premier (0)
indique c’est une opération arithmétique, le deuxième de quelle
opération arithmétique précisément.
op1
000000
$t7
01111
$t3
01011
$sp
11101
(inutilisé)
00000
op2
010100
Compilation directe
L’assembleur est une version “lisible” du langage machine, mais on
pourrait en écrire directement.
add
$t7, $t3, $sp
Le registre $t0 est en fait le numéro 8, et $sp le numéro 29.
Le code de l’opération est ici coupé en deux : le premier (0)
indique c’est une opération arithmétique, le deuxième de quelle
opération arithmétique précisément.
op1
000000
$t7
01111
$t3
01011
$sp
11101
(inutilisé)
00000
op2
010100
Soit, en hexadécimal, 01ebe814.
On pourrait donc compiler directement un exécutable MIPS.
Pseudo-instructions
Certaines opérations ne sont pas des opérations réelles, et ne peuvent
pas être traduites directement. Par exemple, l’instruction li
(chargement d’une valeur directe) doit être encodée sous une autre
forme.
li
$t0, 12
ori
$t0, $0, 12
C’est toutefois une traduction mineure, que l’on ferait si on
produisait du code exécutable, mais qui n’apporte pas grand-chose.
Références
Cours d’architecture de Peter Niebert :
http://www.cmi.univ-mrs.fr/~niebert/archi2012.php
Introduction au MIPS :
http://logos.cs.uic.edu/366/notes/mips%20quick%20tutorial.htm
Table de référence du MIPS :
http://pageperso.lif.univ-mrs.fr/~laurent.braud/compilation/mipsref.pdf
Code à trois adresses
Intuition
Le code à trois adresses est une version simplifiée de l’assembleur où
on ne se préoccupe pas
ni de l’adressage des variables,
ni des registres,
ni du protocole des appels de fonction,
ni d’autres subtilités de l’assembleur. . .
Il s’agit simplement de “découper” un code simple en morceaux de
taille (à peu près) trois : deux arguments, un résultat.
Exemple
a := b + f(2*c);
devient
t0
t1
a
:=
:=
:=
2∗c
f( t0 )
b + t1
Instructions
On se donne donc une quantité limitée d’instructions qui imitent
l’assembleur.
opérations arithmétiques et logiques ;
lire, ecrire ;
gestion mémoire limitée :
load, store,
ltab, stab pour les tableaux,
loadimm pour les valeurs directes ;
sauts : jump, jsifaux ;
appels :
param pour déclarer un paramètre,
call pour appeler une fonction,
entree, sortie pour délimiter une fonction.
Exemple, suite
a := b + f(2*c);
t0
t1
a
2∗c
f( t0 )
b + t1
:=
:=
:=
En pratique, on oublie les variables temporaires, il est plus simple de
se référer au numéro de la ligne même.
ligne
0
1
2
3
4
5
6
7
op
load
loadimm
mult
param
appel
load
add
store
arg1
arg2
2
1
2
0
variable
c
f
b
5
6
4
a
Implémentation
struct triplet{
c3a_op op;
int arg1;
int arg2;
char *var;
};
struct triplet code[TAILLECODE];
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
appel
f
op
2
*
op
arg1
c
arg2
variable
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
0
appel
f
op
2
*
op
load
arg1
c
arg2
variable
b
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
0
1
appel
f
op
2
*
op
load
loadimm
arg1
2
c
arg2
variable
b
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
0
1
2
appel
f
op
2
*
op
load
loadimm
load
arg1
c
arg2
variable
b
2
c
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
0
1
2
3
appel
f
op
2
*
op
load
loadimm
load
mult
arg1
c
arg2
variable
b
2
c
1
2
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
0
1
2
3
3
appel
f
op
2
*
op
load
loadimm
load
mult
param
arg1
c
arg2
variable
b
2
c
1
3
2
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
0
1
2
3
3
5
appel
f
op
2
*
op
load
loadimm
load
mult
param
appel
arg1
c
arg2
variable
b
2
c
1
3
2
f
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
0
1
2
3
3
5
6
appel
f
op
2
*
op
load
loadimm
load
mult
param
appel
add
arg1
c
arg2
variable
b
2
c
1
3
2
0
5
f
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
a := b + f(2*c);
a
op
b
+
ligne
0
1
2
3
3
5
6
7
appel
f
op
2
*
op
load
loadimm
load
mult
param
appel
add
store
arg1
c
arg2
variable
b
2
c
1
3
2
0
6
5
f
a
Pile et appels fonctionnels
. . . retour au véritable assembleur.
La pile
Zone de mémoire supposée très grande.
Le registre traditionnellement utilisé pour le haut de pile
s’appelle $sp. Par convention, il pointe vers le mot (4 octets) le
plus en haut.
Attention, on compte à l’envers en MIPS ! On augmente la pile en
diminuant $sp.
données de l’environnement
données du programme
←$sp
0 (jamais atteint)
La pile
Attention, on compte à l’envers en MIPS !
Pour empiler un entier de 4 octets :
addi
sw
$sp, $sp, -4
$t0, 0($sp)
Pour dépiler,
lw
addi
$t0, 0($sp)
$sp, $sp, 4
On n’a pas besoin de s’occuper de la valeur initiale de $sp, elle est
définie comme il faut.
Appels de fonction
Les fonctions (et procédures) en assembleur sont simplement des
adresses dans le code.
le passage des arguments se fait à la main,
la récupération du résultat aussi,
il n’y a pas de “variable locale”.
On utilise la pile pour stocker
le résultat,
les arguments,
les variables locales.
Exemple : fonction simple
function double(n : integer) : integer;
begin double := n + n; end;
begin
a := double (b);
end;
carre: addi $sp, $sp, -4
main: lw
$t0, b
sw
$ra, 0($sp)
addi $sp, $sp, -8
lw
$t0, 4($sp)
sw
$t0, 0($sp)
add $t0, $t0, $t0
jal carre
sw
$t0, 8($sp)
lw
$t0, 0($sp)
lw
$ra, 0($sp)
addi $sp, $sp, 4
addi $sp, $sp, 8
sw
$t0, a
jr
$ra
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
←$sp
$t0
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
←$sp
3
$t0
b
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
3
(...)
3
$t0
←$sp
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
3
(...)
$t0
3
$ra
←$sp
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
3
(...)
$t0
3
$ra
←$sp
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
6
(...)
$t0
3
$ra
←$sp
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
6
6
$t0
3
$ra
←$sp
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
6
6
←$sp
$t0
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
6
6
←$sp
$t0
Exemple de fonction
Si b = 3 :
carre: addi
sw
lw
add
sw
lw
addi
jr
main: lw
addi
sw
jal
lw
addi
sw
$sp,
$ra,
$t0,
$t0,
$t0,
$ra,
$sp,
$ra
$sp, -4
0($sp)
4($sp)
$t0, $t0
8($sp)
0($sp)
$sp, 8
$t0, b
$sp, $sp, -8
$t0, 0($sp)
carre
$t0, 0($sp)
$sp, $sp, 4
$t0, a
←$sp
6
$t0
a
Appels — côté appelant
Pour appeler une fonction, on doit
1
sauvegarder (= empiler) les registres encore utiles,
2
réserver de la place pour le résultat,
3
empiler les arguments,
4
sauter (jal) vers l’adresse de la fonction.
A la réception de l’appel,
1
dépiler le résultat,
2
dépiler les registres sauvegardés.
Appels — côté appelé
Quand on entre dans une fonction,
1
réserver de la place dans la pile pour les variables locales,
2
empiler l’adresse de retour $ra.
Quand on en sort,
2
dépiler l’adresse de retour,
désallouer la mémoire des variables locales (et des arguments),
3
stocker le résultat,
4
sauter vers l’adresse de retour.
1
Exemple : fonction récursive
function serie(n : integer) : integer;
begin
if n = 0 then serie := 0
else serie := n + serie (n-1);
end;
Au moment de l’appel récursif ...
+
n
appel
...
On charge n dans $t0, mais comme on ne s’en sert pas avant l’appel à
la fonction suivante, il faut penser à le sauvegarder.
Exemple : fonction récursive
function serie(n : integer) : integer;
begin
if n = 0 then serie := 0
else serie := n + serie (n-1);
end;
...
addi
sw
addi
li
sub
addi
sw
jal
lw
addi
lw
addi
add
...
$sp, $sp, -4
$t0, 0($sp)
$sp, $sp, -4
$t1, 1
$t0, $t0, $t1
$sp, $sp, -8
$t0, 0($sp)
serie
$t1, 0($sp)
$sp, $sp, 4
$t0, 0($sp)
$sp, $sp, 4
$t0, $t0, $t1
Téléchargement