Suivez-nous sur X

|
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
ALL
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
Programmation : Initiation à l'assembleur 680x0 : les sous-routines, l'ABI du 680x0
(Article écrit par Victorien Ferry et extrait de GuruMed.net - juin 2003)
|
|
Je fais une sous-routine
Nous avons déjà essayé de créer des programmes court de quelques instructions au
chapitre 1
pour nous familiariser avec la syntaxe. Maintenant, nous allons apprendre tout ce qui sera nécessaire
à l'élaboration d'un programme plus long et plus utile, toujours écrit à 100% en assembleur. Il est d'abord
nécessaire d'apprendre en détail l'architecture du fonctionnement d'un exécutable, pour comprendre exactement
ce qui se passe au niveau le plus bas. Nous verrons seulement plus tard les opérations algorithmiques courantes
tels que les tests, les boucles, etc.
Familiarisons-nous avec un registre spécial dont on n'entendra pas parler souvent : le PC, program counter.
Théoriquement, même le programmeur assembleur n'a pas à y toucher. Il représente simplement un pointeur
sur l'emplacement mémoire actuellement exécuté. Quand un programme s'exécute, le PC exécute l'instruction
qu'il pointe, puis s'incrémente pour passer à la suivante. Les zones mémoire potentiellement pointées par
le PC sont donc celles où le code est chargé en mémoire.
Comment réaliser un saut en sous-routine ? Deux instructions sont disponibles : "bsr" (branchement vers sous-routines)
et "jsr" (jump, saut vers sous-routine).
bsr(.b,.w,.l) labeldebutroutine ;
jsr.l labeldebutroutine ; vers autres sections mémoire
jsr.l decalage(ax) ; vers un code pointé par ax+décalage
|
Dans leurs effets, ces instructions sont identiques (voir plus loin) mais elles différent dans leurs
méthodes d'accès à une zone mémoire de code : "bsr" ne peut être utilisé qu'avec un label posé dans
le code, dans la même section de code (voir chapitre 1).
le format spécifié (".b" ou ".w" ou ".l") définit la taille de la donnée (8, 16 ou 32 bits) maximum pour
exprimer le décalage mémoire du PC jusqu'à ce label. Par exemple, "bsr.b label" ne peut être compilé que
si "label" est déclaré entre -128 et 127 octets de l'instruction "bsr" elle-même dans la mémoire. Cela
ne correspond qu'à une dizaine de lignes de code, avant ou après. Si l'écart entre label et "bsr" est
trop grand, le compilateur indiquera une erreur et ne compilera pas. Même chose pour ".w" qui permet un
écart 256 fois plus grand. C'est rarement une source de bogue, il suffit de changer le format quand nécessaire
(".l" marche toujours, mais ".b" et ".w" sont censés être plus rapides).
Cette note sur les formats servant à indiquer des écarts d'espace mémoire de code est vraie aussi pour
les instructions "bra" et les instructions de branchement de tests). Notez aussi que dans ces cas,
".b" peut être noté ".s".
"jsr" peut s'utiliser dans tous les cas où "bsr" est valide, mais est toujours en ".l" (pas besoin de le
spécifier d'ailleurs). Dans le cas où un saut en sous-routine appelle une autre section code, "jsr"
est obligatoire. Il permet aussi de sauter en sous-routine dans un espace mémoire pointé par un registre
d'adressage :
C'est très puissant, car un registre d'adressage est manipulable à loisir, alors qu'un simple label est
complètement statique et compilé une bonne fois pour toutes. Comme d'habitude avec "move" par exemple,
"decalage(ax)" pointe l'adresse indiquée par "ax + le décalage".
Pour construire une sous-routine simple, seul un label et une instruction "rts" (return to the source)
sont alors nécessaires. Une première sous-routine peut ressembler à :
;-------------------------
ma_sous_routine_simple:
;(mon code ici)
...rts ; retour au code d'appel.
;--------------------------
|
Et un appel à cette sous routine ressemblera à :
... (code juste avant le saut en sous-routine)
... bsr.w ma_sous_routine_simple
... (suite du code après retour)
|
On peut comprendre que la sous-routine (on dira fonction) "ma_sous_routine_simple" peut être appelée
depuis n'importe où, par plusieurs autres fonctions. Comment l'instruction "rts" sait-elle revenir d'où
elle vient ?
Que se passe-t-il lors d'un saut en sous-routine ?
Pour bien comprendre, il faut savoir ce qu'est la "pile de la tâche". Quand il lance un programme, AmigaOS
place dans le registre a7, aussi appelé "sp" (stack pointer, pointeur de pile), l'adresse d'une zone mémoire
dédiée à la tâche, la "pile de la tâche". Vous pouvez remarquer, sous le Workbench, dans le menu "Informations",
qu'une taille maximum de pile est spécifiée pour chaque exécutable (environ 4 ou 8 kilooctets à défaut, je
crois, cela dépend). En entrée du programme, a7 (sp donc) pointe en fait la fin de la pile, car elle est
utilisée "à l'envers". C'est une pile au sens informatique du terme, c'est-à-dire une mémoire "LIFO"
(last in, first out : premier entré, dernier sorti) en opposition au terme "FIFO" (premier entré, premier sorti)
qui représentent des "files". En clair, deux opérations sont possibles avec une pile :
- Entrer une nouvelle information dans la pile.
- Sortir la dernière opération de la pile pour la lire.
Ainsi, si on entre dans l'ordre : 1, 2, 3, 4, 5 et 6 dans une pile, quand on la lira, on lira : 6, 5, 4, 3, 2 et 1
(pour une file FIFO, on aurait l'ordre inverse).
On peut donc utiliser la pile de la tâche pour y stocker, par exemple, la valeur 32 bits de d4, puis la relire :
move.l d4,-(sp) ; réalise sp=sp-4 , puis move.l d4,(sp)
|
...ou lire la dernière valeur 32 bits mis dans la pile :
move.l (sp)+,d4 ; réalise move.l (sp),d4 , puis sp=sp+4
|
De la même façon, imaginons qu'on ait besoin de d4 et d5 pour "autre chose", pour une opération par exemple,
mais que leurs valeurs doivent être les mêmes avant et après cette opération. stockons-les dans la
pile, elle est faite pour ça :
move.l d4,-(sp) ; stockage des valeurs de d4/d5
move.l d5,-(sp)
(code utilisant d4 et d5)
move.l (sp)+,d5
move.l (sp)+,d4 ; déstockage d4/d5 (à l'envers !)
|
Dès lors, comment fonctionnent "jsr"/"bsr" et "rts" ? Lors d'une utilisation de "jsr" et "bsr",
de façon invisible, ceci se produit :
move.l pc,-(sp) ; stockage de l'adresse retour dans la pile.
(puis seulement après, saut)
|
Et lors d'un "rts", cette opération invisible a lieu :
move.l (sp)+,pc ; l'adresse retour est lue dans la pile et c'est là qu'on retourne !
|
Et voilà, maintenant, vous savez qu'un saut dans une fonction ou une sous-routine utilise la pile
pour stocker l'adresse de retour. Ainsi, la pile ne cesse de reculer (-(sp)) et d'avancer ((sp)+),
pendant la vie d'un programme. Mais attention, ceci est une grande source de bogues : la pile doit
pointer au même endroit en entrée et en sortie de sous-routine (ou de tâche), donc pour un
"-(sp)" doit correspondre un "(sp)+".
Dernière note : on peut parfaitement imaginer en algorithmique faire du "code récursif".
Il s'agit d'appeler une fonction "A" depuis... une fonction "A". En pratique, cela reviendrait
à faire une boucle infinie, qui, si vous suivez, "exploserait" la pile, puisque les "bsr"
s'enchaînant sans "rts", elle stockerait de plus en plus d'adresses retour. En fait, en algorithmique,
toute fonction récursive doit contenir une "condition de sortie" comme suit :
ma_fonction_recursive :
...subq.l #1,d0
...tst.l d0 ; teste si plus petit que 0
...ble.s pluspetitque ; saute
......bsr.s ma_fonction_recursive
pluspetitque:
...rts
|
Ici, si vous appelez "ma_fonction_recursive" avec 15 dans d0, la fonction va se lancer elle-même 15
fois décalant la pile de 15x4 octets, puis quand d0 va atteindre 0, 15 "rts" vont refermer la pile.
Tout ça pour vous faire comprendre que réaliser des algorithmes récursifs est possible, mais AmigaOS
ne gérant pas les dépassements de pile, il faut faire attention à ne pas la dépasser.
J'apprends ce qu'est l'ABI du 68000 et à quoi ça sert
Techniquement, il est possible d'utiliser les registres, la mémoire qu'on s'est alloué, et la
pile de la tâche comme bon vous semble, tant que vous n'allez pas lire/écrire la mémoire qui
n'est pas à vous, ou corrompre le pointeur de pile, tout ira bien.
Mais en informatique, il est nécessaire d'utiliser certains standards pour permettre à tout le monde
de mieux travailler. L'ABI (Application Binary Interface) du 68000 est faite pour ça. Il s'agit de
décrire comment sont passés des paramètres en entrée dans une fonction, et comment ces fonctions vont
renvoyer des informations en sortie.
Quand il a construit son processeur, monsieur Motorola a bien spécifié cela (voir tableau ci-après). Que
signifie-t-il ? que a7, comme on l'a vu, est toujours le pointeur de pile, donc pas touche, sauf
stockage/déstockage ponctuel de données. Ensuite, d0,d1,a0,a1 sont les seuls registres "volatiles",
les autres sont "persistants". Cela signifie que lorsqu'on saute dans une fonction "standard",
il faut s'attendre à ce que cette fonction modifie les valeurs de d0/d1/a0/a1, alors qu'il est
garanti que les autres (de d2 à d7, puis de a2 à a6) garderont leurs valeurs au retour de la fonction.
Aussi, les registres volatils sont fréquemment utilisés pour passer des paramètres en "entrée"
aux fonctions, ainsi que pour donner des résultats en sortie, lorsque nécessaire.
Nous respecterons cette ABI dans certaines fonctions importantes que nous écrirons. Cela nous
permettra, par exemple, de les utiliser depuis d'autres langages comme le C, ou de les mettre
dans des ".o" (voir début
du chapitre 1) une bonne fois pour toutes. Ceci avec des documentations indiquant quelles fonctions sont
présentes, leurs noms, quels registres correspondent à quelles données en entrée et en sortie, de
sorte qu'un individu lambda, avec cette simple documentation et ce simple ".o" puisse réutiliser
votre code assembleur sans avoir à l'assembler ou voir le code présent à l'intérieur.
Quels registres sont volatils ou pas dans un passage...
d0 |
d1 |
d2 |
d3 |
d4 |
d5 |
d6 |
d7 |
Volatil |
Volatil |
|
|
|
|
|
|
...de fonction standard ?
a0 |
a1 |
a2 |
a3 |
a4 |
a5 |
a6 |
a7 |
Volatil |
Volatil |
|
|
|
|
|
Privée |
|