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 :

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 19 juillet 2022

par Laurent Bloch

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) :

# strcpy en assembleur Risc-V, d’après Patterson et Hennessy
#

.globl strcpy  # adresse de démarrage du programme pour l’éditeur de liens

strcpy:
    addi sp, sp, -4   # accroît la pile pour un nouvel élément
    sw   x19, 0(sp)   # sauvegarde x19
    add  x19, x0, x0  # i <- 0+0
    la   x10, destination
    la   x11, origine
L1: add  x5, x19, x11 # adresse de y[i] dans x5
    lbu  x6, 0(x5)    # x6 <- y[i]
    add  x7, x19, x10 # adresse de x[i] dans x7
    sb   x6, 0(x7)    # x[i] <- y[i]
    beq  x6, x0, L2   # si caractère NULL, c’est fini
    addi x19, x19, 1  # sinon i <- i+1
    jal  x0, L1       # on va à L1
L2: lw   x19, 0(sp)   # restauration de x19
    addi sp, sp, 4    # étête la pile d’un mot
    addi a0, x0, 1    # 1 = StdOut
    la   a1, destination # charger l’adresse
    addi a2, x0, 12   # longueur de la chaîne
    addi a7, x0, 64   # appel système Linux write
    ecall             # appel Linux écriture de la chaîne

    addi a0, x0, 0    # code de retour 0
    addi a7, x0, 93   # le code de commande 93 
    ecall             # Appel Linux pour finir

.data

destination:	.string "destination\n"	

origine:	.string	"initiations\n"

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.

# strcpy en assembleur Risc-V, d’après Patterson et Hennessy
#

.globl start  # adresse de démarrage du programme pour l’éditeur de liens

start:
# Afficher avant
    addi a0, x0, 1    # 1 = StdOut
    la   a1, destination # charger l’adresse
    addi a2, x0, 12   # longueur de la chaîne
    addi a7, x0, 64   # appel système Linux write
    ecall             # appel Linux écriture de la chaîne
    
    la   x10, origine
    la   x11, destination
    jal  ra, strcpy_fun

# Afficher après
    addi a0, x0, 1    # 1 = StdOut
    la   a1, destination # charger l’adresse
    addi a2, x0, 12   # longueur de la chaîne
    addi a7, x0, 64   # appel système Linux write
    ecall             # appel Linux écriture de la chaîne

    addi a0, x0, 0    # code de retour 0
    addi a7, x0, 93   # le code de commande 93 
    ecall             # Appel Linux pour finir
######

strcpy_fun:
    addi sp, sp, -4   # accroît la pile pour un nouvel élément
    sw   x19, 0(sp)   # sauvegarde x19
    add  x19, x0, x0  # i <- 0+0
    mv   x12, x10      # p2 = adresse de origine[0]
    mv   x13, x11      # p1 = adresse de destination[0]
L1:
    lbu   x19, 0(x12)   # x19 <- origine[i]
    sb    x19, 0(x13)   # destination[i] <- x19
    beq   x19, x0, L2   # si NULL -> fin
    addi  x12, x12, 1
    addi  x13, x13, 1
    jal   x0, L1
L2:
    lw   x19, 0(sp)   # restauration de x19
    addi sp, sp, 4    # étête la pile d’un mot

    jalr zero, ra, 0

######

.data

destination:	.string "destination\n"	

origine:	.string	"initiations\n"	

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 :

# strcpy en assembleur Risc-V, d’après Patterson et Hennessy
#

.globl start  # adresse de démarrage du programme pour l’éditeur de liens

start:
# Afficher avant
    addi a0, x0, 1    # 1 = StdOut
    la   a1, destination # charger l’adresse
    addi a2, x0, 12   # longueur de la chaîne
    addi a7, x0, 64   # appel système Linux write
    ecall             # appel Linux écriture de la chaîne
    
    la   x10, origine
    la   x11, destination
    jal  ra, strcpy_fun

# Afficher après
    addi a0, x0, 1    # 1 = StdOut
    la   a1, destination # charger l’adresse
    addi a2, x0, 12   # longueur de la chaîne
    addi a7, x0, 64   # appel système Linux write
    ecall             # appel Linux écriture de la chaîne

    addi a0, x0, 0    # code de retour 0
    addi a7, x0, 93   # le code de commande 93 
    ecall             # Appel Linux pour finir
######

strcpy_fun:
    add  x28, x0, x0  # i <- 0+0
    mv   x12, x10      # p2 = adresse de origine[0]
    mv   x13, x11      # p1 = adresse de destination[0]
L1:
    lbu   x28, 0(x12)   # x28 <- origine[i]
    sb    x28, 0(x13)   # destination[i] <- x28
    beq   x28, x0, L2   # si NULL -> fin
    addi  x12, x12, 1
    addi  x13, x13, 1
    jal   x0, L1
L2:
    jalr zero, ra, 0

######

.data

destination:	.string "destination\n"	

origine:	.string	"initiations\n"	

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 :

# strcpy en assembleur Risc-V, d’après Patterson et Hennessy
#

.globl _start  # adresse de démarrage du programme pour l’éditeur de liens

_start:
# Afficher avant
    la   a1, destination    # charger l’adresse
    jal  ra, print          # affichage de la chaîne
    
    la   x10, origine
    la   x11, destination
    jal  ra, strcpy_fun

# Afficher après
    la   a1, destination    # charger l’adresse
    jal  ra, print          # affichage de la chaîne
    
    addi a0, x0, 0          # code de retour 0
    addi a7, x0, 93         # le code de commande 93 
    ecall                   # Appel Linux pour finir

######
 # fonction strlen : calcule la longueur d’une chaîne
 #  a1 : pointeur sur le début de la chaîne
 #  a2 : renvoyé avec la longueur
strlen:
    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 : affiche une chaîne
 #  a1 : pointeur sur la chaîne
print:
    addi sp, sp, -4	        # sauvegarde ra sur la pile
    sw   ra, 0(sp)
    jal  ra, strlen         # 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

######
strcpy_fun:
    mv   x12, x10           # p2 = adresse de origine[0]
    mv   x13, x11           # p1 = adresse de destination[0]
L1:
    lbu   x28, 0(x12)       # x28 <- origine[i]
    sb    x28, 0(x13)       # destination[i] <- x28
    beq   x28, x0, L2       # si octet nul -> fin
    addi  x12, x12, 1
    addi  x13, x13, 1
    jal   x0, L1
L2:
    ret
######

.data

destination:	.string "destination\n"

origine:	.string	"initiations\n"