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 !