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 :

  1. ##### Module file-mgt.s
  2.  # Ouverture de fichier en lecture
  3.  # a0 : -> nom du fichier, .string
  4.  # a1 : drapeau : 0 lecture, 1 écriture
  5.  # a0 renvoie le descripteur
  6. open_file:
  7.     addi sp, sp, -4 # sauvegarde ra sur la pile
  8.     sw   ra, 0(sp)
  9.     li   a7, 1024   # appel système ouverture de fichier
  10.     ecall           # ouverture, descripteur en a0
  11.     lw   ra, 0(sp)  # restauration de ra depuis la pile
  12.     addi sp, sp,4   #  pour l’adresse de retour
  13.     ret
  14. ######
  15.  
  16. ######
  17.  # Fermeture du fichier
  18. # a0 : descripteur du fichier à fermer
  19. close_file:
  20.     addi sp, sp, -4 # sauvegarde ra sur la pile
  21.     sw   ra, 0(sp)
  22.     li   a7, 57     # appel système fermeture de fichier
  23.     ecall
  24.     lw   ra, 0(sp)  # restauration de ra depuis la pile
  25.     addi sp, sp,4   #  pour l’adresse de retour
  26.     ret
  27. ######

Télécharger

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 :

  1. ###### Module de chaînes strings.s
  2.  # fonction str_len : calcule la longueur d’une chaîne
  3.  #  a1 : pointeur sur le début de la chaîne
  4.  #  a2 : renvoyé avec la longueur
  5. str_len:
  6.     mv    t1, a1            # copie de a1 pour utilisation
  7.     addi  a2, zero, -1      # a2 <- -1
  8. loop:
  9.     lbu   t2, 0(t1)         # caractère courant
  10.     addi  a2, a2, 1         # un caractère de plus
  11.     addi  t1, t1, 1         # pointer sur le caractère suivant
  12.     bne   t2, zero, loop    # encore ?
  13.     ret
  14.  
  15. ######
  16.  # fonction print_str : affiche une chaîne
  17.  #  a1 : pointeur sur la chaîne
  18. print_str:
  19.     addi sp, sp -4          # sauvegarde ra sur la pile
  20.     sw   ra, 0(sp)
  21.     jal  ra, str_len         # fonction de calcul de la longueur
  22.     addi a0, x0, 1          # 1 = StdOut
  23.     addi a7, x0, 64        # appel système Linux write
  24.     ecall                   # appel Linux écriture de la chaîne
  25.     lw   ra, 0(sp)          # restauration de ra depuis la pile
  26.     addi sp, sp,4           #  pour l’adresse de retour
  27.     ret
  28. ######
  29. ######
  30.  # Lecture d’une ligne
  31.  # a0 : descripteur du fichier
  32.  # a1 : -> buffer
  33.  # a2 : longueur maximum du buffer
  34. read_line:
  35.     addi sp, sp -4  # sauvegarde ra sur la pile
  36.     sw   ra, 0(sp)
  37.     li   a7, 63
  38.     ecall           # lecture
  39.     lw   ra, 0(sp)  # restauration de ra depuis la pile
  40.     addi sp, sp,4   #  pour l’adresse de retour
  41.     ret
  42. ######
  43. ######
  44.  # Écriture d’une ligne
  45.  # a0 : descripteur du fichier
  46.  # a1 : -> buffer
  47.  # a2 : longueur du buffer
  48. write_line:
  49.     addi sp, sp -4  # sauvegarde ra sur la pile
  50.     sw   ra, 0(sp)
  51.     li   a7, 64
  52.     ecall           # écriture
  53.     lw   ra, 0(sp)  # restauration de ra depuis la pile
  54.     addi sp, sp,4   #  pour l’adresse de retour
  55.     ret
  56. ######

Télécharger

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 :

  1. ##### Module de lecture-écriture à la console read-print.s
  2. # Lire des entiers à la console
  3. # Imprimer des entiers et des chaînes
  4.  
  5. #####
  6. read_int:
  7.     li a7, 5    # system call ReadInt
  8.     ecall       # lecture
  9.     mv a1, a0   # a1 <-- l’entier lu
  10.     ret
  11. #####
  12. ######
  13.  # fonction print_int : affiche un entier
  14.  #  a0 : pointeur sur l’entier
  15. print_int:
  16.     addi sp, sp -4 # sauvegarde ra sur la pile
  17.     sw   ra, 0(sp)
  18.     addi a7, x0, 1 # appel système Linux PrintInt
  19.     ecall          # appel Linux écriture d’un entier
  20.     lw   ra, 0(sp) # restauration de ra depuis la pile
  21.     addi sp, sp,4  #  pour l’adresse de retour
  22.     ret
  23. ######

Télécharger

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 :

  1. # Lecture de fichier, d’après Kenneth Vollmar and Pete Sanderson
  2. #
  3.  
  4. .globl _start  # adresse de démarrage du programme pour l’éditeur de liens
  5.  
  6. _start:
  7.  
  8. ###############
  9. # Ouverture de fichier en lecture
  10.  
  11.     la  a0, influx  # nom du fichier entrée
  12.     li  a1, 0 # ouverture (drapeau 0 lecture, 1 écriture)
  13.     jal ra, open_file
  14.     mv  x30, a0     # x30 ->  descripteur
  15.  
  16. # Ouvrir un fichier en écriture
  17.     la   a0, exflux # nom du fichier sortie
  18.     li   a1, 1 # ouverture (drapeau 0 lecture, 1 écriture)
  19.     jal  ra, open_file
  20.     mv   x31, a0    # x31 ->  descripteur
  21.  
  22.     li x29, 16 # x29 <-- nb choses à écrire
  23.     li x28, 0  # x28 <-- pour boucle
  24. # Appel de la lecture du fichier
  25. loop1:
  26.     mv   a0, x30     # a0 -> descripteur
  27.     la   a1, ma_zone # a1 <-- adresse du buffer
  28.     li   a2, 2       # a2 <-- taille du buffer
  29.     jal     ra, read_line
  30. # Écriture de ce que l’on vient de lire
  31. ecrire:
  32.     mv   a0, x31     # a0 -> descripteur    
  33.     la   a1, ma_zone # a1 <-- adresse du buffer
  34.     jal  ra, write_line
  35.     jal  ra, print_str
  36. for1tst:
  37.     addi x28, x28, 1
  38.     bge  x28, x29, exit
  39.     j     loop1
  40.  
  41. # Fin du programme
  42. exit:    
  43.     mv     a0, x30
  44.     jal     ra, close_file
  45.     mv     a0, x31
  46.     jal     ra, close_file
  47.     addi a0, x0, 0  # code de retour 0
  48.     addi a7, x0, 93 # le code de commande 93
  49.     ecall           # Appel Linux pour finir
  50.  
  51. #####
  52. .include "strings.s"
  53. .include "read-print.s"
  54. .include "file-mgt.s"
  55. ######
  56.  
  57. .data
  58. .align    2  # Aligner ce qui suit sur une frontière de mot
  59. ma_zone:  .word    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
  60. influx:   .string "intab.txt"
  61. exflux:    .string "extab.txt"

Télécharger

Au prochain épisode, algorithmes de tri !