Grâce au livre de Patterson et Hennessy :
Mon premier programme en assembleur RISC-V
Computer Organization and Design - RISC-V Edition
Article mis en ligne le 3 mars 2021
dernière modification le 31 mars 2021

Cet article se poursuit par celui-ci, puis celui-là.

 Pourquoi l’assembleur ?

Programmer en assembleur donne accès à une connaissance intime, presque physique, du fonctionnement de l’ordinateur. Ce fut mon métier pendant quelques années de la décennie 1970, période passionnante de ma vie professionnelle, mais en France il n’est guère possible de faire une carrière décente dans ce genre d’activité. De ce fait l’assembleur et le système d’exploitation que je connais le plus intimement, encore aujourd’hui, sont ceux de la série IBM 360, aujourd’hui renommée zSeries mais restée étonnamment stable, comme j’ai pu en juger lors d’un exposé d’Ayoub Elaassal sur la sécurité de ces systèmes lors d’une réunion de l’Observatoire de la sécurité des systèmes d’information et des réseaux (cf. aussi la vidéo).

Frustré d’assembleur, j’ai néanmoins dédaigné celui des processeurs d’architecture Intel x86, archaïque et biscornue. J’ai nourri quelques espoirs pour l’architecture ARM, me suis acheté un RaspberryPi 3 et un manuel d’assembleur ARM et j’ai attaqué les exercices. La promesse de la révolution RISC, c’était la sobriété, la simplicité et la régularité, avec un jeu d’instruction réduit : ARM ne tient pas vraiment cette promesse, leur assembleur est presque aussi biscornu que celui d’Intel.

 L’espoir RISC-V

Aujourd’hui mes espoirs (et ceux de beaucoup d’autres) se portent vers l’architecture libre et ouverte RISC-V, surtout depuis qu’ARM est lourdement impliqué par les mesures de boycott des entreprises chinoises par le gouvernement américain, sans parler des controverses autour du projet de son rachat par Nvidia.

L’architecture ISA (Instruction Set Architecture) de RISC-V est disponible sans frais pour tout industriel, et les premiers matériels commencent à apparaître. Les compilateurs GCC, LLVM (donc Clang), Rust et d’autres produisent du code RISC-V. Les systèmes d’exploitation Linux, FreeBSD, HarmonyOS (de Huawei) et d’autres tournent sur plate-forme RISC-V. Si on veut essayer il existe des cartes FPGA (field-programmable gate array, circuit logique programmable). Certes, pour que l’écosystème RISC-V soit en mesure de faire jeu égal avec x86 et ARM, il faudrait que se développent les logiciels de conception de circuits, et que les fonderies TSMC, Samsung, STMicro, etc., s’y mettent, ce qui demandera des investissements qui se comptent en milliards d’euros et en centaines d’ingénieurs pendant des années. En tout cas la firme chinoise Alibaba a d’ores et déjà annoncé un processeur RISC-V 64 bits (RV64GCV) 16 cœurs 2,5 GHz baptisé XuanTie 910.

 Un simulateur RISC-V bien conçu

Je ne possède pas de machine dotée du processeur XuanTie 910, mais deux bienfaiteurs de l’humanité, Pete Sanderson et Kenneth Vollmar, ont écrit un simulateur en Java, que l’on peut télécharger ici, d’une utilisation pratique et agréable. Il est distribué sous forme d’une arcchive jar exécutable, ce qui donne l’occasion de se remettre à l’esprit le mode de déploiement d’une application Java, dont il faut bien dire qu’il est puissant, sûr, commode, quand même autre chose que du Python...

On peut charger dans le simulateur son code assembleur, l’assembler, on voit le code machine (instructions de 32 bits), la valeur des registres et des différents drapeaux, celle des champs de la section .data, que l’on peut afficher en décimal, hexadécimal ou Ascii, puis on peut lancer l’exécution pas à pas ou directe. Il accepte les appels système Linux. Que demande le peuple ?

Cette expérience me rappelle combien l’agencement des bits dans les mots selon l’option Big Endian est bien plus pratique que l’option Little Endian, malheureusement retenue par les concepteurs de RISC-V. Je crains être un des derniers Big Endianers.

 Un livre bien écrit

