Site WWW de Laurent Bloch
Slogan du site

ISSN 2271-3905
Cliquez ici si vous voulez visiter mon autre site, orienté vers des sujets moins techniques.

Pour recevoir (au plus une fois par semaine) les nouveautés de ce site, indiquez ici votre adresse électronique :

Nouvel épisode de programmation en assembleur RISC-V
Article mis en ligne le 25 mars 2021
dernière modification le 31 mars 2021

par Laurent Bloch

Cet article fait suite à celui-ci et se poursuit par celui-là.

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 !