IV. Gestion du contexte et des variables▲
IV-A. Gestion de la pile▲
La première utilisation que l'on fait de la pile système est la sauvegarde du contexte. En effet, lorsqu'on écrit un module en assembleur, la maîtrise de son contexte d'exécution est toujours imparfaite. Les mécanismes mis en oeuvre dans une machine sont tellement complexes qu'il est impossible dans la plus part des cas de déterminer avec certitude l'ensemble des cas de passage d'un module à un autre. Cela est vrai lorsqu'on travaille en temps partagé, mais aussi en mono tâche à cause des départs en interruption. De plus pour pouvoir réutiliser et maintenir facilement du code, il faut écrire propre.
Dans un module, on va utiliser un certain nombre de registres qu'il faudra impérativement rendre dans leur état initial : sauver le contexte.
Comme nous l'avons dit la pile système est définie par le pointeur dans A7. Pour mémoire une pile est une zone désignée par un pointeur dans laquelle on ajoute les éléments selon un algorithme LIFO (Last In First Out). Le pointeur de pile désigne toujours le dernier emplacement occupé.
- Décrémenter le pointeur
- Pousser l'élément
- Retirer l'élément
- Incrémenter le pointeur
A7 est parfois appelé SP pour stack pointer
Les ColdFire n'ont pas d'instructions PUSH et POP. Par contre, on se rend vite compte que les modes d'adressage indirect registre d'adresse pré décrémenté et post-incrémentés conviennent parfaitement à cet usage.
Imaginons un module dans lequel on utilise A0, A1, D0, D1 . La première chose à faire en début de module, est de sauver le contenu de ces quatre registres. Il faudra les restaurer en sortant. à la fin.
MOVE.L A0, -(A7)
MOVE.L A1, -(A7)
MOVE.L D0, -(A7)
MOVE.L D1, -(A7)
* Code du module ici
* ...
* Restauration du contexte
* Attention à l'ordre *
MOVE.L (A7)+,D1
MOVE.L (A7)+,D0
MOVEA.L (A7)+,A1
MOVEA.L (A7)+,A0
Avec ce système, quel que soit le contexte, on sort toujours proprement d'un module.
Une autre solution consiste à utiliser les instructions LEA et MOVEM. Cela est plus rapide lorsqu'il y a une grande quantité de registres à sauvegarder. Par exemple, sauvons tous les registres de donnée et deux registres d'adresse :
LEA.L (-40,A7),A7 ; on décrémante le pointeur de pile de 8*4+2*4 octets
MOVEM.L A0/A1/D0-D7,(A7)
* Code du module ici
* ...
MOVEM.L (A7),A0/A1/D0-D7 LEA.L (40,A7),A7 ;
on restaure le pointeur de pile
La pile doit être gérée avec la plus grande rigueur. Tout ce qui a été empilé doit être dépilé. Les erreurs en ce domaine peuvent passer inaperçues. Pour ne provoquer des catastrophes qu'une fois en production. L'exemple type est une machine fonctionnant sans arrêt. Si on oublie de dépiler 1 mot long dans un module utilisé 10 fois par jour, on décale le pointeur de pile d'environ 14 ko par an. Un jour au bout de quelques mois la pile va grignoter la mémoire d'un programme (stack overflow). Bien malheureux celui qui doit identifier le problème.
Réécrire l'ensemble des modules vus de la partie précédente avec sauvegarde du contexte.
IV-B. Départ en sous-programme▲
On a tendance à abuser des départs en sous programme.
Il ne faut jamais perdre de vue que ce n'est pas parce qu'on fait
appel deux fois au même bout de code dans un module qu'il est forcément
plus judicieux d'en faire un sous programme que de le recopier.
La problématique est la même qu'en langage évolué. Par exemple en C, vaut-il mieux écrire :
#define min(a,b) a>b?b:a
int
main
(
void
){
int
m =
min
(
50
,25
);
printf
(
"
Le minimum est : %d
"
,m);
return
0
;
}
Ou :
int
min
(
int
a, int
b){
return
a>
b?b:a;}
int
main
(
void
){
int
m =
min
(
50
,25
);
printf
(
"
Le minimum est : %d
"
,m);
return
0
;
}
Cela est une question d'appréciation. On doit toujours se poser la question du coût d'une telle mécanique avant de la mettre en oeuvre.
Un sous programme sera appelé grâce à l'instruction BSR.
Un RTS reviendra vers l'appelant.
BSR effectue un branchement en poussant l'adresse de retour
sur la pile. RTS récupère cette adresse et la force dans PC.
Reprenons l'exemple de l'addition et utilisons un sous programme :
org $10000
memOp equ $20010
memRes equ $2001C
ss_prog : nop
move.L -(A0),D0
move.L (-8, A0),D1
addx.L D0,D1
move.L D1,-(A1)
fin_ss_prog : rts
debut: nop
move.L A0, -(A7) ;sauvegarde du contexte
move.L A1, -(A7)
move.L D0, -(A7)
move.L D1, -(A7)
movea.L #$memOp,A0 ;mise en place des pointeurs
movea.L #$memRes,A1
addi.L #0,D0 ;mise à zéro du bit x
bsr ss_prog
bsr ss_prog
clr.L D0
addx.L D0,D0
move.L D0,-(A1)
move.L (A7)+,D1 ;restauration du contexte
move.L (A7)+,D0
movea.L (A7)+,A1
movea.L (A7)+,A0
fin: nop
On voit que, dans cet exemple, le choix d'un départ en sous programme n'est pas du tout judicieux puisqu'il ajoute quatre accès mémoire pour gagner quatre lignes de code. Par contre, si l'on faisait des additions sur 64 octets ça serait bien plus intéressant.
BSR utilise l'adressage relatif. Le déplacement étant sur 16 bits signés, la portée du branchement est donc limitée. On pourra utiliser JSR en cas de dépassement.
IV-C. Gestion des variables▲
Supposons qu'on utilise le module d'addition précédent dans un programme. Celui-ci se déroule sans anicroche. Tout à coup, un départ en exception interrompt mon programme. Le traitement de l'exception utilisant mon module d'addition, les valeurs en mémoire (opérandes 1 et 2 et résultat) sont irrémédiablement écrasés. Du coup ma machine devient incohérente. C'est le problème de la réentrance des modules, pour lequel, la solution la plus commune est de travailler comme un compilateur.
En langage C une fonction se caractérise par une valeur de retour et
des paramètres : int min(int a, int b)
Sur une machine où l'entier fait 16 bits, ce prototype revient à réserver un
espace d'échange de 3x16 bits entre la fonction appelante et la fonction appelée.
En C rien n'empêche également de déclarer 10, 20, ou 30 variables de type long
int dans une fonction, ou encore une chaine de 200 caractères, ou un tableau 2000 pointeurs ?
On voit bien que nos 8 registres de donnée ne sauraient être suffisants dans tout les cas.
Le moyen le plus rapide de passer des paramètres et des valeurs de retour reste les registres. Lorsque cela est possible, cette méthode dont la rapidité n'a aucune commune mesure avec les accès mémoires de la pile, doit être privilégiée.
D'une manière générale, on peut distinguer trois types de variables :
- Les variables internes. Elles ne concernent qu'un module donné.
- Les variables externes. Qui sont les paramètres et les valeurs de retour.
- Les variables globales qui sont communes à plusieurs modules.
Pour gérer les variables, les compilateurs utilisent l'instruction LINK. Nous ferons de même. L'instruction LINK a pour but de créer des zones dans la pile pour stocker des variables. On l'utilise de la façon suivante :
LINK Ax,#-déplacement
Généralement par convention c'est A6 qui est utilisé. Il va sans dire que si on utilise ce système, A6 ne doit plus être touché.
Que se passe-t-il lorsqu'on fait un LINK A6 #-4 ?
LINK utilise toujours des opérandes d'un mot.
- la valeur d'A6 est poussée dans la pile.
- la valeur de A7 est copiée dans A6.
- Le déplacement sur 16 bits sera étendu sur 32 bits.
- Le déplacement (négatif) sera ajouté à A7.
- A7 sera donc décrémenté de 4 mots.
On aura donc dans notre module accès à notre zone de variable en utilisant A7 et le déplacement positif approprié.
Si on change de module, et que le nouveau module commence par un link, nous aurons accès à la zone de variable du premier module par A6 et un déplacement positif. (il faudra tenir compte de ce qui aura été empilé entre temps. Comme par exemple l'adresse de retour.)
Il faudra faire un ULNK A6 à chaque sortie de module.
L'instruction LINK permet donc de gérer les variables internes et
externes d'un module. Si par contre on n'a besoin que de variables
internes, on peut se contenter d'utiliser LEA qui est une instruction
bien moins gourmande.
Par exemple :
LEA.L (-6,A7),A7
réservera une zone de 3 mots dans la pile.
En sortie de module :
LEA.L (6,A7),A7
remettra tout en place.
Enfin pour les variables globales et les constantes, on peut utiliser des adresses fixes (pas de réentrance, pas translatable), ou un autre pointeur sur la pile (par exemple A5).
La gestion des variables n'est pas une mince affaire.
Il faut absolument être méthodique : ne pas hésiter à faire des schémas
et utiliser des noms explicites.
La programmation en assembleur permet de ne pas être aussi systématique qu'un compilateur.
Il faut savoir profiter de cette souplesse.
Toutefois les modules doivent rester réentrant et translatables.
Pour mieux appréhender la gestion des variables, regardons le squelette d'un module utilisant deux variables de 32 bits en interne. Il appellera un second module en passant une variable de 16 bits en paramètre. Le second module utilisera quand à lui une variable interne de 32 bits
*********************************************************
* *
* Définition des pointeurs *
* *
* * Etape 1 (e1) : on réserve 3x32 bits pour module 1 *
* (laissons la pile alignée sur un multiple de 4) *
* * Etape 2 (e2) : on accède aux 3 variables du module 1*
* * Etape 3 (e3) on part en sous programme *
* * Etape 4 (e4) on réserve 32 bits pour module 2 *
* * Etape 5 (e5) on accède aux variables depuis module 2*
*********************************************************
* Pile e0 e1 e2 e3 e4 e5
* 3FFE1
* 3FFE2
* 3FFE3
* 3FFE4<---------------------A7--(A7)
* 3FFE5 |
* 3FFE6 |
* 3FFE7-----------------------------
* 3FFE8<---------------------A6--(8,A6)
* 3FFE9 |
* 3FFEA |
* 3FFEB |
* 3FFEC<-----------------A7 |
* 3FFED |
* 3FFEE |
* 3FFEF |
* 3FFF0<-----A7--(A7)<----------------
* 3FFF1 | |
* 3FFF2 | |
* 3FFF3-------------------------------
* 3FFF4<---------(4,A7)
* 3FFF5 |
* 3FFF6 |
* 3FFF7---------------
* 3FFF8<---------(8,A7)
* 3FFF9 |
* 3FFFA |
* 3FFFB---------------
* 3FFFC<-----A6
* 3FFFD
* 3FFFE
* 3FFFF
* 40000<-A7
Faire de tels schémas est souvent la meilleure solution pour s'y retrouver.
org $20000
mod1: nop
link a6,#-6 ;étape 1 à noter que link est en .w
;Faire quelque chose avec ces variables
move.l (8,A7),D0
move.l (4,A7),D0
;Initialisation de la variable externe
move.w #$BBBB,D0
move.w D0,(A7)
;départ en sous programme
bsr mod2
;liberer la pile
unlk a6
fin1: nop
mod2: nop
link a6,#-2
;récuperer la variable externe
move.w (8,a6),D1 ; Oh ! miracle BBBB.w apparait dans D1 :)
;faire des choses
;...
;puis partir
unlk a6
fin2: rts
Pour se familiariser avec la gestion des variables on pourra également traduire en module réentrant les exemples vus précédemment. Une bonne idée enfin est de regarder le code assembleur résultant de la compilation d'un programme en C (Option -S de gcc).