En fait j’avais décidé de me mettre à l’assembleur RISC-V en découvrant que la dernière édition du livre classique de David A. Patterson et John L. Hennessy était intitulée Computer Organization and Design - RISC-V Edition, ce qui donnait à penser (à juste titre) qu’elle comporterait une description de cette architecture et des exercices de programmation. David Patterson, professeur à Berkeley, est l’architecte principal des processeurs SPARC de Sun (maintenant Oracle) et RISC-V, John Hennessy, professeur à Stanford, est l’architecte principal des processeurs MIPS. Tous les deux se sont vu décerner le prix Turing 2017 pour leurs contributions éminentes tant aux architectures RISC qu’à l’élaboration théorique de leurs principes. Quand ces deux-là parlent de processeurs, on peut leur faire confiance. Leur livre, qui s’adresse à des presque débutants, est bien écrit et très pédagogique. Je me suis attelé aux exercices.

 Comment débuter

Mes débuts en assembleur RISC-V m’ont ramené aux recommandations que je donne à mes étudiants : pour commencer, contentez-vous de recopier un programme vu en cours ou dans le bouquin, compilez-le et faites-le tourner, vous verrez que déjà là vous allez rencontrer des problèmes, et que les résoudre vous en apprendra beaucoup. Le plus difficile pour un débutant dans un programme, ce sont les cinq ou dix premières lignes, les cinq dernières, et toutes celles où il y a des interactions avec le monde extérieur, telles que lecture, écriture, appels système. Le reste, c’est ce que le cours raconte, et sauf algorithmes très subtils c’est plus facile.

C’est bien ce que j’ai rencontré : le code recopié directement du livre ne fonctionnait pas tel quel. Pour savoir ce qui clochait j’ai couru le Web. Par exemple le blog de Stephen Smith, qui m’a mis le pied à l’étrier. Son programme Hello World m’a donné ces fameuses lignes de début et de fin, ainsi que les appels système Linux.

 Première version de strcpy

Sur les traces de Patterson et Hennessy, voici ma première version de strcpy. Je sais que ce programme n’est pas optimal, mal écrit, pas structuré, il serait plus efficace avec des pointeurs qu’avec des tableaux, c’est un premier jet à peu près illisible, ce sera mieux pour la version suivante ci-dessous (pour lire le code voici une carte de référence du jeu d’instructions) :

  1. # strcpy en assembleur Risc-V, d’après Patterson et Hennessy
  2. #
  3.  
  4. .globl strcpy  # adresse de démarrage du programme pour l’éditeur de liens
  5.  
  6. strcpy:
  7.     addi sp, sp, -4   # accroît la pile pour un nouvel élément
  8.     sw   x19, 0(sp)   # sauvegarde x19
  9.     add  x19, x0, x0  # i <- 0+0
  10.     la   x10, destination
  11.     la   x11, origine
  12. L1: add  x5, x19, x11 # adresse de y[i] dans x5
  13.     lbu  x6, 0(x5)    # x6 <- y[i]
  14.     add  x7, x19, x10 # adresse de x[i] dans x7
  15.     sb   x6, 0(x7)    # x[i] <- y[i]
  16.     beq  x6, x0, L2   # si caractère NULL, c’est fini
  17.     addi x19, x19, 1  # sinon i <- i+1
  18.     jal  x0, L1       # on va à L1
  19. L2: lw   x19, 0(sp)   # restauration de x19
  20.     addi sp, sp, 4    # étête la pile d’un mot
  21.     addi a0, x0, 1    # 1 = StdOut
  22.     la   a1, destination # charger l’adresse
  23.     addi a2, x0, 12   # longueur de la chaîne
  24.     addi a7, x0, 64   # appel système Linux write
  25.     ecall             # appel Linux écriture de la chaîne
  26.  
  27.     addi a0, x0, 0    # code de retour 0
  28.     addi a7, x0, 93   # le code de commande 93
  29.     ecall             # Appel Linux pour finir
  30.  
  31. .data
  32.  
  33. destination:    .string "destination\n"
  34.  
  35. origine:        .string "initiations\n"

Télécharger

 Le même, structuré, avec des pointeurs

