De MIPS à RISC-V
L’article précédent reconnaissait ma dette envers David A. Patterson et John L. Hennessy, auteurs du livre Computer Organization and Design - RISC-V Edition qui m’a guidé dans ces travaux, et auparavant auteurs principaux des architectures de microprocesseurs, respectivement SPARC et MIPS, qui toutes les deux ont donné lieu à des réalisations industrielles très significative, par Sun Microsystems (maintenant Oracle) pour la première, par MIPS Computer Systems (aujourd’hui MIPS Technologies) pour la seconde.
Dès lors que ces architectures furent incarnées dans des ordinateurs réels achetés par des clients qui voulaient s’en servir, il apparut assez vite que l’architecture MIPS était magnifiquement conçue, par sa sobriété et son élégance. Les ingénieurs de MIPS furent les premiers à comprendre, par exemple, que le taux de succès du TLB (Translation Lookaside Buffer) était tellement élevé que l’on pouvait se dispenser d’un dispositif DAT (Dynamic Address Translation) câblé pour traduire les adresses de mémoire virtuelle en mémoire réelle, puisque le TLB conservait les résultats des traductions les plus récentes, et donnait la réponse dans plus de 99% des cas.
De son côté, l’architecture SPARC comportait 160 registres et utilisait l’idée de fenêtre mobile de registres pour y conserver beaucoup de données sans pour autant augmenter le nombre de bits nécessaires à leur désignation dans les instructions. Cette idée, séduisante, devait se révéler difficile à utiliser en pratique, et l’architecture SPARC fut globalement un échec.
David Patterson a conçu les bases de RISC-V en comprenant que les idées de son co-auteur pour MIPS s’étaient révélées meilleures que les siennes pour SPARC, et RISC-V emprunte à MIPS la sobriété et la simplicité qui permettent un assembleur pratique, utilisable. Jean-François Perrot m’avait déjà aiguillé vers l’architecture MIPS.
Le simulateur
L’article précédent chantait les louanges du simulateur RISC-V de Pete Sanderson et Kenneth Vollmar. En fait ces deux enseignants avaient écrit (en Java) un simulateur MIPS, nommé MARS, mais en prenant soin d’isoler la spécification du jeu d’instructions de sorte qu’il soit possible d’en changer. L’adaptation à RISC-V sous le nom de RARS est en fait l’œuvre de Benjamin Landers.
Depuis l’article précédent j’ai passé quelques dizaines d’heures avec ce simulateur, et je ne puis que renouveler les éloges formulés précédemment. Il y a par exemple un système d’aide en ligne, avec répertoire des instructions, pseudo-instructions, directives et appels système. Les appels système sont ceux de Linux, bien sûr, j’ignore ce qui peut se passer pour Windows ou macOS...
Ouvrir des fichiers, lire et écrire
Mon projet initial était l’implantation du programme de tri à bulles proposé par Patterson et Hennessy, mais curieusement ils ne proposaient aucun procédé d’acquisition des données. J’ai donc entrepris d’explorer les entrées-sorties. Curieusement, l’exploration du Web donne fort peu de résultat, à croire que la question n’intéresse pas grand monde.
La directive .include
permet de répartir le texte du programme en différents modules, ce qui améliore lisibilité et réutilisabilité.
L’aide en ligne de RARS documente les appels système pour ouvrir et fermer des fichiers, y lire ou écrire des lignes de caractères, lire ou afficher à la console des entiers ou des flottants. Je n’ai pas trouvé comment détecter une fin de fichier, ni comment convertir en nombres des chaînes de caractères numériques. Donc voilà déjà un premier jet rudimentaire, proposé à vos suggestions d’amélioration :
##### Module file-mgt.s
# Ouverture de fichier en lecture
# a0 : -> nom du fichier, .string
# a1 : drapeau : 0 lecture, 1 écriture
# a0 renvoie le descripteur
open_file:
addi sp, sp, -4 # sauvegarde ra sur la pile
sw ra, 0(sp)
li a7, 1024 # appel système ouverture de fichier
ecall # ouverture, descripteur en a0
lw ra, 0(sp) # restauration de ra depuis la pile
addi sp, sp,4 # pour l’adresse de retour
ret
######
######
# Fermeture du fichier
# a0 : descripteur du fichier à fermer
close_file:
addi sp, sp, -4 # sauvegarde ra sur la pile
sw ra, 0(sp)
li a7, 57 # appel système fermeture de fichier
ecall
lw ra, 0(sp) # restauration de ra depuis la pile
addi sp, sp,4 # pour l’adresse de retour
ret
######
Chaînes de caractères
L’épisode précédent avait présenté des procédures de manipulation de chaînes de caractères (dues à l’obligeance d’Emmanuel Lazard), désormais nous pouvons aussi les écrire ou les lire dans des fichiers :
###### Module de chaînes strings.s
# fonction str_len : calcule la longueur d’une chaîne
# a1 : pointeur sur le début de la chaîne
# a2 : renvoyé avec la longueur
str_len:
mv t1, a1 # copie de a1 pour utilisation
addi a2, zero, -1 # a2 <- -1
loop:
lbu t2, 0(t1) # caractère courant
addi a2, a2, 1 # un caractère de plus
addi t1, t1, 1 # pointer sur le caractère suivant
bne t2, zero, loop # encore ?
ret
######
# fonction print_str : affiche une chaîne
# a1 : pointeur sur la chaîne
print_str:
addi sp, sp -4 # sauvegarde ra sur la pile
sw ra, 0(sp)
jal ra, str_len # fonction de calcul de la longueur
addi a0, x0, 1 # 1 = StdOut
addi a7, x0, 64 # appel système Linux write
ecall # appel Linux écriture de la chaîne
lw ra, 0(sp) # restauration de ra depuis la pile
addi sp, sp,4 # pour l’adresse de retour
ret
######
######
# Lecture d’une ligne
# a0 : descripteur du fichier
# a1 : -> buffer
# a2 : longueur maximum du buffer
read_line:
addi sp, sp -4 # sauvegarde ra sur la pile
sw ra, 0(sp)
li a7, 63
ecall # lecture
lw ra, 0(sp) # restauration de ra depuis la pile
addi sp, sp,4 # pour l’adresse de retour
ret
######
######
# Écriture d’une ligne
# a0 : descripteur du fichier
# a1 : -> buffer
# a2 : longueur du buffer
write_line:
addi sp, sp -4 # sauvegarde ra sur la pile
sw ra, 0(sp)
li a7, 64
ecall # écriture
lw ra, 0(sp) # restauration de ra depuis la pile
addi sp, sp,4 # pour l’adresse de retour
ret
######
Lire et afficher des entiers à la console
Ce n’est pas l’idéal, mais toujours mieux que de coder les données en dur dans le texte du programme :
##### Module de lecture-écriture à la console read-print.s
# Lire des entiers à la console
# Imprimer des entiers et des chaînes
#####
read_int:
li a7, 5 # system call ReadInt
ecall # lecture
mv a1, a0 # a1 <-- l’entier lu
ret
#####
######
# fonction print_int : affiche un entier
# a0 : pointeur sur l’entier
print_int:
addi sp, sp -4 # sauvegarde ra sur la pile
sw ra, 0(sp)
addi a7, x0, 1 # appel système Linux PrintInt
ecall # appel Linux écriture d’un entier
lw ra, 0(sp) # restauration de ra depuis la pile
addi sp, sp,4 # pour l’adresse de retour
ret
######
Assembler le tout
Muni de cet appareillage rudimentaire, on peut assembler un programme qui fait quelques choses, et qui, au prochain épisode, devrait nous permettre de trier des tableaux, un des activités favorites des programmeurs :
# Lecture de fichier, d’après Kenneth Vollmar and Pete Sanderson
#
.globl _start # adresse de démarrage du programme pour l’éditeur de liens
_start:
###############
# Ouverture de fichier en lecture
la a0, influx # nom du fichier entrée
li a1, 0 # ouverture (drapeau 0 lecture, 1 écriture)
jal ra, open_file
mv x30, a0 # x30 -> descripteur
# Ouvrir un fichier en écriture
la a0, exflux # nom du fichier sortie
li a1, 1 # ouverture (drapeau 0 lecture, 1 écriture)
jal ra, open_file
mv x31, a0 # x31 -> descripteur
li x29, 16 # x29 <-- nb choses à écrire
li x28, 0 # x28 <-- pour boucle
# Appel de la lecture du fichier
loop1:
mv a0, x30 # a0 -> descripteur
la a1, ma_zone # a1 <-- adresse du buffer
li a2, 2 # a2 <-- taille du buffer
jal ra, read_line
# Écriture de ce que l’on vient de lire
ecrire:
mv a0, x31 # a0 -> descripteur
la a1, ma_zone # a1 <-- adresse du buffer
jal ra, write_line
jal ra, print_str
for1tst:
addi x28, x28, 1
bge x28, x29, exit
j loop1
# Fin du programme
exit:
mv a0, x30
jal ra, close_file
mv a0, x31
jal ra, close_file
addi a0, x0, 0 # code de retour 0
addi a7, x0, 93 # le code de commande 93
ecall # Appel Linux pour finir
#####
.include "strings.s"
.include "read-print.s"
.include "file-mgt.s"
######
.data
.align 2 # Aligner ce qui suit sur une frontière de mot
ma_zone: .word 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
influx: .string "intab.txt"
exflux: .string "extab.txt"
Au prochain épisode, algorithmes de tri !