LA PROGRAMMATION EN ASSEMBLEUR _NOSTUB POUR TI-89/92+
Rédigé pour Ti-Fr par Kevin Kofler.
Version 1.02
An English version is also available here.
I) Objectifs de ce tutorial
Ce tutorial vise à apprendre aux débutants en assembleur (pas en C), ou aux programmeurs en assembleur ayant des expériences uniquement en la programmation de programmes nécessitant un kernel, la programmation de programmes ne nécessitant aucun kernel (dits "programmes _nostub"). Il ne vise pas à décrire l'assembleur Motorola 68000 lui-même ou les ROM_CALLs du système Advanced Mathematics Software (AMS). Pour cela, référez-vous aux liens suivants:
II) Différences entre le mode kernel et le mode _nostub
Les différences relevantes entre le mode kernel et le mode _nostub sont les suivantes:
En résumé, un programme en _nostub est plus facile à utiliser qu'un programme en mode kernel, et il peut faire tout ce qu'un programme en mode kernel peut faire, à condition de connaître les techniques, qui sont parfois différentes. Ne vous laissez pas décourager si la liste ci-dessus de choses auxquelles il faut faire attention vous paraît compliquée. C'est beaucoup plus simple que l'on pourrait croire à première vue. J'espère qu'après la lecture de ce tutorial, vous programmerez également en mode _nostub comme moi.
III) Les bases de la programmation en _nostub
III.1) Avant de commencer à programmer
Avant de commencer, il vous faudra télécharger les outils nécessaires à la programmation, c'est-à-dire la version la plus récente du paquet TIGCC. (On utilisera notamment l'environnement de programmation TIGCC IDE, le header OS.h, l'assembleur A68k et, dans la partie V, la librairie statique TIGCCLIB.)
III.2) Premiers pas en _nostub
Nous commencerons par écrire un programme vide en _nostub. Donc il vous faudra ouvrir TIGCC IDE et créer un nouveau projet en assembleur A68k. Vous pourrez utiliser ce même projet pour tout ce qui suit. Voici le programme:
include "OS.h" ;fichier include pour les programmes en _nostub, contenant notamment les ROM_CALLs xdef _nostub ;pas besoin de kernel xdef _ti89 ;Ce programme tourne sur TI-89. xdef _ti92plus ;Ce programme tourne sur TI-92+. rts
Vous remarquerez qu'il n'y a pas de _main: ni de xdef _main. En effet, cela est totalement optionnel en assembleur _nostub vu que l'exécution commence toujours au début du programme. Il est possible de mettre un label _main juste après les instructions xdef, avant rts. Le mettre autre part serait un non-sens. Pour commencer l'exécution ailleurs, un bra debut juste après les instructions xdef suffit.
Autre particularité: il n'y a pas de END à la fin. Cela n'a rien à voir avec le mode _nostub, mais la dernière version de A68k n'en a plus besoin, donc on peut s'en passer.
III.3) Quelques mots sur la sauvegarde des registres
En _nostub, les registres doivent être sauvegardés manuellement. Nous allons voir comment le faire à l'aide d'un exemple tout bête: un programme qui se contente d'attendre l'appui d'une touche.
La solution la plus simple est de sauvegarder tous les registres sur la pile, c'est-à-dire:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d0-d7/a0-a6,-(a7) ;sauvegarde TOUS les registres ROM_CALL ngetchx ;appelle le ROM_CALL ngetchx ;La partie suivante (III.4) vous apprendra à optimiser cet appel. movem.l (a7)+,d0-d7/a0-a6 ;restaure TOUS les registres rts
Cependant, un programme a le droit de modifier les registres a0-a1/d0-d2. (ATTENTION: Les ROM_CALLs ont également le droit de modifier ces registres!) L'exemple se réduit donc à:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d3-d7/a2-a6,-(a7) ;sauvegarde les registres ROM_CALL ngetchx ;appelle le ROM_CALL ngetchx ;La partie suivante (III.4) vous apprendra à optimiser cet appel. movem.l (a7)+,d3-d7/a2-a6 ;restaure les registres rts
De plus, on ne doit sauvegarder que les registres que l'on utilise. (Comme les ROM_CALLs ne peuvent détruire que les registres a0-a1/d0-d2 qu'on n'est pas obligés de sauvegarder, on ne doit pas en tenir compte. Cependant, ATTENTION, la macro ROM_CALL détruit le registre a4!)
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus move.l a4,-(a7) ;sauvegarde a4 (ici, move suffit puisqu'il y a un seul registre) ROM_CALL ngetchx ;appelle le ROM_CALL ngetchx ;La partie suivante (III.4) vous apprendra à optimiser cet appel. move.l (a7)+,a4 ;restaure a4 rts
III.4) Comment optimiser les ROM_CALLs en _nostub
Comme nous l'avons vu dans l'exemple précédent, la macro ROM_CALL permet de facilement effectuer un ROM_CALL (comme jsr doorsos::ngetchx en mode kernel). Cependant, cette macro détruit a4 pour y placer l'adresse de la fonction. Ceci peut être pratique pour appeler la même fonction plusieurs fois de suite, mais la plupart du temps, ce n'est pas idéal. Regardons donc comment la macro ROM_CALL est définie et pourquoi elle est définie de cette manière. Extrait de OS.h et commenté:
ROM_CALL macro move.l $C8,a4 ;place l'adresse de la table des ROM_CALLs en le registre a4 move.l \1*4(a4),a4 ;calcule l'adresse de la fonction et la place en a4 jsr (a4) ;appelle la fonction de la ROM endm
Une première idée est de remplacer a4 par a0. Ceci a 2 avantages:
Le seul désavantage est que si on veut appeler la fonction une deuxième fois de suite, on doit refaire tous les calculs alors que pour la première méthode, jsr (a4) suffit. Mais passons à l'implémentation:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus ;Aucun registre ne doit être sauvegardé ici. move.l $c8,a0 ;place l'adresse de la table des ROM_CALLs en le registre a0 move.l ngetchx*4(a0),a0 ;calcule l'adresse de ngetchx et la place en a0 jsr (a0) ;appelle ngetchx rts
Cependant, pour un programme appelant de nombreux ROM_CALLs, il existe une méthode
encore meilleure: on peut placer l'adresse de la table des ROM_CALLs en un registre qui ne
sera pas détruit par le ROM_CALL et qu'on ne détruira pas par la suite. C'est
aussi l'astuce utilisée en C par OPTIMIZE_ROM_CALLS de TIGCC.
include "OS.h"
xdef _nostub
xdef _ti89
xdef _ti92plus
move.l a5,-(a7) ;sauvegarde a5
move.l $c8,a5 ;place l'adresse de la table des ROM_CALLs en le registre a5
;Cela ne doit être fait qu'UNE SEULE fois AU DÉBUT du programme.
;On réutilisera ensuite a5 pour chaque ROM_CALL!
move.l ngetchx*4(a5),a0 ;calcule l'adresse de ngetchx et la place en a0
;On N'utilise PAS a5 ici! a5 doit rester constant.
jsr (a0) ;appelle ngetchx
move.l (a7)+,a5 ;restaure a5
rts
Ensuite, pour chaque ROM_CALL, il suffira d'utiliser:
move.l ngetchx*4(a5),a0 jsr (a0)
Cela permet de réduire la taille nécessitée par chaque ROM_CALL à 6 octets. Le jsr doorsos::ngetchx du mode kernel nécessite entre 8 et 10 octets (8 octets par appel + 2 octets supplémentaires pour chaque ROM_CALL différent). Grâce à ceci et au fait qu'il n'y a pas de "stub", un programme _nostub bien programmé peut être plus petit qu'un programme pour kernel correspondant.
III.5) ROM_CALL2 ou les variables globales de AMS
Dans le paragraphe précédent, nous avons vu des ROM_CALLs qui représentent des fonctions. Cependant, il en existe aussi qui correspondent à des variables, comme par exemple FirstWindow. Nous allons voir comment les utiliser à l'aide d'un programme qui se contente de placer cette adresse en a2 pour un moment. (Nous verrons pas ici comment l'afficher. Ceci est possible en utilisant les ROM_CALLs sprintf et DrawStr, que vous apprendrez à utiliser dans le paragraphe suivant.) Pour ces ROM_CALLs, la manière la plus simple est d'utiliser la macro ROM_CALL2, qui, comme ROM_CALL, détruit a4. Voilà le programme:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l a2/a4,-(a7) ;sauvegarde a2 et a4 ROM_CALL2 FirstWindow ;place l'adresse de la variable FirstWindow en a4 move.l (a4),a2 ;place le contenu de la variable FirstWindow, un pointeur vers une structure ;WINDOW décrivant la fenêtre actuelle, en a2 ;Ici, on utiliserait a2. movem.l (a7)+,a2/a4 ;restaure a2 et a4 rts
Regardons maintenant comment la macro ROM_CALL2 est définie et pourquoi elle est définie de cette manière. Extrait de OS.h et commenté:
ROM_CALL2 macro move.l $C8,a4 ;place l'adresse de la table des ROM_CALLs en le registre a4 move.l \1*4(a4),a4 ;calcule l'adresse de la variable et la place en a4 ;Contrairement à ROM_CALL, il n'y a pas ici le jsr (a4) qui appelle la fonction. ;En revanche, l'adresse de la variable est placée en a4 et peut être utilisée. endm
On peut donc utiliser les mêmes techniques d'optimisation que dans la partie ci-dessus, c'est-à-dire soit simplement remplacer a4 par un autre registre comme a0 qu'on n'est pas obligés de sauvegarder ou a2 qu'on détruit de toute façon puisque c'est le registre de destination choisi, soit utiliser un registre constant pour la table des fonctions. Ce dernier cas est le plus intéressant, donc voici le programme adapté:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l a2/a5,-(a7) ;sauvegarde a2 et a5 move.l $c8,a5 ;place l'adresse de la table des ROM_CALLs en le registre a5 ;Cela ne doit être fait qu'UNE SEULE fois AU DÉBUT du programme. ;On réutilisera ensuite a5 pour chaque ROM_CALL! move.l FirstWindow*4(a5),a2 ;calcule l'adresse de FirstWindow et la place en a2 ;On N'utilise PAS a5 ici! a5 doit rester constant. ;J'ai choisi a2 puisque c'est la destination choisie. move.l (a2),a2 ;place le contenu de FirstWindow en a2 movem.l (a7)+,a2/a5 ;restaure a2 et a5 rts
III.6) Comment utiliser les ROM_CALLs
Dans les exemples précédents, nous avons vu 2 ROM_CALLs: ngetchx et FirstWindow. Ce ne sont que 2 des centaines de ROM_CALLs de AMS. Évidemment, il est hors de question de les documenter tous ici. Pour cela, veuillez vous référer à la documentation de TIGCC. Cette documentation contient aussi une partie Information for Assembly Programmers qui explique comment les utiliser en assembleur. Je vais tout de même répéter les informations les plus importantes ici:
Nous avons maintenant les connaissances suffisantes pour écrire un Hello, World! en _nostub. Commençons par écrire notre texte dans la barre d'état:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus ;Aucun registre ne doit être sauvegardé ici. pea.l hello_world(PC) ;passe le message à afficher en argument move.l $c8,a0 move.l ST_helpMsg*4(a0),a0 jsr (a0) ;appelle ST_helpMsg addq.l #4,a7 ;nettoie la pile rts hello_world: dc.b 'Hello, World!',0
Malheureusement, on risque de devoir afficher du texte autre part que dans la ligne d'état. Affichons donc notre message en haut à gauche à l'aide du ROM_CALL DrawStr, beaucoup plus flexible:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus ;Aucun registre ne doit être sauvegardé ici. move.w #4,-(a7) ;passe l'attribut A_REPLACE en argument pea.l hello_world(PC) ;passe le message à afficher en argument clr.l -(a7) ;passe x=0 et y=0 en argument (On efface 4 octets, dont 2 pour x et 2 pour y.) move.l $c8,a0 move.l DrawStr*4(a0),a0 jsr (a0) ;appelle DrawStr lea.l 10(a7),a7 ;nettoie la pile rts hello_world: dc.b 'Hello, World!',0
Vous remarquerez que le texte reste sur l'écran après exécution et que ce n'est pas très beau. Sur votre calculatrice, appuyez sur [F5] 2 fois pour vous en débarasser. La partie suivante nous montrera la solution au problème.
III.7) Quelques lignes toutes prêtes pour la sauvegarde de l'écran
Pour compléter cette partie et pour fournir une application importante du paragraphe qui précède, je vais donner des sources commentées, que vous pouvez aussi utiliser telles quelles si vous voulez, mais que je vous conseille quand-même de comprendre puisqu'il s'agit d'une application de ce qui précède. Voilà:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d4/a5,-(a7) ;sauvegarde d4 et a5 move.l $c8,a5 ;place la table des ROM_CALLs en a5 pea.l 3840 ;taille à allouer ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;alloue les 3840 octets move.l a0,d4 ;sauvegarde l'adresse du bloc alloué en d4 tst.l d4 beq nomem ;si le pointeur est nul, quitte le programme move.l #3840,(a7) ;taille à copier ;Cette instruction n'est pas vraiment nécessaire puisque HeapAllocPtr ne modifiera ;pas la valeur qui est déjà sur la pile, mais mieux vaut ne pas se fier. pea.l $4c00 ;source: adresse de l'écran move.l d4,-(a7) ;destination: adresse du bloc alloué move.l memcpy*4(a5),a0 jsr (a0) ;sauvegarde l'écran lea.l 12(a7),a7 ;nettoie la pile ;début du programme principal move.w #4,-(a7) ;passe l'attribut A_REPLACE en argument pea.l hello_world(PC) ;passe le message à afficher en argument clr.l -(a7) ;passe x=0 et y=0 en argument (On efface 4 octets, dont 2 pour x et 2 pour y.) move.l DrawStr*4(a5),a0 jsr (a0) ;appelle DrawStr lea.l 10(a7),a7 ;nettoie la pile ;fin du programme principal pea.l 3840 ;taille à copier ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l d4,-(a7) ;source: adresse du bloc alloué pea.l $4c00 ;destination: adresse de l'écran move.l memcpy*4(a5),a0 jsr (a0) ;restaure l'écran addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;libère le bloc alloué nomem: addq.l #4,a7 ;nettoie la pile movem.l (a7)+,d4/a5 ;restaure d4 et a5 rts hello_world: dc.b 'Hello, World!',0
Vous verrez que le texte a disparu. Insérons donc l'attente de l'appui sur une touche. De plus, nous pouvons maintenant nous permettre d'effacer l'écran sans causer trop de dégats. Voici la version finale de notre Hello, World!:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d4/a5,-(a7) ;sauvegarde d4 et a5 move.l $c8,a5 ;place la table des ROM_CALLs en a5 pea.l 3840 ;taille à allouer ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;alloue les 3840 octets move.l a0,d4 ;sauvegarde l'adresse du bloc alloué en d4 tst.l d4 beq nomem ;si le pointeur est nul, quitte le programme move.l #3840,(a7) ;taille à copier ;Cette instruction n'est pas vraiment nécessaire puisque HeapAllocPtr ne modifiera ;pas la valeur qui est déjà sur la pile, mais mieux vaut ne pas se fier. pea.l $4c00 ;source: adresse de l'écran move.l d4,-(a7) ;destination: adresse du bloc alloué move.l memcpy*4(a5),a0 jsr (a0) ;sauvegarde l'écran lea.l 12(a7),a7 ;nettoie la pile ;début du programme principal move.l ScreenClear*4(a5),a0 jsr (a0) ;appelle ScreenClear (ClrScr dans la documentation de TIGCC - c'est la seule ;fonction à ma connaissance dont les noms ne correspondent pas) move.w #4,-(a7) ;passe l'attribut A_REPLACE en argument pea.l hello_world(PC) ;passe le message à afficher en argument clr.l -(a7) ;passe x=0 et y=0 en argument (On efface 4 octets, dont 2 pour x et 2 pour y.) move.l DrawStr*4(a5),a0 jsr (a0) ;appelle DrawStr lea.l 10(a7),a7 ;nettoie la pile move.l ngetchx*4(a5),a0 jsr (a0) ;appelle ngetchx ;fin du programme principal pea.l 3840 ;taille à copier ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l d4,-(a7) ;source: adresse du bloc alloué pea.l $4c00 ;destination: adresse de l'écran move.l memcpy*4(a5),a0 jsr (a0) ;restaure l'écran addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;libère le bloc alloué nomem: addq.l #4,a7 ;nettoie la pile movem.l (a7)+,d4/a5 ;restaure d4 et a5 rts hello_world: dc.b 'Hello, World!',0
Voilà, vous connaissez maintenant les bases de la programmation en _nostub. Avec ceci, vous devriez déjà être capables d'écrire de bons programmes en _nostub. Les 3 sections suivantes vous expliqueront comment remplacer les fonctionnalités des kernels que vous risquez de regretter. Enfin, la dernière section sera dédiée à la compression ExePack.
IV) Comment remplacer les sections BSS et les RAM_CALLs
IV.1) L'allocation dynamique de mémoire
Une des possibilités du mode kernel que l'on regrette souvent est celle de créer des "sections BSS", qui contiennent des données non initialisées qui ne sont pas enregistrées dans le programme, mais automatiquement alloués et désalloués dynamiquement par le kernel. Cela n'est pas possible en _nostub. Heureusement, on n'est pas obligés de mettre toutes ses variables dans le programme lui-même. En effet, il existe des ROM_CALLs qui permettent de facilement allouer et désallouer des blocs dynamiquement soi-mêmes. Je vous conseille de lire la documentation de alloc.h de TIGCCLIB. Pour un exemple de code, vous pouvez regarder le code pour la sauvegarde de l'écran ci-dessus, qui utilise HeapAllocPtr et HeapFreePtr.
IV.2) Les RAM_CALLs et leurs équivalents en _nostub
Une autre fonction souvent regrettée des kernels est donnée par les RAM_CALLs. Paxal (Cyril Pascal) donne sur son site une liste d'équivalents en _nostub des RAM_CALLs. Je tiens aussi à préciser que certains RAM_CALLs sont à éviter puisqu'il existe des ROM_CALLs qui font la même chose plus proprement:
Enfin, je précise que pour la version de la ROM, il existe dans AMS 2 un ROM_CALL donnant la version de la ROM en toutes lettres (par exemple '2.05',0). C'est le ROM_CALL numéro $440. Donc:
move.l $c8,a5 cmp.l #$440,-4(a5) ;vérifie si le ROM_CALL relevant est présent bcs AMSversion_AMS1 ;sinon, utilise une autre routine move.l $440*4(a5),a0 ;obtient l'adresse de la chaîne de caractères
Pour AMS 1, on devra utiliser une autre technique. On pourra par exemple utiliser une table des adresses de la table des ROM_CALLs comme le conseille Paxal. D'ailleurs, si on a juste besoin de savoir si on utilise AMS 1 ou AMS 2, il suffit de mettre:
move.l $c8,a5 cmp.l #1000,-4(a5) ;teste s'il y a au moins 1000 ROM_CALLs bcs AMS1 ;sinon, c'est AMS 1
V) Les librairies statiques - le partage de code totalement transparent pour l'utilisateur
Arrivés ici, vous vous poserez probablement la question suivante: Le _nostub ne supportant pas les librairies dynamiques (on peut s'arranger comme le fait FAT Engine de Thomas Nussbaumer, mais c'est très compliqué), comment dois-je faire si j'ai besoin d'une fonction d'une librairie? D'abord, un grand nombre de fonctions des librairies standard des kernels (userlib, graphlib, filelib) sont déjà dans AMS: il y a des ROM_CALLs correspondants. Par exemple, userlib::idle_loop peut être remplacé par ngetchx, graphlib::clr_scr peut être remplacé par ScreenClear, ... En cas de doûte, la documentation de TIGCC est une bonne référence. Cependant, malheureusement, il existe quelques fonctions importantes, comme par exemple les niveaux de gris, qui ne sont pas dans AMS. Comment faire en ce cas? Heureusement, les librairies dynamiques ne sont pas le seul moyen de partage de code. Il existe un autre type de librairies, les librairies statiques, qui feront l'objectif de cette partie.
V.1) Les avantages des librairies statiques par rapport aux librairies dynamiques
Quelles sont donc les particularités de ce type de librairies? Les librairies statiques sont gérées par l'éditeur de lien lors de la traduction de la source en un programme exécutable. Ceci entraîne de nombreux avantages par rapport aux librairies dynamiques:
Les librairies statiques fournissent donc les avantages des librairies, c'est-à-dire la facilité de réutilisation de code qui évite de tout réécrire à chaque fois, sans les inconvénients des librairies dynamiques.
V.2) Comment utiliser une vraie librairie statique (*.a) avec A68k
Depuis la version 0.92, TIGCC supporte l'utilisation (et même la création) de librairies statiques avec A68k. Et leur usage est très facile: il suffit de faire un bsr ou un jsr vers la fonction que l'on compte utiliser, et le système d'édition de liens de TIGCC s'occupera du reste. Donnons-en un exemple: le Gray Test en assembleur des exemples de TIGCC converti en _nostub:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus plane0 equ __D_plane ;On ne doit pas accéder directement à des variables plane1 equ __L_plane ;commençant par __. movem.l d4/a5,-(a7) ;sauvegarde d4 et a5 move.l $c8,a5 ;place la table des ROM_CALLs en a5 pea.l 3840 ;taille à allouer ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;alloue les 3840 octets move.l a0,d4 ;sauvegarde l'adresse du bloc alloué en d4 tst.l d4 beq nomem ;si le pointeur est nul, quitte le programme move.l #3840,(a7) ;taille à copier ;Cette instruction n'est pas vraiment nécessaire puisque HeapAllocPtr ne modifiera ;pas la valeur qui est déjà sur la pile, mais mieux vaut ne pas se fier. pea.l $4c00 ;source: adresse de l'écran move.l d4,-(a7) ;destination: adresse du bloc alloué move.l memcpy*4(a5),a0 jsr (a0) ;sauvegarde l'écran lea.l 12(a7),a7 ;nettoie la pile ;début du programme principal move.l ScreenClear*4(a5),a0 jsr (a0) ;efface l'écran principal (premier plan) bsr GrayOn ;active les niveaux de gris move.l #$ef007f,-(a7) move.l plane1(PC),-(a7) move.l PortSet*4(a5),a0 jsr (a0) ;active le deuxième plan addq.l #8,a7 move.l ScreenClear*4(a5),a0 jsr (a0) ;efface le deuxième plan move.l #20*$1000000+20*$10000+40*$100+40,-(a7) move.w #1,-(a7) move.l ScrRect*4(a5),a0 pea.l (a0) pea.l 6(a7) move.l ScrRectFill*4(a5),a0 jsr (a0) ;dessine un rectangle rempli en gris clair move.l #80*$1000000+20*$10000+100*$100+40,10(a7) move.l ScrRectFill*4(a5),a0 jsr (a0) ;dessine le plan clair d'un rectangle rempli en noir lea.l 14(a7),a7 move.l #$ef007f,-(a7) move.l plane0(PC),-(a7) move.l PortSet*4(a5),a0 jsr (a0) ;active le premier plan addq.l #8,a7 move.l #50*$1000000+20*$10000+70*$100+40,-(a7) move.w #1,-(a7) move.l ScrRect*4(a5),a0 pea.l (a0) pea.l 6(a7) move.l ScrRectFill*4(a5),a0 jsr (a0) ;dessine un rectangle rempli en gris foncé move.l #80*$1000000+20*$10000+100*$100+40,10(a7) move.l ScrRectFill*4(a5),a0 jsr (a0) ;dessine le plan foncé d'un rectangle rempli en noir lea.l 14(a7),a7 move.l ngetchx*4(a5),a0 jsr (a0) ;attend l'appui d'une touche bsr GrayOff ;fin du programme principal pea.l 3840 ;taille à copier ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l d4,-(a7) ;source: adresse du bloc alloué pea.l $4c00 ;destination: adresse de l'écran move.l memcpy*4(a5),a0 jsr (a0) ;restaure l'écran addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;libère le bloc alloué nomem: addq.l #4,a7 ;nettoie la pile movem.l (a7)+,d4/a5 ;restaure d4 et a5 rts
Voilà. Après exécution de ce programme, la calculatrice affichera 3 rectangles remplis en niveaux de gris: un rectangle gris clair, un rectangle gris foncé et un rectangle noir. On a donc réussi à écrire un programme en niveaux de gris en assembleur _nostub, ce qui est souvent l'obstacle principal pour les programmeurs qui visent à programmer de cette manière. N'hésitez pas à réutiliser du code de cet exemple, il est là pour vous aider.
D'ailleurs, dans cet exemple, nous avons utilisé la librairie statique TIGCCLIB, avec laquelle votre projet sera linké automatiquement. Si vous désirez utiliser les fonctions d'une autre librairie statique (comme par exemple ExtGraph), vous devrez d'abord la rajouter à votre projet: Allez dans Project / Add Files... et choisissez extgraph.a, puis OK. Vous pouvez maintenant utiliser les fonctions de ExtGraph de la même manière que celles de TIGCCLIB.
V.3) Une autre solution pour le partage statique de code
Les "vraies" librairies statiques (les fichiers *.a) ne sont pas le seul moyen de partager du code de manière statique. Il existe une autre solution qui fonctionne comme les librairies statiques, à un désavantage près: la fonction à utiliser est recompilée ou réassemblée à chaque fois que l'on compile un projet qui l'utilise. Il s'agit de tout bêtement ajouter la source contenant la fonction au projet. Mais voyons tout de suite un exemple: la fonction InputStr décrite dans la FAQ de TIGCC. Commencez par ajouter à votre projet, en plus du fichier assembleur A68k, un deuxième fichier en C. Peu importent les options choisies, on effacera de toute façon le cartouche créé automatiquement par TIGCC IDE. ATTENTION: Le nom doit être différent. En effet, l'extension diffère, mais si vous donnez le même nom avec juste les extensions différentes, il y aura collision pour les fichiers objets créés. Le contenu du fichier C est le suivant:
#define NO_EXIT_SUPPORT #include <tigcclib.h> /* fonction InputStr tirée de la FAQ de TIGCC - merci à Zeljko Juric */ void InputStr (char *buffer, short maxlen) { SCR_STATE ss; short key, i = 0; buffer[0] = 0; SaveScrState (&ss); do { MoveTo (ss.CurX, ss.CurY); printf ("%s_ ", buffer); /* 2 espaces sont nécessaire pour la fonte 4 x 6. */ key = ngetchx (); if (key >= ' ' && key <= '~' && i < maxlen) buffer[i++] = key; if (key == KEY_BACKSPACE && i) i--; buffer[i] = 0; } while (key != KEY_ENTER); }
Passons maintenant à notre programme principal, en assembleur A68k:
include "OS.h" xdef _ti89 xdef _ti92plus xdef _nostub ;On n'est pas obligés de réexporter _nostub ici. En effet, TIGCC le fera dans la partie en C. movem.l d4/a5,-(a7) ;sauvegarde d4 et a5 move.l $c8,a5 ;place la table des ROM_CALLs en a5 pea.l 3840 ;taille à allouer ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;alloue les 3840 octets move.l a0,d4 ;sauvegarde l'adresse du bloc alloué en d4 tst.l d4 beq nomem ;si le pointeur est nul, quitte le programme move.l #3840,(a7) ;taille à copier ;Cette instruction n'est pas vraiment nécessaire puisque HeapAllocPtr ne modifiera ;pas la valeur qui est déjà sur la pile, mais mieux vaut ne pas se fier. pea.l $4c00 ;source: adresse de l'écran move.l d4,-(a7) ;destination: adresse du bloc alloué move.l memcpy*4(a5),a0 jsr (a0) ;sauvegarde l'écran lea.l 12(a7),a7 ;nettoie la pile ;début du programme principal bsr clrscr ;efface l'écran et met à 0 les coordonnées pour printf (appelle une ;fonction de TIGCCLIB) move.w #100,-(a7) ;passe maxlen en argument pea.l buffer(PC) ;passe buffer en argument bsr InputStr ;appelle la fonction en C addq.l #6,a7 ;nettoie la pile ;fin du programme principal pea.l 3840 ;taille à copier ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l d4,-(a7) ;source: adresse du bloc alloué pea.l $4c00 ;destination: adresse de l'écran move.l memcpy*4(a5),a0 jsr (a0) ;restaure l'écran addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;libère le bloc alloué nomem: addq.l #4,a7 ;nettoie la pile movem.l (a7)+,d4/a5 ;restaure d4 et a5 rts buffer: ds.b 101
Cette technique est en général à déconseiller par rapport aux vraies librairies statiques parce qu'on perd du temps à recompiler la fonction à chaque fois, mais elle peut être pratique dans certains cas, comme ici où la fonction ne se trouve pas dans une librairie statique.
D'ailleurs, encore une fois, je n'ai pas choisi l'exemple au hasard. En effet, comme pour les niveaux de gris, une routine de type InputStr facile d'usage fait partie des rares fonctions utiles qu'on ne trouve pas dans AMS. À l'aide des 2 derniers exemples dont je vous invite à réutiliser le code, nous avons surmonté les 2 plus grands obstacles qui se posent à un débutant en programmation en assembleur _nostub.
VI) Fichiers de données externes
Il peut arriver parfois qu'on se voit obligés à utiliser un fichier externe pour les données, pour 2 raisons:
Heureusement, il y a des ROM_CALLs qui rendent tout ceci relativement facile. Je vous renvoie
à la documentation de vat.h de TIGCC. Je vais me contenter ici de donner un
exemple simple, qui affiche à l'écran le contenu de la chaîne de caractères
datafile dans le répertoire main. Commençons donc par créer cette
chaîne de caractères. Rentrez ceci sur votre calculatrice:
"Hello, World!"->main\datafile
(-> représente évidemment le caractère obtenu à l'aide de la touche [STO->].)
Passons maintenant à notre programme exemple:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d4/a5,-(a7) ;sauvegarde d4 et a5 move.l $c8,a5 ;place la table des ROM_CALLs en a5 pea.l 3840 ;taille à allouer ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;alloue les 3840 octets move.l a0,d4 ;sauvegarde l'adresse du bloc alloué en d4 tst.l d4 beq nomem ;si le pointeur est nul, quitte le programme move.l #3840,(a7) ;taille à copier ;Cette instruction n'est pas vraiment nécessaire puisque HeapAllocPtr ne modifiera ;pas la valeur qui est déjà sur la pile, mais mieux vaut ne pas se fier. pea.l $4c00 ;source: adresse de l'écran move.l d4,-(a7) ;destination: adresse du bloc alloué move.l memcpy*4(a5),a0 jsr (a0) ;sauvegarde l'écran lea.l 12(a7),a7 ;nettoie la pile ;début du programme principal move.l ScreenClear*4(a5),a0 jsr (a0) ;efface l'écran move.w #4,-(a7) ;cherche dans main pea.l sym(PC) ;symbole à chercher move.l SymFindPtr*4(a5),a0 jsr (a0) ;cherche la variable addq.l #4,a7 move.w 12(a0),(a7) ;Le handle du symbole se trouve à l'offset 12 de la structure SYM_ENTRY. move.l HeapDeref*4(a5),a0 jsr (a0) ;déréférence le handle move.w #4,(a7) ;passe l'attribut A_REPLACE en argument pea.l 3(a0) ;saute la taille de la variable et l'octet nul au début de la chaîne de caractères clr.l -(a7) ;passe x=0 et y=0 en argument (On efface 4 octets, dont 2 pour x et 2 pour y.) move.l DrawStr*4(a5),a0 jsr (a0) ;affiche le contenu de la chaîne de caractères lea.l 10(a7),a7 ;nettoie la pile move.l ngetchx*4(a5),a0 jsr (a0) ;attend l'appui d'une touche ;fin du programme principal pea.l 3840 ;taille à copier ;(même chose que move.l #3840,-(a7), mais plus optimisé) move.l d4,-(a7) ;source: adresse du bloc alloué pea.l $4c00 ;destination: adresse de l'écran move.l memcpy*4(a5),a0 jsr (a0) ;restaure l'écran addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;libère le bloc alloué nomem: addq.l #4,a7 ;nettoie la pile movem.l (a7)+,d4/a5 ;restaure d4 et a5 rts dc.b 0,'datafile' sym: dc.b 0 ;Le format des symboles est particulier: Il faut mettre un octet nul au début et à la fin, ;et le pointeur doit pointer sur le 0 final, pas sur le début.
Quelques remarques pour finir:
VII) La compression ExePack
Arrivés ici, il vous reste probablement un dernier problème: Que faire si votre programme dépasse les 24 KO (8 KO pour AMS 2.00-2.03)? Heureusement, il n'est pas très compliqué de créer un lanceur qui fonctionne même sur HW2 AMS 2 sans patches (comme le HW2Patch de Julien Muchembled) ou programmes anti-protection résidents en mémoire (comme mon HW2 AMS 2 TSR support (h220xTSR)) quels qu'ils soient. Cependant, il existe une solution encore plus simple et plus pratique: TIGCC peut automatiquement compresser votre programme pour vous. Cela a non seulement l'avantage que c'est au décompresseur automatiquement généré de s'occuper de dépasser la limite des 24 (ou 8) KO, mais permet aussi de réduire la taille du programme tant qu'on y est. Cette compression est appelée ExePack.
Commencez par choisir un fichier exemple. Vous pouvez utiliser le programme vide de la partie III.2:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus rts
Maintenant, compressez-le:
Je vous conseille de compresser avec ExePack tout programme de taille supérieure à 8 KO. Pour des programmes de taille inférieure à 8 KO, ce n'est pas vraiment la peine, et je vous déconseille donc d'utiliser ExePack dans ce cas.
VIII) Conclusion
Vous devrez maintenant savoir programmer en _nostub et faire en ce mode tout ce que vous pourriez faire dans des programmes pour kernel, voire même plus. Comme vous l'avez pu voir, le mode _nostub n'est pas réservé aux programmes en C! J'espère que ce tutorial vous a aidé et qu'il vous inspirera à programmer en _nostub comme je le fais depuis plus d'un an.
Kevin Kofler
kevin.kofler@chello.at
http://francais.kevinkofler.cjb.net