Assembleur Rappels d`architecture Mémoire Les registres

publicité
Rappels d’architecture
Un ordinateur se compose principalement
Assembleur
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 :
Les registres
En plus de cette mémoire, le processeur dispose de registres.
la zone de code,
en nombre limité : pour MIPS, 32 registres
la zone de données,
en taille limitée : 32 ou 64 bits pour la plupart des processeurs
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 !
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
Langage machinemachine lisible
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
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
Registres
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.
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
Programme
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).
.data
.byte
.halfword
.word
.space
c
n1
n2
tab
’a’
26
353
40
; octet
; 2 octets
; 4 octets
Chargement, sauvegarde
Les instructions commençant par
l chargent (load) dans les registres,
s sauvegardent (save) un registre en mémoire.
La zone mémoire du programme est signalée par le mot-clef .text.
.text
li
li
saut: add
addi
bgt
$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
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.
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 :
Le caractère suivant donne la taille (word ou byte), ou bien
immediate pour une valeur directe.
lw
lb
sw
li
$t1,
$t2,
$t1,
$t2,
$t2,
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
J-commandes : sauts
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).
Les sauts inconditionnels commencent par j comme jump :
L’opération de division utilise ces mêmes registres pour
enregistrer le quotient (dans Lo) et le reste (dans Hi).
j
jal
adresse
adresse
jr
$t0
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
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 ?
;
;
;
;
va au label "adresse:"
enregistre en plus la ligne du programme
dans ra
va a l’adresse de code t0
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
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).
La mauvaise nouvelle : on ne peut pas écrire
On ne peut pas écrire i($t0), ni $t1($t0), ni 0(tab).
a:
b:
Pour accéder à l’indice i d’un tableau tab d’entiers de taille 10 :
i:
tab:
8
0
On se retrouve à factoriser toutes les variables.
.data
.word 5
.space 40
.text
la
$t0,
li
$t1,
add $t1,
add $t1,
add $t0,
lw
$t2,
.data
.word
.word
.data
vars: .word
.word
Ce n’est pas grave, on a justement une table des symboles !
tab
i
$t1, $t1
$t1, $t1
$t1, $t0
0($t0)
Chargement d’adresses
nom
a
b
8
0
Et plus encore ...
adresse
0
4
...
Il reste des zones de MIPS que l’on n’utilisera pas :
coprocesseur pour flottants
exceptions (retenue de l’addition, etc ...)
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
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
Pseudo-instructions
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
01111
$t7
01111
01011
$t3
01011
11101
$sp
11101
00000
(inutilisé)
00000
010100
op2
010100000000
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.
Soit, en hexadécimal, 01ebe814.
On pourrait donc compiler directement un exécutable MIPS.
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
Exemple
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,
a := b + f(2*c);
devient
ni du protocole des appels de fonction,
ni d’autres subtilités de l’assembleur. . .
t0
t1
a
:=
:=
:=
2∗c
f( t0 )
b + t1
t0
t1
a
:=
:=
:=
2∗c
f( t0 )
b + t1
Il s’agit simplement de “découper” un code simple en morceaux de
taille (à peu près) trois : deux arguments, un résultat.
Instructions
Exemple, suite
a := b + f(2*c);
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.
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
Production
Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre.
affect
struct triplet{
c3a_op op;
int arg1;
int arg2;
char *var;
};
a
op
b
+
ligne
0
1
2
3
3
5
6
7
struct triplet code[TAILLECODE];
a := b + f(2*c);
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
La pile
Pile et appels fonctionnels
. . . retour au véritable assembleur.
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
Appels de fonction
Attention, on compte à l’envers en MIPS !
Pour empiler un entier de 4 octets :
addi
sw
$sp, $sp, -4
$t0, 0($sp)
le passage des arguments se fait à la main,
la récupération du résultat aussi,
il n’y a pas de “variable locale”.
Pour dépiler,
lw
addi
Les fonctions (et procédures) en assembleur sont simplement des
adresses dans le code.
$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.
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
On utilise la pile pour stocker
le résultat,
les arguments,
les variables locales.
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
63
(...)
6
←$sp
$t0
3
←$sp
$ra
←$sp
a
b
Appels — côté appelant
Appels — côté appelé
Pour appeler une fonction, on doit
Quand on entre dans une fonction,
1
sauvegarder (= empiler) les registres encore utiles,
1
réserver de la place dans la pile pour les variables locales,
2
réserver de la place pour le résultat,
2
empiler l’adresse de retour $ra.
3
empiler les arguments,
4
sauter (jal) vers l’adresse de la fonction.
Quand on en sort,
A la réception de l’appel,
2
dépiler l’adresse de retour,
désallouer la mémoire des variables locales (et des arguments),
1
1
dépiler le résultat,
3
stocker le résultat,
2
dépiler les registres sauvegardés.
4
sauter vers l’adresse de retour.
Exemple : fonction récursive
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.
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