Obligement - L'Amiga au maximum

Lundi 02 juin 2025 - 01:44  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in English


Réseaux sociaux

Suivez-nous sur X




Liste des jeux Amiga

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


Trucs et astuces

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


Glossaire

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


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Associations
Jeux
Logiciels
Matériel
Magazines et médias
Pages personnelles
Réparateurs
Revendeurs
Scène démo
Sites de téléchargement
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


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 :

jsr fonction_label(a6)

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


[Retour en haut] / [Retour aux articles]