D'un Z qui veut dire Z80
Par Jean-Seb le lundi 21 mai 2012, 22:48 - Coding - Lien permanent
Les outils GNU sont d'une richesse insoupçonnée. J'en veux pour preuve
l'amateur de vieux micros qui va trouver avec eux tous les outils nécessaires à
un développement serein.
Notez que je prends pour exemple le MSX, mais les principes sont aisément
transposables.
Avertissement
Lorsque j'ai écrit ce billet, je me suis posé la question de son contenu. En effet, il reprenait mes notes, et donc mes différents essais.
Fallait-il tout reprendre afin de garder le cheminement original ? Ou risquais-je de perdre le lecteur en route ? Un billet synthétique qui va droit à la solution, ou des techniques inutiles ici, mais qui pourront reservir ?
J'ai finalement décidé que les gens qui lisent ceci sont des hobbystes qui savent ce qu'ils risquent. J'ai coupé les branches vraiment mortes, et gardé le reste.
Les parties que les gens pressés peuvent passer sont indiquées, mais j'ai la faiblesse de croire qu'il vaut mieux lire l'intégralité.
Quel assembleur pour votre Z80 ?
Un précédent billet a détaillé les principaux assembleurs disponibles pour le Z80.
Il en sortait qu'il n'y avait pas grand chose à en tirer. Développés par des gens plus ou moins inspirés , ils peinent à convaincre.
De trop rares exceptions tirent leur épingle du jeu, comme WLA DX, dont le développement est arrêté.
Vous pouvez charitablement oublier le reste.
Heureusement, il reste une piste que je n'avais pas exploré la première fois. Elle consiste à utiliser l'assembleur GNU et lui adjoindre un backend Z80.
L'assembleur GNU et le Z80
L'assembleur GNU est une valeur sûre. Il est développé par des gens compétents, et ne vous lachera pas en rase campagne. Et il ne coûte rien, alors pourquoi se priver ?
L'assembleur GNU (et son linker) sont inscrits dans le projet binutils.
L'assembleur est remarquablement générique. Il compilera sans problème du code pour pratiquement tout les processeurs existants, y compris bien sûr le Z80.
L'avantage de la généricité est qu'entre les différentes plate-formes matérielles, le fonctionnement général des outils ne change pas. À part bien sûr quelques options supplémentaires pour supporter les spécificités matérielles.
Dans le cas du Z80, il s'agira surtout de la déclaration de bloc de données et des opcodes non documentés….
L'assembleur supporte également le R800. Le R800 est un descendant du Z80, né trop tard dans un monde trop vieux.
Vous trouverez ici la liste complète des spécificités Z80 concernant l'assembleur.
La seule difficulté est d'obtenir le support Z80 dans binutils, ce qui n'est pas le cas par défaut.
Un simple backend (étage de sortie en bon français) produisant du Z80 suffit. Par chance, GNU fournit ce backend dans le tronc officiel.
Compilation du back end Z80 sous Arch-Linux
Note : pour les utilisateurs de MS-Windows, j'ai compilé une version adaptée au Z80 , à l'aide de mingw. Vous la trouverez sur le site MSX Village. Pensez tout de même à passer à Linux un de ces jours.
Récupérer les sources avec ABS
NOTE: ceci ne concerne que les utilisateurs Arch Linux. Autrement, vous pouvez simplement charger les sources depuis le site GNU.
Pour Arch-Linux, le mieux est de partir du build ABS : (pour moi : core/binutils 2.22-4 (base-devel) )
cp -r /var/abs/core/binutils/ .
Vérifiez dans le PKGBUILD que vous avez la même version.
pkgver=2.22 pkgrel=4 date=20111227
Dans le cas contraire, lancez la commande abs pour mettre à
niveau l'arbre abs. À noter que la correspondance de version entre le binutils
Z80 et celui de votre version est facultative, et ne vise qu'à préserver une
certaine cohérence sur votre système.
Dépacker les sources récupérées:
makepkg --nocheck --nobuild
nocheckest à utiliser en connaissance de cause. Il empêche la vérification des dépendances du paquet. Dans le cas présent, cela évite d'installer dejagnu, qui est un framework pour des tests unitaires. Ici, il ne sert à rien.
nobuildempêche la construction du paquet. Les paramètres par défaut n'ont pas le support Z80, et je ne souhaitais pas faire un paquet Arch des binutils obtenus. Si vous êtes courageux, vous pouvez personnaliser le PKGBUILD pour vous éviter une copie à la main des binaires obtenus, et garder un suivi de version pour les outils.
Pourquoi n'ai-je pas fait de paquet pour la distribution ?
Les fichiers sont indépendants les uns des autres, et il vaut mieux ne pas tout mélanger avec l'installation «officielle» de binutils.
Un simple ajustement du $PATH suffira.
Compilation des sources
Quelques mots sur BFD
NOTE: pour les curieux.
BFD est nécessaire pour construire la majorité des utilitaires de binutils.
C'est une bibliothéque qui s'occupe de la partie bas niveau. Elle crée une abstraction vers le hardware pour utiliser un objet commun à toutes les plateformes. BFD ne fait pas qu'envoyer de l'opcode. Fini les problèmes avec l'endianess, la taille d'un mot qui change selon les cpu. Il s'utilise même pour le debug sur les coredumps.
Un listing du répertoire source achève de convaincre le dubitatif. Nous y trouvons une impressionnante collection de plateformes supportées, ainsi que les formats de sortie les plus divers : elf32/64 bien sûr, mais aussi coff, pe, et de façon générale, tout l'exotisme du silicium.
Prenons le fichier «cpu-z80.c». Une rapide inspection confirme que le z80 et son avatar r800 sont supportés. On a les modes «strict» et «full» pour le z80, qui correspondent surement au support des opcodes documentés ou non.
GAS : l'assembleur GNU.
Alors là, mauvaise nouvelle pour le génération des targets.
La doc le dit elle-même: «There is no convenient way to generate a list of all available hosts» , ce qui se traduit par «débrouillez-vous».
Heureusement, l'aide du configure donne quelques indications, notamment sur le fait que les noms des hosts et des cibles sont stockés dans le script «config.sub».
Nous allons donc jouer un peu avec celui-ci.
Sur ma machine, il est installé ici:
/usr/share/libtool/config/config.sub
Le nommage utilise un schéma tripartite de type:
ARCHITECTURE-VENDOR-OS
Heureusement, des alias sont prévus, on peut les découvrir a l'entrée
$basic_machine.
Comme je l'espérais très fort, il existe un alias z80. En explorant un peu, nous trouvons aussi m68000 (m68k fonctionne également). Je vous laisse chercher pour les autres architectures.
Comme nous le supposons également, avec ces deux choix «VENDOR» et «OS» prennent une valeur générique (intitulée sobrement «unknown» ou «none»).
Ex: ~/abs/binutils/src/binutils/gas$ sh
/usr/share/libtool/config/config.sub z80-unknown-none
(même topo pour le 68000: m68k-unknown-none )
Bon. En réalité, la bonne cible pour z80 est «z80-coff» (le triplet ci-dessus ne fonctionnant pas, il m'a fallu tatonner un peu).
Pourquoi pas «z80-elf» ? Vous auriez pu tout aussi bien le choisir. Le format des objets n'a aucune importance, car il y a peu de chance pour tomber sur une plateforme z80 comprenant elf ou coff.
Il faudra donc ruser pour faire du fichier objet un fichier exécutable sur la plateforme cible.
En résumé, les options à passer à configure:
--prefix=$HOME/binutils --exec-prefix=$HOME/binutils --target=z80-coff
Ces options permettent d'éviter de polluer votre filesystem, et de faire l'installation dans votre home (pas besoin d'être root).
L'installation se fait avec le classique «make install» après la compilation.
Après le configure, lançons la compilation:
«make -jn+1» , avec 'n' représentant le nombre de threads sur votre machines (cores*ht). Par exemple «make -j5» sur un core i5 (4 cores + 1). Ceci parallélise la compilation et accélère grandement son déroulement.
Vous pouvez avoir une erreur, dûe à un flag un peu exigeant sur un fichier peu utilisé (et donc avec moins de chance d'être corrigé immédiatement dans les sources).
Si vous êtes dans ce cas (make qui s'arrête en erreur), relancez make pour repérer le fautif (un warning qui se transforme en erreur)
Je suis évidemment tombé sur ce cas de figure, sinon je n'insisterai pas aussi lourdement.
Dans mon cas, il m'a fallu exécuter dans le répertoire «gas»:
gcc -DHAVE_CONFIG_H -I. -I. -I. -I../bfd -I./config -I./../include -I./.. -I./../bfd -DLOCALEDIR="\"votre chemin à vous le locale dir qu'il faudra adapter"" -W -Wall -Wstrict-prototypes -Wmissing-prototypes -Wshadow -g -O2 -MT tc-z80.o -MD -MP -MF .deps/tc-z80.Tpo -c -o tc-z80.o `test -f 'config/tc-z80.c' || echo './'`config/tc-z80.c
(par rapport à la compilation avec make, nous enlevons simplement le flag
-Werror)
Ensuite, nous retournons à la racine de binutils, et nous relançons
make
Une fois la compilation terminée et les binutils en ligne, n'oubliez pas d'ajuster le $PATH sur le répertoire contenant les programmes z80-coff-*
Exécution des objets
Nous pouvons maintenant assembler notre programme de test (cf le listing ci-dessous).
~/test/z80-coff-as -als hello_world.asm
GAS LISTING hello_world.asm page 1
1 0000 FE db $fe
2 0001 00C0 1AC0 dw debut,fin,debut
2 00C0
3
4 0007 0000 0000 ORG $C000
4 0000 0000
4 0000 0000
4 0000 0000
4 0000 0000
5
6 debut:
7 c000 210D C0 LD HL,LABEL
8 c003 7E AFF: LD A,(HL)
9 c004 23 INC HL
10 c005 A7 AND A
11 c006 C8 RET Z
12 c007 CDA2 00 CALL $A2 ; HFDA4 ;CHPUT
13 c00a 18F7 JR AFF
14 c00c C9 RET
15 c00d 5465 7374 LABEL: DEFB "Test chput",13,10,0
15 2063 6870
15 7574 0D0A
15 00
16 fin:
17 END
GAS LISTING hello_world.asm page 2
DEFINED SYMBOLS
*ABS*:0000000000000000 fake
hello_world.asm:6 .text:000000000000c000 debut
hello_world.asm:16 .text:000000000000c01a fin
hello_world.asm:15 .text:000000000000c00d LABEL
hello_world.asm:8 .text:000000000000c003 AFF
NO UNDEFINED SYMBOLS
Nous n'en avons pas tout à fait fini, mais au moins, le code z80 se compile correctement.
Dumpons maintenant l'objet obtenu.
~/test$ hexdump -C hello_world.out
00000000 5a 80 03 00 00 00 00 00 e6 c0 00 00 0c 00 00 00 |Z...............| 00000010 00 00 04 11 2e 74 65 78 74 00 00 00 00 00 00 00 |.....text.......| 00000020 00 00 00 00 1a c0 00 00 8c 00 00 00 a6 c0 00 00 |................| 00000030 00 00 00 00 04 00 00 00 20 00 00 00 2e 64 61 74 |........ ....dat| 00000040 61 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |a...............| 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000060 40 00 00 00 2e 62 73 73 00 00 00 00 00 00 00 00 |@....bss........| 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000080 00 00 00 00 00 00 00 00 80 00 00 00 fe 00 c0 1a |................| 00000090 c0 00 c0 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 0000c080 00 00 00 00 00 00 00 00 00 00 00 00 21 0d c0 7e |............!..~| 0000c090 23 a7 c8 cd a2 00 18 f7 c9 54 65 73 74 20 63 68 |#........Test ch| 0000c0a0 70 75 74 0d 0a 00 01 00 00 00 06 00 00 00 00 00 |put.............| 0000c0b0 00 00 01 00 53 43 03 00 00 00 06 00 00 00 00 00 |....SC..........| 0000c0c0 00 00 01 00 53 43 05 00 00 00 06 00 00 00 00 00 |....SC..........| 0000c0d0 00 00 01 00 53 43 01 c0 00 00 06 00 00 00 00 00 |....SC..........| 0000c0e0 00 00 01 00 53 43 2e 66 69 6c 65 00 00 00 00 00 |....SC.file.....| 0000c0f0 00 00 fe ff 00 00 67 01 66 61 6b 65 00 00 00 00 |......g.fake....| 0000c100 00 00 00 00 00 00 00 00 00 00 64 65 62 75 74 00 |..........debut.| 0000c110 00 00 00 c0 00 00 01 00 00 00 06 00 66 69 6e 00 |............fin.| 0000c120 00 00 00 00 1a c0 00 00 01 00 00 00 06 00 4c 41 |..............LA| 0000c130 42 45 4c 00 00 00 0d c0 00 00 01 00 00 00 06 00 |BEL.............| 0000c140 41 46 46 00 00 00 00 00 03 c0 00 00 01 00 00 00 |AFF.............| 0000c150 06 00 2e 74 65 78 74 00 00 00 00 00 00 00 01 00 |...text.........| 0000c160 00 00 03 01 1a c0 00 00 04 00 00 00 00 00 00 00 |................| 0000c170 00 00 00 00 00 00 2e 64 61 74 61 00 00 00 00 00 |.......data.....| 0000c180 00 00 02 00 00 00 03 01 00 00 00 00 00 00 00 00 |................| 0000c190 00 00 00 00 00 00 00 00 00 00 2e 62 73 73 00 00 |...........bss..| 0000c1a0 00 00 00 00 00 00 03 00 00 00 03 01 00 00 00 00 |................| 0000c1b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 |................| 0000c1c0 00 00 |..| 0000c1c2
Nous avons assemblé au format COFF.
Il est inconnu du MSX, qui bien entendu n'est pas fait pour ces subtilités modernes.
Nous voulons nous débarasser des headers et récupérer uniquement l'assemblage du fichier source. En d'autres termes, nous voulons le contenu de la section .TEXT.
Le stub MSX est contenu dans le source, et fera office de header.
Pour isoler une section, nous avons deux possibilités:
- Avec objcopy, nous pouvons nous débarasser des sections COFF inutiles:
~/test$ objcopy -O binary hello_world.out hello_world.bin
~/test$ hexdump -C hello_world.bin
00000000 fe 00 c0 1b c0 00 c0 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 0000c000 21 0d c0 7e 23 a7 c8 cd a2 00 18 f7 c9 48 65 6c |!..~#........Hel| 0000c010 6c 6f 20 57 6f 72 6c 64 0d 0a 00 |lo World...| 0000c01b
- Une autre possibilité est d'utiliser le linker binutils z80.
~/test$ z80-coff-ld.bfd hello_world.out -o hello_world.bin
Nous constatons que ces deux possibilités souffrent du même problème :le linker utilise comme adresse d'origine 0x100 (souvenir des .com).
~/test$ hexdump hello_world.bin -C
00000000 fe 00 c1 1b c1 00 c1 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 0000c000 21 0d c1 7e 23 a7 c8 cd a2 00 18 f7 c9 48 65 6c |!..~#........Hel| 0000c010 6c 6f 20 57 6f 72 6c 64 0d 0a 00 |lo World...|
Nous voyons bien au début: 0xc100 au lieu de l'attendu: 0xc000
Le linker peut, grâce à l'utilisation d'une configuration ld, faire beaucoup mieux que cela.
Pour l'instant, nous nous cantonnerons à utiliser objcopy. L'utilisation du linker, beaucoup plus pratique, sera abordée ensuite.
Solution sans script ld
Note : les allergiques aux commandes Unix peuvent passer directement au script ld.
Revenons au fichier produit par objcopy.
Il est presque parfait, mais nout avons toujours du padding entre l'entête (les 7 premiers octets) et la section de code, qui commence à 0xc000.
Nous voudrions plutôt cela:
00000000 fe 00 c0 1b c0 00 c0 21 0d c0 7e 23 a7 c8 cd a2 |.......!..~#....| 00000010 00 18 f7 c9 48 65 6c 6c 6f 20 57 6f 72 6c 64 0d |....Hello World.| 00000020 0a 00 |..|
Avec quelques commandes shell, nous pouvons arriver facilement au résultat ci-dessus.
- Créer avec le fichier de sortie hello_world.exe en copiant les 7 premiers octets, qui font office de stub :
~/test$ dd if=hello_world.bin of=hello_wo.exe count=7 bs=1
- ensuite, nous avons deux possibilités pour trouver l'offset à passer pour arriver sur le code.
1) lire la valeur de l'offset du code dans le fichier source:
skip=$(awk 'BEGIN { FS="$" }; /ORG/ { print strtonum("0x"$2) }'
hello_world.asm)
2) ou bien, trouver cette valeur en lisant le header du fichier binaire:
skip=$(od -d -t d1 hello_world.bin --skip=1 -N 2)
- la sortie se fait en décimal (-d)
- le type de la sortie est défini comme char (-t d1)
- on lit deux éléments (-N 2)
- on passe le premier octet (--skip=1) pour arriver sur l'adresse du symbole «debut».
1 0000 FE db $fe 2 0001 00C0 1AC0 00C0 dw debut,fin,debut
Une fois la valeur skip récupérée, nous ajoutons le code au header :
dd if=hello_world.bin of=hello_wo.exe skip="$skip" bs=1 oflag=append
conv=notrunc
Et voila :
~/test$ hexdump -C hello_wo.exe
00000000 fe 00 c0 1b c0 00 c0 21 0d c0 7e 23 a7 c8 cd a2 |.......!..~#....| 00000010 00 18 f7 c9 48 65 6c 6c 6f 20 57 6f 72 6c 64 0d |....Hello World.| 00000020 0a 00 |..|
Nous pouvons en faire un script pour aller plus vite la prochaine fois. Ce script est donné à titre d'illustration et ne permet que de compiler un seul fichier source à la fois.
J'ai rapidement laissé tomber, pour revenir à l'utilisation du linker que nous allons voir dans la prochaine section.
~/test$ cat compile.sh
#!/bin/bash
#paramètre: source.asm "options_gas"
#Attention, les paramètres doivent être entre guillemets si il y en a
# plusieurs séparées par des espaces.
#exemple: ./compile.sh foo.asm "-als -Wup"
#à changer selon l'emplacement de binutils
install_root="$HOME"/binutils
#pas besoin de toucher au reste, normalement
src=$1
if [ "$src" == "" ]; then
echo "Usage:"
echo " $0 source.asm"
exit
fi
options_gas="$2"
install_bins="$install_root"/z80-coff/bin/
GAS="$install_bins"as
OCP="$install_bins"objcopy
base="$(basename $src .asm)"
#si besoin est, tronque la destination en 8.3
dst="$(echo $base | cut -c1-8 )".exe
log="$base".log
out_gas="$base".out
out_ocp="$base".bin
echo compilation de "$src" " -> $out_gas"
"$GAS" "$src" $options_gas -o "$out_gas" > "$log"
if [ $? -ne 0 ]; then exit; fi
echo conversion de "$out_gas" " -> $out_ocp"
"$OCP" -O binary "$out_gas" "$out_ocp"
if [ $? -ne 0 ]; then exit; fi
echo création exécutable avec "$out_ocp" " -> $dst"
dd if="$out_ocp" of="$dst" count=7 bs=1 2>/dev/null
skip=$(od -d -t d1 "$out_ocp" --skip=1 -N 2 | awk 'NR==1 { print $2 }')
dd if="$out_ocp" of="$dst" skip="$skip" bs=1 oflag=append conv=notrunc 2>/dev/null
Pour tester, j'utilise OpenMSX qui est mon émulateur MSX favori.
openmsx -machine Philips_NMS_8255 -diska /home/jseb/test/
Une fois l'invite du basic obtenue dans l'émulateur, entrez les deux commandes:
files bload "a.msx",r
Mais tout ceci est bien compliqué…
La meilleure solution pour un exécutable: le script ld
Nous allons créer un exécutable en utilisant le linker.
Voici comment procéder:
- Enlever la directive «.ORG» du source assembleur
- Ajouter des sections (plus propre pour le linker)
- Compiler :
z80-coff-as -als foo.asm -o foo.out
1 .section .stub
2 0000 FE db $fe
3 0001 0000 1B00 dw debut,fin,debut
3 0000
4
5 ; ne PAS utiliser la directive .ORG
6 ; .ORG $C000
7
8 .section .mycode
9 ; par défaut, la section de code s'appelle .text
10 debut:
11 0000 210D 00 LD HL,LABEL
12 0003 7E AFF: LD A,(HL)
13 0004 23 INC HL
14 0005 A7 AND A
15 0006 C8 RET Z
16 0007 CDA2 00 CALL $A2 ; HFDA4 ;CHPUT
17 ; call outside
18 000a 18F7 JR AFF
19 000c C9 RET
20 000d 4865 6C6C LABEL: DEFB "Hello World",13,10,0
20 6F20 576F
20 726C 640D
20 0A00
21 fin:
22 END
DEFINED SYMBOLS
*ABS*:0000000000000000 fake
foo.asm:10 .mycode:0000000000000000 debut
foo.asm:21 .mycode:000000000000001b fin
foo.asm:20 .mycode:000000000000000d LABEL
foo.asm:12 .mycode:0000000000000003 AFF
Notez la section .stub dans le source ci-dessus.
Vérification du contenu des sections: z80-coff-objdump -s
foo.out@
foo.out: file format coff-z80 Contents of section .stub: 0000 fe00001b 000000 ....... Contents of section .mycode: 0000 210d007e 23a7c8cd a20018f7 c948656c !..~#........Hel 0010 6c6f2057 6f726c64 0d0a00 lo World...
Nous voyons pour la section «.stub» que l'origine est à 0x0 pour l'instant. C'est au linker qu'il incombera la tâche de recalculer les adresses.
Nous pouvons linker, pour l'instant sans utilisation d'un script ld.
- L'origine sera donc à 0x100 (origine par défaut pour ce linker)
- Option -M : en plus du link, affiche le mapping mémoire:
z80-coff-ld foo.out -o foo.exe -M
Memory Configuration
Name Origin Length Attributes
*default* 0x0000000000000000 0xffffffffffffffff
Linker script and memory map
LOAD foo.out
0x0000000000000100 . = 0x100
0x0000000000000100 __Ltext = .
.text 0x0000000000000100 0x0
*(.text)
.text 0x0000000000000100 0x0 foo.out
*(text)
0x0000000000000100 __Htext = .
.data 0x0000000000000100 0x0
0x0000000000000100 __Ldata = .
*(.data)
.data 0x0000000000000100 0x0 foo.out
*(data)
0x0000000000000100 __Hdata = .
.bss 0x0000000000000100 0x0
0x0000000000000100 __Lbss = .
*(.bss)
.bss 0x0000000000000100 0x0 foo.out
*(bss)
0x0000000000000100 __Hbss = .
OUTPUT(foo.exe binary)
.stub 0x0000000000000100 0x7
.stub 0x0000000000000100 0x7 foo.out
.mycode 0x0000000000000107 0x1b
.mycode 0x0000000000000107 0x1b foo.out
On voit (dans OUTPUT) que les seules sections trouvées furent «.stub» et «.mycode».
ld a donc linké ces sections à la suite, en l'absence d'autres instructions (c'est ce qu'on appelle un «script de link implicite»).
Pour linker correctement, il faut utiliser ce script ld , que nous spécifierons à «ld» avec l'option «-T».
OUTPUT_FORMAT(binary)
SECTIONS
{
.stub 0xC000 - 7 : { *(.stub) }
.code : { *(.mycode) }
}
z80-coff-ld foo.out -o foo.exe -T nom_du_script_ld
C'est fini
Par manque de temps (et un aussi un peu de motivation), je ne pense pas revenir sur la programmation d'ordinosaures.
J'ai quand même décidé de publier ces notes, en espérant que quelqu'un pourra en faire bon usage.
J'ai encore quelques autres notes du même acabit dans mes tiroirs. Je les
publierai peut-être un jour, mais ne retenez pas votre respiration. 