IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Introduction au Motorola ColdFire


précédentsommairesuivant

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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
	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 :

 
Sélectionnez
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 :

 
Sélectionnez
LEA.L (-6,A7),A7

réservera une zone de 3 mots dans la pile.
En sortie de module :

 
Sélectionnez
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

 
Sélectionnez
*********************************************************
*							*
* 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.

 
Sélectionnez
	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).


précédentsommairesuivant

Copyright © 2005 Joris Dedieu. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.