Le programme ci-dessus a plusieurs défauts : il utilise les indices pour parcourir les chaînes, ce qui est légitime mais moins efficace que l’utilisation de pointeurs. Surtout, le texte de la fonction strcpy proprement dite est mélangé avec celui des données de test et de leur manipulation. Il faut séparer les choses, en d’autres termes structurer le programme.

Un assembleur moderne, en principe, transforme les indices du précédent en pointeurs de celui qui vient, mais à la main c’est plus amusant.

  1. # strcpy en assembleur Risc-V, d’après Patterson et Hennessy
  2. #
  3.  
  4. .globl start  # adresse de démarrage du programme pour l’éditeur de liens
  5.  
  6. start:
  7. # Afficher avant
  8.     addi a0, x0, 1    # 1 = StdOut
  9.     la   a1, destination # charger l’adresse
  10.     addi a2, x0, 12   # longueur de la chaîne
  11.     addi a7, x0, 64   # appel système Linux write
  12.     ecall             # appel Linux écriture de la chaîne
  13.    
  14.     la   x10, origine
  15.     la   x11, destination
  16.     jal  ra, strcpy_fun
  17.  
  18. # Afficher après
  19.     addi a0, x0, 1    # 1 = StdOut
  20.     la   a1, destination # charger l’adresse
  21.     addi a2, x0, 12   # longueur de la chaîne
  22.     addi a7, x0, 64   # appel système Linux write
  23.     ecall             # appel Linux écriture de la chaîne
  24.  
  25.     addi a0, x0, 0    # code de retour 0
  26.     addi a7, x0, 93   # le code de commande 93
  27.     ecall             # Appel Linux pour finir
  28. ######
  29.  
  30. strcpy_fun:
  31.     addi sp, sp, -4   # accroît la pile pour un nouvel élément
  32.     sw   x19, 0(sp)   # sauvegarde x19
  33.     add  x19, x0, x0  # i <- 0+0
  34.     mv   x12, x10      # p2 = adresse de origine[0]
  35.     mv   x13, x11      # p1 = adresse de destination[0]
  36. L1:
  37.     lbu   x19, 0(x12)   # x19 <- origine[i]
  38.     sb    x19, 0(x13)   # destination[i] <- x19
  39.     beq   x19, x0, L2   # si NULL -> fin
  40.     addi  x12, x12, 1
  41.     addi  x13, x13, 1
  42.     jal   x0, L1
  43. L2:
  44.     lw   x19, 0(sp)   # restauration de x19
  45.     addi sp, sp, 4    # étête la pile d’un mot
  46.  
  47.     jalr zero, ra, 0
  48.  
  49. ######
  50.  
  51. .data
  52.  
  53. destination:    .string "destination\n"
  54.  
  55. origine:        .string "initiations\n"

Télécharger

 Encore mieux grâce à Samuel Tardieu et à Twitter

Samuel Tardieu a lu mon code et répondu : « Plutôt que t’embêter à sauvegarder et restaurer x19, je te suggère d’utiliser un registre “caller-save” comme x28. Si jamais l’appelant de ta fonction l’utilise, c’est à lui de le sauvegarder. Cela économise quatre instructions dont deux accès mémoire à la pile. » Un accès mémoire, en 2021, c’est le temps de 100 à 200 instructions. Ce qui donne :

  1. # strcpy en assembleur Risc-V, d’après Patterson et Hennessy
  2. #
  3.  
  4. .globl start  # adresse de démarrage du programme pour l’éditeur de liens
  5.  
  6. start:
  7. # Afficher avant
  8.     addi a0, x0, 1    # 1 = StdOut
  9.     la   a1, destination # charger l’adresse
  10.     addi a2, x0, 12   # longueur de la chaîne
  11.     addi a7, x0, 64   # appel système Linux write
  12.     ecall             # appel Linux écriture de la chaîne
  13.    
  14.     la   x10, origine
  15.     la   x11, destination
  16.     jal  ra, strcpy_fun
  17.  
  18. # Afficher après
  19.     addi a0, x0, 1    # 1 = StdOut
  20.     la   a1, destination # charger l’adresse
  21.     addi a2, x0, 12   # longueur de la chaîne
  22.     addi a7, x0, 64   # appel système Linux write
  23.     ecall             # appel Linux écriture de la chaîne
  24.  
  25.     addi a0, x0, 0    # code de retour 0
  26.     addi a7, x0, 93   # le code de commande 93
  27.     ecall             # Appel Linux pour finir
  28. ######
  29.  
  30. strcpy_fun:
  31.     add  x28, x0, x0  # i <- 0+0
  32.     mv   x12, x10      # p2 = adresse de origine[0]
  33.     mv   x13, x11      # p1 = adresse de destination[0]
  34. L1:
  35.     lbu   x28, 0(x12)   # x28 <- origine[i]
  36.     sb    x28, 0(x13)   # destination[i] <- x28
  37.     beq   x28, x0, L2   # si NULL -> fin
  38.     addi  x12, x12, 1
  39.     addi  x13, x13, 1
  40.     jal   x0, L1
  41. L2:
  42.     jalr zero, ra, 0
  43.  
  44. ######
  45.  
  46. .data
  47.  
  48. destination:    .string "destination\n"
  49.  
  50. origine:        .string "initiations\n"

