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

  • nocheck est à 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.
  • nobuild empê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. :)