Après cette pause estivale, reprenons là où l'on était restés. Dans l'article précédent, je parlais du langage machine, une suite de signaux provoquant l'activité d'un processeur selon des directives précises.
Un inconvénient du langage machine, c'est qu'il est peu pratique à manipuler. Par exemple, sur un Z80, charger l'accumulateur (une mémoire spécifique interne au processeur) avec la valeur 1, je dois écrire : 00111110 00000001
en binaire, ou encore 3E 01
en système hexadécimal.
Écrire de cette manière n'est pas simple, prend beaucoup de temps avec un grand risque d'erreurs. Relire est encore pire. Programmez une machine de cette manière et vous ressentirez a priori rapidement le besoin de manipuler des éléments plus faciles à comprendre pour un Humain.
Et c'est ainsi que du langage machine on passe au langage d'assemblage, ou, par abus de langage, à l'assembleur.
L'assembleur
La première chose à savoir est que l'assembleur est un programme. Mais par extension, le langage qui est utilisé par ce programme est aussi appelé assembleur. Dans la suite, le mot servira principalement à désigner le langage en lui-même.
L'assembleur donc, est un langage à la syntaxe et à la grammaire extrêmement proche du langage machine. Chaque instruction en assembleur est directement traduisible dans sa version en langage machine et chaque instruction en langage machine a son écriture en assembleur.
En assembleur Z80, pour continuer sur le même exemple, la suite de signaux 00111110 00000001
qui charge 1 dans l'accumulateur s'écrit LD A, 1
en assembleur. C'est toujours un peu cryptique, mais ça se lit bien plus facilement. LD pour « load », charger en anglais, est suivi de deux paramètres : la destination A
, qui désigne l'accumulateur, et la valeur 1
.
Je précise bien assembleur Z80 car, tout comme le langage machine diffère d'un processeur à l'autre, l'assembleur, qui est une transcription lisible de ce langage machine est lui aussi spécifique à chaque processeur.
Ça reste ardu non ?
Les commandes du processeur Z80 couvrent le chargement de données dans sa mémoire interne (les registres), des calculs, des comparaisons,... Ces commandes étant très simples, il en faut souvent un certain nombre pour arriver au résultat voulu. Et la programmation est sans filet : le processeur exécutera à la lettre les commandes envoyées.
C'est pour cela que des langages de plus haut niveau d'abstraction, comme le BASIC (pour rester sur le VG5000µ), ont été créés. Le BASIC a des instructions simples à utiliser, son interpréteur se charge de vérifier les erreurs, de gérer la mémoire,... Les instructions de base du BASIC sont aussi les mêmes, pour ce qui est du coeur du langage, d'une machine à une autre, alors qu'il existe un assembleur différent dès que l'on change de processeur.
Toute la simplification des actions et la gestion d'erreurs a par contre un coût. De plus, le BASIC des machines telles que le VG5000µ est interprété, c'est-à-dire que les instructions et leurs paramètres sont décodées à chaque fois qu'elles sont exécutées. Cette interprétation amène au final à des instructions machines, mais très nombreuses.
Programmer en assembleur permet de s'affranchir de toute la lourdeur d'un langage interprété comme le BASIC, au prix d'un peu d'effort et d'attention. Et sur ce point, programmer une ancienne machine en 2017 aide beaucoup. Imaginez : programmer sur la machine elle-même à l'époque nécessitait de charger un programme d'édition, ce qui pouvait être long, et de repartir de zéro au moindre problème, puisqu'une erreur pouvait amener au plantage de la machine.
En 2017, avec émulation et outils d'assemblage qui tournent sur un ordinateur récent pour une vérification ultime sur la vraie machine, c'est beaucoup, beaucoup plus simple.
La mission
Le premier objectif en assembleur sur VG5000µ va être d'afficher une chaîne de caractères à l'écran. L'équivalent d'un PRINT "Bonjour!"
. Mais avant cela, il me faut parler un peu de la ROM
de la machine.
La ROM
est une mémoire persistante, son contenu reste le même à chaque fois que la machine est en fonctionnement et il ne peut pas être modifié pendant l'exécution. On l'oppose souvent à la RAM
, qui est une mémoire volatile, dont le contenu est perdu dès que la machine est éteinte.
La ROM
, dans une machine comme le VG5000µ, contient l'ensemble des opérations nécessaires à son fonctionnement : comment dialoguer avec le processeur qui s'occupe de l'affichage, comment jouer un son, comment charger et sauver des données sur cassette,... La ROM
contient aussi l'interpréteur BASIC et la gestion de l'interaction avec l'utilisateur.
Si son contenu n'est pas modifiable, il est par contre tout à fait possible de le lire, ou même d'en appeler des routines.
Dans le cadre de cette mission, je vais donc utiliser la routine d'affichage de chaîne de caractères. C'est exactement la même qui est utilisée in fine par l'instruction PRINT
, mais je vais l'utiliser directement.
La routine
La routine d'affichage de chaîne de caractères est située à l'adresse $36AA (le préfixe $
indique un nombre hexadécimal, en décimal, c'est l'équivalent de l'adresse 13994
).
Cette routine se charge en fait de l'impression à l'écran, sur une imprimante ou sur une cassette de sauvegarde. À défaut d'une action spéciale, l'affichage se fait à l'écran, je ne touche donc à rien.
La chaîne de caractères doit se terminer par le caractère de valeur 0
en mémoire.
La routine a besoin de connaître l'adresse en mémoire de la chaîne de caractères à afficher et pour celui utilise le registre HL
. Tous les autres registres seront modifiés, il faudra donc en préserver le contenu.
Aparté sur les registres : programmer en assembleur nécessite de connaître l'architecture logique du processeur. Une de ses composantes est que sa mémoire interne est découpée en registres. On peut imaginer les registres comme des conteneurs d'informations numériques.
Le programme va donc ressembler à quelque chose comme ça :
- Sauvegarde des registres
- Charger le registre HL avec l'adresse de la chaîne de caractères
- Appeler la routine
$36AA
- Restaurer les registres
- Revenir au programme appelant
La dernière étape de retour au programme appelant est nécessaire car l'appel depuis le BASIC se fera par l'instruction CALL
qui appelle une routine en assembleur qui doit rendre la main afin que le programme BASIC puisse continuer.
Écriture en assembleur
Pour sauvegarder les registres, je vais utiliser la pile. La pile est une zone de mémoire dans laquelle on peut pousser des informations pour les récupérer plus tard, dans un ordre tel que la dernière information poussée est la première information récupérée. Vous pouvez imaginez une pile de feuilles de papiers sur laquelle vous pouvez ajouter une nouvelle feuille sur le dessus, ou bien retirer la feuille du dessus pour en lire le contenu.
Pousser tous les registres sur la pile peut se faire comme ceci :
PUSH AF
PUSH BC
PUSH DE
PUSH HL
À la fin de ce petit programme, il faudra dépiler les valeurs de registre dans l'ordre inverse :
POP HL
POP DE
POP BC
POP AF
Pour charger le registre HL
avec l'adresse de la chaîne de caractère, on va demander de l'aide à l'assembleur (le programme, ici). Cette adresse en effet pourrait être fixée à la main, mais cela ne serait pas très flexible. Plutôt qu'une adresse, on utilise plutôt une étiquette (un label) comme une référence au contenu pointé.
LD HL, chaine
(... plus loin ...)
chaine:
DEFB "Bonjour !", 0
DEFB
n'est pas réellement une instruction qui s'associe au langage machine. C'est une pseudo instruction, ou directive d'assemblage, qui indique à l'assembleur les valeurs numériques à placer directement en mémoire. Ici, on y place les valeurs numériques correspondantes aux caractères de "Bonjour !", suivi de la valeur 0, qui indique la fin de la chaîne. L'étiquette chaîne
prendra automatiquement lors de l'assemblage l'adresse mémoire calculée.
Si DEFB
est séparé de programme lui-même, c'est qu'une fois en mémoire, le processeur Z80 exécutera les instructions une à une, sans avoir de moyen de distinguer ce qui est une véritable instruction de ce qui est de la donnée. Si la chaîne de caractères se trouve sur son chemin d'exécution, elle sera prise pour une série d'instructions.
Pour les curieux : « Bonjour! » pris comme une suite d'instructions donne ce qui suit et qui n'a pas beaucoup de sens
LD B,D
LD L,A
LD L,(HL)
LD L,D
LD L,A
LD (HL),L
LD (HL),D
LD HL,$0000
Reprenons... appeler la routine $36AA
est simplement :
CALL $36AA
Et revenir au programme appelant se fait avec l'instruction RET
.
Dernière petite opération, il faut spécifier à l'assembleur l'adresse de début du programme, afin que les adresses des étiquettes puissent être calculées. Cela peut se faire, entre autre méthode, par l'utilisation de la directive d'assemblage ORG
, comme origin.
Ce qui donne au final la séquence suivante :
ORG $7000
PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD HL, chaine
CALL $36AA
POP HL
POP DE
POP BC
POP AF
RET
chaine:
DEFB "Bonjour !", 0
Ce qui donne une fois passé à l'assembleur la séquence en langage machine suivante (en représentation hexadécimale) :
E5 C5 F5 D5 CD AA 36 D1 F1 C1 E1 C9 42 6F 6E 6A 6F 75 72 21 00
Lancement du programme
Cette séquence en langage machine doit ensuite être implantée dans la mémoire et appelée depuis le BASIC. Une manière de faire est d'utiliser les instructions DATA
et POKE
du BASIC. DATA
indique des séquences de valeurs à lire avec READ
. POKE
modifie une valeur de la mémoire à l'adresse spécifiée par le premier paramètre avec la valeur en deuxième paramètre.
Voici un exemple de chargeur de programme en langage machine écrit en BASIC sur VG5000µ.
10 S=&"7000"
20 READ A$
30 IF A$="FIN" THEN END
20 A$="&"+CHR$(34)+A$+CHR$(34):A=VAL(A$)
30 POKE S,A
50 S=S+1
60 GOTO 20
300 DATA F5,C5,D5,E5,21,07,70,CD,AA,36,E1,D1,C1,F1,C9,42,6F,6E,6A,6F,75,72,20,21,00
400 DATA FIN
Une fois ce programme entré et lancé avec RUN
, la routine d'affichage peut être appelée avec CALL &"7000"
et affichera Bonjour!
à l'écran.
Résultat
Forcément, j'ai déployé beaucoup d'efforts pour afficher une simple chaîne de caractères. Il n'y a pas vraiment d'intérêt autre que pédagogique. Le programme en lui-même est long à cause de la sauvegarde et la restauration des registres, il faut l'accompagner d'un chargeur qui est plus grand que le programme en assembleur lui-même, et l'action n'étant appelée qu'une fois, il n'a pas d'intérêt en vitesse d'exécution.
Mais nous avons pu voir à quoi ressemblait la programmation en assembleur.