Télécharger

 Emmanuel Lazard enrichit le programme

Publier un programme est utile, on reçoit des améliorations substantielles, dont celles d’Emmanuel Lazard, qui améliorent la structuration et la généralité du code :

  1. # strcpy en assembleur Risc-V, d’après Patterson et Hennessy
  2. #
  3.  
  4. .globl _start  # adresse de démarrage du programme pour l’éditeur de liens
  5.  
  6. _start:
  7. # Afficher avant
  8.     la   a1, destination    # charger l’adresse
  9.     jal  ra, print          # affichage de la chaîne
  10.    
  11.     la   x10, origine
  12.     la   x11, destination
  13.     jal  ra, strcpy_fun
  14.  
  15. # Afficher après
  16.     la   a1, destination    # charger l’adresse
  17.     jal  ra, print          # affichage de la chaîne
  18.    
  19.     addi a0, x0, 0          # code de retour 0
  20.     addi a7, x0, 93         # le code de commande 93
  21.     ecall                   # Appel Linux pour finir
  22.  
  23. ######
  24.  # fonction strlen : calcule la longueur d’une chaîne
  25.  #  a1 : pointeur sur le début de la chaîne
  26.  #  a2 : renvoyé avec la longueur
  27. strlen:
  28.     mv    t1, a1            # copie de a1 pour utilisation
  29.     addi  a2, zero, -1      # a2 <- -1
  30. loop:
  31.     lbu   t2, 0(t1)         # caractère courant
  32.     addi  a2, a2, 1         # un caractère de plus
  33.     addi  t1, t1, 1         # pointer sur le caractère suivant
  34.     bne   t2, zero,loop     # encore ?
  35.     ret
  36.  
  37. ######
  38.  # fonction print : affiche une chaîne
  39.  #  a1 : pointeur sur la chaîne
  40. print:
  41.     addi sp, sp -4              # sauvegarde ra sur la pile
  42.     sw   ra, 0(sp)
  43.     jal  ra, strlen         # fonction de calcul de la longueur
  44.     addi a0, x0, 1          # 1 = StdOut
  45.     addi a7, x0, 64         # appel système Linux write
  46.     ecall                   # appel Linux écriture de la chaîne
  47.     lw   ra, 0(sp)          # restauration de ra depuis la pile
  48.     addi sp, sp,4           #  pour l’adresse de retour
  49.     ret
  50.  
  51. ######
  52. strcpy_fun:
  53.     mv   x12, x10           # p2 = adresse de origine[0]
  54.     mv   x13, x11           # p1 = adresse de destination[0]
  55. L1:
  56.     lbu   x28, 0(x12)       # x28 <- origine[i]
  57.     sb    x28, 0(x13)       # destination[i] <- x28
  58.     beq   x28, x0, L2       # si octet nul -> fin
  59.     addi  x12, x12, 1
  60.     addi  x13, x13, 1
  61.     jal   x0, L1
  62. L2:
  63.     ret
  64. ######
  65.  
  66. .data
  67.  
  68. destination:    .string "destination\n"
  69.  
  70. origine:        .string "initiations\n"

Télécharger