Un micro-processeur, qui est ici la machine dont on parle, a comme langage, le binaire. Il lit des instructions, où mots, en binaire, de la mémoire, et les exécutent. Ici, je parle de mot puisqu'on parle de langage, mais aussi, sur les machines avec instructions de taille fixe, ce qu'on appelle un mot est une série de bits qui a la même taille que l'instruction. Sur la SRC un mot ("word" en anglais) fait 32 bits, comme les instructions.
Soit le code suivant, sur la SRC : 00001101 10001000 00000000
00011000
(ou 0D 88 00 18 en hexadécimal)
Ceci forme un mot, puisqu'il y a 32 bits, qui est facile à lire
par un ordinateur, mais pas par un humain. Aussi, ce mot peut avoir
plusieurs significations: l'entier 227016728 (en base 10), ou l'instruction
"ld r22,24(r4)", selon qu'on utilise le mot comme donnée ou comme
instruction.
L'assembleur a donc été inventé pour simplifier
la lecture et l'écriture du code par des humains sans enlever la
flexibilité du langage machine. Il y a souvent une correspondance
1 à 1 entre une ligne assembleur et un mot machine.
L'assembleur (ou le langage d'assemblage): (app. C)
Forme symbolique d'un programme en langage machine.
- Lisible et écrivable par des humains (pas toujours facile,
mais bon...)
- Constantes, adresses et noms d'instructions symboliques
- Directives à l'assemblage (pseudo-instructions ou "pseudo-operations"
dans le livre)
- définiton de constantes
- initialisation de la mémoire
- allocation de mémoire
- assemblage conditionnel
(Ce qui suit n'est pas fait dans l'assembleur utilisé pour la
SRC (auteur F. Boyer))
- Arithmétique lors de l'assemblage pour le calcul des constantes
(on peut écrire 38*164 et il va remplacer par le résultat)
- Instructions synthétiques: instructions qui n'existes pas
vraiment sur le processeur; l'assembleur génère une courte
séquence d'instructions pour faire l'opération.
- Expansion des macroinstructions
Le processus d'assemblage :
Avant l'exécution d'un programme en assembleur, il faut passer
par plusieurs étapes: Figure C.1, page 556.
L'assemblage lui-même se fait souvent en deux passes (c'est ce
que fait mon assembleur pour la SRC). Ces passes sont décrites
à la page 562. En gros, la première passe numérote
chaque ligne avec son adresse et fait une table qui associe à chaque
symbole l'adresse de la ligne où sa définition se trouve
(sauf les ".equ", qui associent une valeur spécifiée dans
la pseudo-instruction). La deuxième passe génère
le code machine en remplaçant les symboles par la valeur associée,
selon la table créée dans la première passe.
L'assembleur de la SRC :
Chaque ligne a la structure suivante (où chaque partie est facultative)
:
étiquette: instruction
; commentaire
L'instruction peut être n'importe quelle opération (couverture
arrière du livre), ou pseudo-opération, suivie des opérandes.
Un programme commence d'habitude par des définitions de constantes
et de l'origine du programme (".org").
Attention, la définition de ".org", sur la couverture du livre,
porte à confusion: ça charge les lignes qui suivent
le ".org" à partir de l'adresse spécifiée. Il
peut dont y avoir plusieurs ".org" pour charger différentes parties
du programme à différents endroits en mémoire.
La SRC est une machine qui effectue les opérations arithmétiques entre les registres et qui a des opérations load et store ("ld" et "st") pour accéder à la mémoire. On dit généralement de ces machines qu'elles sont de type load/store, et ceci est une caractéristique des machines RISC.
Procédures et fonctions : (pas dans le livre)
Ce sont des sous programmes (souvent petits) qui font un travail qui est utile à plusieurs endroits dans le programme (comme des méthodes en Java). Ces sous programmes sont souvent appelés sous-routines ("subroutines" en anglais), ou, en plus court, routines. Normalement on appel "fonction" une sous-routine qui retourne un résultat (habituellement dans un registre), et "procédure" une sous-routine qui modifie le contenu de la mémoire ou fait des entrées/sorties, ou retourne des résultats sur la pile. Dans ce cours, on ne fera pas ces distinctions, qui sont souvent vagues pour des programmes en assembleur, et on utilisera surtout les termes génériques sous-routine ou routine.
Avantages:
- Réutilisation d'un algorithme à plusieurs endroits
dans le programme
- Facilite la maintenance (une seule copie à mettre à
jour)
- Le programme complet peut prendre moins de d'espace en mémoire
Inconvénient principal:
- Vitesse d'exécution moindre (temps pour passer les paramètres,
brancher à la sous-routine, et retourner les résultats)
Après son exécution, une sous-routine doit retourner l'exécution
à l'endroit qui l'a appelée. Il faut donc garder l'adresse
d'où le programme a fait l'appel (l'adresse de retour). Il
y a plusieurs place où un peut garder cette adresse:
- Dans une variable globale (une case mémoire fixe): ceci ne
permet pas l'appel de sous-routines à l'intérieur d'une sous-routine.
- Dans une variable locale à la sous-routine: ceci ne permet
pas l'appel récursif (une sous-routine qui s'appel elle-même).
- Dans une pile
Dans le cas de la SRC, l'instruction qui sert à garder la valeur du PC (l'adresse de l'instruction suivante) est BRL (pour BRanch-and-Link). Cette instruction fait un branchement à l'adresse spécifiée dans l'instruction (l'adresse de la sous-routine) et met l'adresse de retour dans un registre. La sous-routine doit s'occuper de garder la valeur de ce registre pour qu'à la fin de la routine on puisse faire un branchement à cette adresse. On peut, par exemple, empiler la valeur du registre au début de la sous-routine et la dépiler à la fin.
Pile:
Habituellement elle est réalisée dans la mémoire
(un tableau), et on utilise un registre pour nous dire où se trouve
le dessus de la pile. Souvent on met la pile avec son fond dans les
grosses adresses, et son dessus dans les adresses plus petite. Donc,
quand on empile, on dimminue l'adresse du dessus de pile. Ceci permet
une utilisation plus complète de la mémoire; l'allocation
des objets se faisant à partir des petites adresses, il y aura un
problème d'allocation seulement lorsqu'il n'y aura plus de mémoire.
Supposons que r1 est le pointeur de dessus de pile.
Empiler le mot qui est dans le registre r2:
addi r1,r1,-4
st r2,(r1)
Dépiler un mot dans le registre r2:
ld r2,(r1)
addi r1,r1,4
Mettre le k-ième mot à partir du dessus de la pile, dans
r2:
ld r2,k*4(r1)
(ici il faut calculer à la main k*4 puisque l'assembleur utilisé
ne fait pas l'arithmétique à l'assemblage)
Conventions d'appel:
Ce sont des conventions pour le passage des paramètres, le retour des valeurs et la sauvegarde des registres lors d'appels de sous-routines. Pour simplifier la lecture d'un programme, on utilise généralement la même convention pour toutes les sous-routines (ou on spécifi clairement, pour chaque sous-routine, quel convention est utilisée).
Passage de paramètres (même chose pour le retour de
valeurs):
Les paramètres, ou arguments, d'une fonction (sous-routine)
peuvent être passés de différentes façons:
- Les valeurs sont mises dans les registres: rapide mais ne permet
pas un grand nombre de valeurs.
- L'adresse d'un tableau est mise dans un registre: permet un grand
nombre de valeurs, il faut allouer le tableau.
- Les valeurs et/ou les adresses des valeurs (scalaires ou tableaux)
en mémoire sont mises sur la pile.
Exemple de passage par la pile:
; poiteur de pile dans r1
appel: addi r1,r1,-8 ; Empile deux valeurs (avec
les deux additions ensembles)
st r2,(r1)
; Premier argument
st r3,4(r1)
; Deuxieme argument
la r0,max
; L'adresse de la sous-routine
brl r2,r0
; L'appel (met l'adr. de retour dans r2)
; Le resultat est dans r3
; ...
; Prend deux arguments sur la pile et retourne le plus gros
des deux dans r3
max: addi r1,r1,-4 ; Empile
st r2,(r1)
; l'adr. de retour
ld r2,4(r1)
; Le premier argument -> r2
ld r3,8(r1)
; Le deuxieme argument -> r3
sub r2,r2,r3
; Compare les deux
lar r0,maxY
brmi r0,r2
; Branche si premier<deuxieme
ld r3,4(r1)
; Le premier argument -> r3
maxY: ld r2,(r1) ; Prend
l'adresse de retour sur la pile
addi r1,r1,12
; Enleve l'adresse de retour et les arguments de la pile
br r2
; Retourne
Sauvegarde des registres:
Il y a deux conventions principales:
- caller-saved (sauvés par la routine qui fait l'appel):
on sauve tous les registres (habituellement sur la pile), qui contiennent
des valeurs importantes, avant de faire l'appel d'une sous-routine, et
on recharge ces valeurs avant de les utiliser. Dans une sous-routine
on peut écrire dans n'importe quel registre sans avoir à
conserver les anciennes valeurs de registres et les restaurer à
la fine de la routine.
- callee-saved (sauvés par la routine qui est appelée):
au début d'une sous-routine, on sauve tous les registres qu'on utilise,
et à la fin on les restaure. Quand on fait un appel on peut
supposer que les registres out la même valeur lors du retour que
lors de l'appel de la sous-routine (sauf le registre qui sert à
retourner le résultat, si on utilise une convention de retour dans
un registre, et celui utilisé pour conserver l'adresse de retour,
s'il y a lieu comme dans la SRC).
Exceptions : (sections 8.3 et.B.2)
Une exception est un événement hors de la séquence
normale d'exécution, qui force un changement de cette séquence.
Par exemple on pourrait vouloir afficher une horloge. Sur un
processeur qui génère une exception à chaque seconde,
il suffit de faire une routine qui change l'affichage et de dire au processeur
que c'est cette routine qu'il faut exécuter lors de cette exception.
S'il n'y avait pas d'exception, il faudrait faire une boucle qui perd
son temps (busy waiting) à vérifier l'heure tant que
la seconde n'est pas écoulée.
Exception externe - appelée une interruption :
causée par un événement à l'extérieur
du processur; habituellement asynchrone (peut arriver entre deux
cycles d'horloge). Par exemple: horloge, souris, clavier, et en générale
les entrées/sorties.
Exception interne - appelée un(e) trap : causée
par le processeur. Par exemple: division par 0, débordement,
adresse invalide, instruction invalide, violation de protection d'accès
mémoire, défaut de page (pour la mémoire virtuelle).
Les deux types d'exception sont traitées de la même façon
par le processeur: exécute une routine pour traiter l'exception.
Les exceptions:
- ont des priorités; s'il y en a plusieurs en même temps
le processeur commence par exécuter la routine pour l'exception
la plus prioritaire.
- peuvent être masquables ou non-masquables (une
exception masquée ne change pas la séquence habituelle d'exécution
immédiatement, sa routine de traitement est exécutée
lorsqu'on enlève le masque sur l'exception) individuellement ou
globalement.
- sont traitées entre les instructions (attendent que l'instruction
termine) sauf pour certains traps qui doivent être traités
immédiatement (on ne peut pas terminer l'exécution d'une
instruction s'il y a un défaut de page, par exemple).
- causent l'exécution d'une routine. Dans les machines
modernes, la routine exécutée dépend du type d'exception
et il y a un tableau des mots en mémoire qui dit ce qu'il faut faire
pour chaque type d'exception. Ce tableau est souvent appelé
vecteur d'interruption (même s'il contient aussi les informations
pour les traps). Une autre possibilitée est d'avoir une seule
routine et un identificateur du type d'interruption (dans un registre)
pour permettre une exécution conditionnelle selon le type.
Lors du traitement d'une exception, il faut préserver l'état du processeur pour revenir à cet état lorsque le traitement est fini. Cette sauvegarde est souvent faite sur la pile (comme dans le cas des routines). Il faut aussi sauver l'état du processeur tel que vu par le programme interrompu, car l'interruption peut être traitée entre toute paire d'instructions consécutives. Pour les exception qui doivent être traitées en plein milieu d'un intruction (défaut de page dans le cas de mémoire virtuelle) il faut soit préserver le micro-état de la machine (pas nécessairement facile) ou sauver l'état avant l'exécution de l'instruction et recommencer son exécution en entier lors du retour (ce qui est fait sur la plupart des processeurs).
Sur la SRC:
Matériel additionnel pour le traitement des interruption (et
exception):
- signaux ireq et iack
- IE (Interrupt Enable): drapeau d'autorisation des interruption (de
manière globale)
- IPC<31..0> : registre d'adresse de retour de l'exception
- II<31..0> : registre d'identification de la source de l'exception
- Isrc_vect<7..0> := II<23..16> : type de l'exception (fourni
par la source)
- Isrc_info<15..0> := II<15..0> : information supplémentaire
fournie par la source de l'exception
- Instructions: svi, ri, een, edi, rfi
Lors d'une exception le processeur met le PC dans le IPC et passe le
contrôle à l'adresse Ivect<31..0> := 20@0#Isrc_vect<7..0>#4@0.
Les modifications à la description du processeur se trouvent
dans la section B.2 (page 547).
Remarques: IE est mis à 0 lors d'une exception, il faut
s'assurer de sauver II et IPC avant de mettre IE à 1 (pour permettre
les interruptions) dans une routine d'exception. Aussi, il faut s'assurer
qu'il n'y aura pas de trap généré par l'exécution
des instructions avant d'avoir sauvé ces registres.
Organisation générale d'un
programme qui traite des exceptions:
la r0,Start
br r0
; brancher au début du programme
; ...
.org 16*IntNumber
str r0,saveR0
la r0,Serve
br r0
; brancher à la routine de traitement de l'exception
saveR0: .dw 1
; ...
.org 1000h
Serve: ;...
; empiler tous les registres...
svi r2,r3
; sauver l'état II, IPC dans r2, r3
;...
; empiler r2,r3
een
; autoriser d'autres interruptions
;...
; traitement... (corps de la routine)
edi
; inhiber les interruptions
;...
; dépiler r2,r3
ri r2,r3
; restaurer II et IPC
;...
; dépiler tous les registres...
ld r0,saveR0
; restaurer r0
rfi
; retour au programme
; ...
Start: ;...
; initialiser la pile
;...
; initialisation des exceptions, si nécessaire
een
; autoriser les interruptions
;...
Projections en classe (.PDF)