Dans cet article, nous explorons divers concepts de bas niveau (compilation et mise en page, temps d'exécution primitifs, assembleur, etc.) à travers le prisme de l'architecture RISC-V et de son écosystème. Je suis moi-même développeur web, je ne fais rien au travail, mais c'est très intéressant pour moi, c'est de là que vient l'article! Rejoignez-moi dans ce voyage mouvementé dans les profondeurs du chaos de bas niveau.
Tout d'abord, parlons un peu de RISC-V et de l'importance de cette architecture, configurons la chaîne d'outils RISC-V et exécutons un programme C simple sur du matériel RISC-V émulé.
Table des matières
- Qu'est-ce que RISC-V?
- Configuration des outils QEMU et RISC-V
- Salut RISC-V!
- Approche naĂŻve
- Lever le rideau -v
- Rechercher dans notre pile
- Disposition
- Arrête ça!
Hammertime! Runtime!
- Déboguer mais maintenant pour de vrai
- Et ensuite?
- En option
Qu'est-ce que RISC-V?
RISC-V est une architecture de jeu d'instructions gratuit. Le projet est né à l'Université de Californie à Berkeley en 2010. Un rôle important dans son succès a été joué par l'ouverture du code et la liberté d'utilisation, qui étaient très différentes de nombreuses autres architectures. Prenez ARM: pour créer un processeur compatible, vous devez payer une avance
de 1 à 10 millions de dollars, ainsi que des redevances de 0,5 à 2% sur les ventes . Un modèle gratuit et ouvert fait de RISC-V une option attrayante pour beaucoup, y compris pour les startups qui ne peuvent pas payer de licence pour un ARM ou un autre processeur, pour les chercheurs universitaires et (évidemment) pour la communauté open source.
La croissance rapide de la popularité de RISC-V n'est pas passée inaperçue. ARM a
lancé un site qui a tenté (plutôt sans succès) de mettre en évidence les prétendus avantages d'ARM sur RISC-V (le site est déjà fermé). Le projet RISC-V est soutenu par de
nombreuses grandes entreprises , dont Google, Nvidia et Western Digital.
Configuration des outils QEMU et RISC-V
Nous ne pouvons pas exécuter le code sur le processeur RISC-V avant d'avoir configuré l'environnement. Heureusement, cela ne nécessite pas de processeur RISC-V physique; à la place, nous prenons
qemu . Suivez les
instructions d'installation de
votre système d'exploitation . J'ai MacOS, alors entrez simplement une commande:
Idéalement,
qemu
est livré avec
plusieurs machines prĂŞtes Ă l'emploi (voir l'
qemu-system-riscv32 -machine
).
Ensuite, installez
OpenOCD pour les
outils RISC-V et RISC-V.
Téléchargez les assemblages prêts à l'emploi des outils RISC-V OpenOCD et RISC-V
ici .
Nous extrayons les fichiers dans n'importe quel répertoire, je l'ai
~/usys/riscv
. N'oubliez pas cela pour une utilisation future.
mkdir -p ~/usys/riscv cd ~/Downloads cp openocd-<date>-<platform>.tar.gz ~/usys/riscv cp riscv64-unknown-elf-gcc-<date>-<platform>.tar.gz ~/usys/riscv cd ~/usys/riscv tar -xvf openocd-<date>-<platform>.tar.gz tar -xvf riscv64-unknown-elf-gcc-<date>-<platform>.tar.gz
Définissez les variables d'environnement
RISCV_OPENOCD_PATH
et
RISCV_PATH
pour que d'autres programmes puissent trouver notre chaîne d'outils. Cela peut sembler différent selon le système d'exploitation et le shell: j'ai ajouté les chemins d'accès au
~/.zshenv
.
Créez un lien symbolique dans
/usr/local/bin
pour ce fichier exécutable afin de pouvoir l'exécuter à tout moment sans spécifier le chemin complet vers
~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/riscv64-unknown-elf-gcc
.
Et le tour est joué, nous avons une boîte à outils RISC-V fonctionnelle! Tous nos exécutables, tels que
riscv64-unknown-elf-gcc
,
riscv64-unknown-elf-gdb
,
riscv64-unknown-elf-ld
et autres, se trouvent dans
~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/
.
Salut RISC-V!
Patch du 26 mai 2019:
Malheureusement, en raison d'un bogue dans RISC-V QEMU, le programme «hello world» de freedom-e-sdk dans QEMU ne fonctionne plus. Un correctif a été publié pour résoudre ce problème, mais pour l'instant, ignorez cette section. Ce programme ne sera pas nécessaire dans les sections suivantes de l'article. Je surveille la situation et met à jour l'article après avoir corrigé le bogue.
Voir ce commentaire pour plus d'informations.Une fois les outils configurés, exécutons le programme RISC-V simple. Commençons par cloner le référentiel SiFive
freedom-e-sdk :
cd ~/wherever/you/want/to/clone/this git clone --recursive https://github.com/sifive/freedom-e-sdk.git cd freedom-e-sdk
Par tradition , commençons par le programme 'Hello, world' du référentiel
freedom-e-sdk
. Nous utilisons le
Makefile
prêt à l'emploi qu'ils fournissent pour compiler ce programme en mode débogage:
make PROGRAM=hello TARGET=sifive-hifive1 CONFIGURATION=debug software
Et exécutez dans QEMU:
qemu-system-riscv32 -nographic -machine sifive_e -kernel software/hello/debug/hello.elf Hello, World!
C'est un bon début. Vous pouvez exécuter d'autres exemples à partir de
freedom-e-sdk
. Après cela, nous allons écrire et essayer de déboguer notre propre programme en C.
Approche naĂŻve
Commençons par un programme simple qui ajoute infiniment deux nombres.
cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; }
Nous voulons exécuter ce programme, et la première chose dont nous avons besoin pour le compiler pour le processeur RISC-V.
Cela crée le fichier
a.out
, que
gcc
utilise par défaut pour les fichiers exécutables. Maintenant, exécutez ce fichier dans
qemu
:
Nous avons choisi la machine
virt
riscv-qemu
.
Maintenant que notre programme s'exécute dans QEMU avec le serveur GDB sur
localhost:1234
, nous nous y connectons avec le client RISC-V GDB Ă partir d'un terminal distinct:
Et nous sommes à l'intérieur de GDB!
Ce GDB a été configuré comme "--host = x86_64-apple-darwin17.7.0 --target = riscv64-unknown-elf". │
Tapez "show configuration" pour les détails de configuration. │
Pour les instructions de rapport de bogue, veuillez consulter: │
<http://www.gnu.org/software/gdb/bugs/>. │
Trouvez le manuel GDB et d'autres ressources de documentation en ligne sur: │
<http://www.gnu.org/software/gdb/documentation/>. │
│
Pour obtenir de l'aide, tapez "help". │
Tapez "mot approprié" pour rechercher des commandes liées à "mot" ... │
Lecture des symboles de a.out ... │
(gdb)
Nous pouvons essayer d'exécuter les commandes
run
ou
start
pour le fichier exécutable
a.out
dans GDB, mais pour le moment cela ne fonctionnera pas pour une raison évidente. Nous avons compilé le programme en tant que
riscv64-unknown-elf-gcc
, donc l'hĂ´te devrait fonctionner sur l'architecture
riscv64
.
Mais il y a une issue! Cette situation est l'une des principales raisons de l'existence du modèle client-serveur de GDB. Nous pouvons prendre le fichier exécutable
riscv64-unknown-elf-gdb
et au lieu de le lancer sur l'hôte, lui spécifier une cible distante (serveur GDB). Comme vous vous en souvenez, nous venons de lancer
riscv-qemu
et nous avons dit de démarrer le serveur GDB sur
localhost:1234
. Connectez-vous simplement Ă ce serveur:
(gdb) cible à distance: 1234 │
Débogage à distance en utilisant: 1234
Vous pouvez maintenant définir des points d'arrêt:
(gdb) b main Breakpoint 1 at 0x1018e: file add.c, line 2. (gdb) b 5
Et enfin, spécifiez GDB
continue
(commande abrégée
c
) jusqu'Ă ce que nous atteignions le point d'arrĂŞt:
(gdb) c Continuing.
Vous remarquerez rapidement que le processus ne se termine en aucune façon. C'est étrange ... ne devrions-nous pas immédiatement atteindre le point d'arrêt
b 5
? Qu'est-il arrivé?

Ici, vous pouvez voir plusieurs problèmes:
- L'interface utilisateur de texte ne peut pas trouver la source. L'interface doit afficher notre code et tous les points d'arrêt à proximité.
- GDB ne voit pas la ligne d'exécution actuelle (
L??
) et affiche le compteur 0x0 ( PC: 0x0
).
- Du texte dans la ligne d'entrée, qui dans son intégralité ressemble à ceci:
0x0000000000000000 in ?? ()
0x0000000000000000 in ?? ()
Combinés au fait que nous ne pouvons pas atteindre le point d'arrêt, ces indicateurs indiquent: nous avons fait
quelque chose de mal. Mais quoi?
Lever le rideau -v
Pour comprendre ce qui se passe, vous devez prendre du recul et parler du fonctionnement de notre programme C simple sous le capot. La fonction
main
fait un simple ajout, mais qu'est-ce que c'est vraiment? Pourquoi devrait-il être appelé
main
, pas d'
origin
ou
begin
? Selon la convention, tous les fichiers exécutables commencent à être exécutés avec la fonction
main
, mais quelle magie fournit ce comportement?
Pour répondre à ces questions, répétons notre équipe GCC avec l'indicateur
-v
pour obtenir une sortie plus détaillée de ce qui se passe réellement.
riscv64-unknown-elf-gcc add.c -O0 -g -v
La sortie est volumineuse, nous ne verrons donc pas la liste entière. Il est important de noter que bien que GCC soit formellement un compilateur, il est également défini par défaut sur la compilation (pour se limiter à la compilation et à l'assemblage, vous devez spécifier l'indicateur
-c
). Pourquoi est-ce important? Eh bien, jetez un œil à l'extrait de la sortie détaillée de
gcc
:
# La commande `gcc -v` réelle génère des chemins complets, mais ceux-ci sont assez
# long, alors faites comme si ces variables existaient.
# $ RV_GCC_BIN_PATH = / Users / twilcock / usys / riscv / riscv64-unknown-elf-gcc- <date> - <version> / bin /
# $ RV_GCC_LIB_PATH = $ RV_GCC_BIN_PATH /../ lib / gcc / riscv64-unknown-elf / 8.2.0
$ RV_GCC_BIN_PATH /../ libexec / gcc / riscv64-unknown-elf / 8.2.0 / collect2 \
... tronquée ...
$ RV_GCC_LIB_PATH /../../../../ riscv64-unknown-elf / lib / rv64imafdc / lp64d / crt0.o \
$ RV_GCC_LIB_PATH / riscv64-unknown-elf / 8.2.0 / rv64imafdc / lp64d / crtbegin.o \
-lgcc --start-group -lc -lgloss --end-group -lgcc \
$ RV_GCC_LIB_PATH / rv64imafdc / lp64d / crtend.o
... tronquée ...
COLLECT_GCC_OPTIONS = '- O0' '-g' '-v' '-march = rv64imafdc' '-mabi = lp64d'
Je comprends que même sous forme abrégée, c'est beaucoup, alors laissez-moi vous expliquer. Sur la première ligne,
gcc
exécute le programme
collect2
, transmet les arguments
crt0.o
,
crtbegin.o
et
crtend.o
, les
-lgcc
et
--start-group
. La description de collect2 se trouve
ici : en bref, collect2 organise diverses fonctions d'initialisation au démarrage, réalisant la mise en page en une ou plusieurs passes.
Ainsi, GCC compile plusieurs fichiers
crt
avec notre code. Comme vous pouvez le deviner,
crt
signifie «runtime C».
Ici, il est décrit en détail à quoi chaque
crt
destinĂ©, mais nous nous intĂ©ressons Ă
crt0
, qui fait une chose importante:
"Cet objet [crt0] devrait contenir le caractère _start
, qui indique le bootstrap du programme."
L'essence du «bootstrap» dĂ©pend de la plate-forme, mais elle implique gĂ©nĂ©ralement des tâches importantes telles que la configuration d'un cadre de pile, la transmission d'arguments de ligne de commande et l'appel Ă
main
. Oui, nous avons
enfin trouvé la réponse à la question: c'est
_start
appelle notre fonction principale!
Rechercher dans notre pile
Nous avons résolu une énigme, mais comment cela nous rapproche-t-il de l'objectif initial - exécuter un programme C simple dans
gdb
? Il reste à résoudre plusieurs problèmes: le premier d'entre eux est lié à la façon dont
crt0
configure notre pile.
Comme nous l'avons vu ci-dessus,
gcc
crt0
défaut la
crt0
. Les paramètres par défaut sont sélectionnés en fonction de plusieurs facteurs:
- Triplet cible correspondant Ă la structure du
machine-vendor-operatingsystem
de machine-vendor-operatingsystem
. Nous l'avons riscv64-unknown-elf
- Architecture cible,
rv64imafdc
- ABI cible,
lp64d
Habituellement, tout fonctionne bien, mais pas pour tous les processeurs RISC-V. Comme mentionné précédemment, l'une des tâches de
crt0
est de configurer la pile. Mais il ne sait pas exactement oĂą devrait ĂŞtre la pile pour notre CPU (
-machine
)? Il ne peut pas le faire sans notre aide.
Dans la commande
qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out
nous avons utilisé la machine
virt
. Heureusement,
qemu
facilite le vidage des informations de la machine dans un vidage
dtb
(blob d'arbre de périphérique).
Les données Dtb sont difficiles à lire car il s'agit essentiellement d'un format binaire, mais il existe un utilitaire de ligne de commande
dtc
(compilateur d'arborescence de périphériques) qui peut convertir le fichier en quelque chose de plus lisible.
Le fichier de sortie est
riscv64-virt.dts
, où nous voyons beaucoup d'informations intéressantes sur
virt
: le nombre de cœurs de processeur disponibles, l'emplacement mémoire de divers périphériques, tels que UART, l'emplacement de la mémoire interne (RAM). La pile doit être dans cette mémoire, alors recherchez-la avec
grep
:
grep memory riscv64-virt.dts -A 3 memory@80000000 { device_type = "memory"; reg = <0x00 0x80000000 0x00 0x8000000>; };
Comme vous pouvez le voir, ce nœud a une «mémoire» spécifiée en tant que
device_type
. Apparemment, nous avons trouvé ce que nous recherchions. Par les valeurs à l'intérieur de
reg = <...> ;
Vous pouvez déterminer où commence la banque de mémoire et quelle est sa longueur.
Dans
la spécification devicetree, nous voyons que la syntaxe
reg
est un nombre arbitraire de paires
(base_address, length)
. Cependant, il y a quatre significations dans
reg
. Étrange, deux valeurs ne suffisent-elles pas pour une banque de mémoire?
Encore une fois, à partir de la spécification devicetree (recherche de la propriété
reg
), nous constatons que le nombre de cellules
<u32>
pour spécifier l'adresse et la longueur est déterminé par les propriétés
#address-cells
et
#size-cells
dans le nœud parent (ou dans le nœud lui-même). Ces valeurs ne sont pas spécifiées dans notre nœud de mémoire, et le nœud de mémoire parent est simplement la racine du fichier. Regardons dedans pour ces valeurs:
head -n8 riscv64-virt.dts /dts-v1/; / { #address-cells = <0x02>; #size-cells = <0x02>; compatible = "riscv-virtio"; model = "riscv-virtio,qemu";
Il s'avère que l'adresse et la longueur nécessitent deux valeurs 32 bits. Cela signifie qu'avec
reg = <0x00 0x80000000 0x00 0x8000000>;
notre mémoire commence
0x00 + 0x80000000 (0x80000000)
et occupe
0x00 + 0x8000000 (0x8000000)
octets, c'est-Ă -dire qu'elle se termine Ă
0x88000000
, ce qui correspond à 128 mégaoctets.
Disposition
En utilisant
qemu
et
dtc
nous avons trouvé les adresses RAM dans la machine virtuelle virt. Nous savons également que
gcc
compose
crt0
par défaut, sans configurer la pile comme nous en avons besoin. Mais comment utiliser ces informations pour éventuellement exécuter et déboguer le programme?
Puisque
crt0
ne nous convient pas, il y a une option évidente: écrire votre propre code, puis le composer avec le fichier objet que nous avons obtenu après avoir compilé notre programme simple. Notre
crt0
besoin de savoir oĂą commence le haut de la pile pour l'initialiser correctement. Nous pourrions
crt0
valeur
0x80000000
directement sur
crt0
, mais ce n'est pas une solution très appropriée, compte tenu des changements qui pourraient être nécessaires à l'avenir. Et si nous voulons utiliser un autre processeur, tel que
sifive_e
, avec des caractéristiques différentes dans l'émulateur?
Heureusement, nous ne sommes pas les premiers à poser cette question, et une bonne solution existe déjà . L'éditeur de liens GNU
ld
vous permet de définir le caractère disponible à partir de notre
crt0
. Nous pouvons définir le symbole
__stack_top
approprié pour différents processeurs.
Au lieu d'écrire votre propre fichier de l'éditeur de liens à partir de zéro, il est logique de prendre le script par défaut avec
ld
et de le modifier un peu pour prendre en charge des caractères supplémentaires. Qu'est-ce qu'un script de l'éditeur de liens?
Voici une bonne description :
Le but principal du script de l'éditeur de liens est de décrire comment les sections de fichier sont mises en correspondance en entrée et en sortie, et de contrôler la disposition de la mémoire du fichier de sortie.
Sachant cela, copions le script de l'éditeur de liens par défaut
riscv64-unknown-elf-ld
dans un nouveau fichier:
cd ~/usys/riscv
Ce fichier contient de
nombreuses informations intéressantes, bien plus que ce que nous pouvons discuter dans cet article. La sortie détaillée avec l'
--Verbose
comprend des informations sur la version
ld
, les architectures prises en charge et bien plus encore. Tout cela est bon à savoir, mais une telle syntaxe n'est pas autorisée dans le script de l'éditeur de liens, alors ouvrez un éditeur de texte et supprimez tout ce qui est superflu du fichier.
vim riscv64-virt.ld
# Supprimez tout ce qui précède et y compris la ligne =============
GNU ld (GNU Binutils) 2,32
Émulations prises en charge:
elf64lriscv
elf32lriscv
en utilisant un script de l'éditeur de liens interne:
====================================================
/ * Script pour -z combreloc: combiner et trier les sections de relocalisation * /
/ * Copyright (C) 2014-2019 Free Software Foundation, Inc.
Copie et distribution de ce script, avec ou sans modification,
sont autorisés sur tout support sans redevance à condition que le copyright
avis et cet avis sont conservés. * /
OUTPUT_FORMAT ("elf64-littleriscv", "elf64-littleriscv",
"elf64-littleriscv")
... reste du script de l'éditeur de liens ...
Après cela, exécutez la commande
MEMORY pour déterminer manuellement où sera
__stack_top
. Recherchez la ligne qui commence par
OUTPUT_ARCH(riscv)
, elle doit se trouver en haut du fichier et ajoutez la commande
MEMORY
dessous:
OUTPUT_ARCH(riscv) /* >>> Our addition. <<< */ MEMORY { /* qemu-system-risc64 virt machine */ RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M } /* >>> End of our addition. <<< */ ENTRY(_start)
Nous avons créé un bloc de mémoire appelé
RAM
, pour lequel la lecture (
r
), l'écriture (
w
) et le stockage du code exécutable (
x
) sont autorisés.
Très bien, nous avons défini une disposition de mémoire qui correspond aux spécifications de notre machine
virt
RISC-V. Vous pouvez maintenant l'utiliser. Nous voulons mettre notre pile en mémoire.
Vous devez définir le caractère
__stack_top
. Ouvrez votre script de l'éditeur de liens (
riscv64-virt.ld
) dans un éditeur de texte et ajoutez quelques lignes:
SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x10000)); . = SEGMENT_START("text-segment", 0x10000) + SIZEOF_HEADERS; /* >>> Our addition. <<< */ PROVIDE(__stack_top = ORIGIN(RAM) + LENGTH(RAM)); /* >>> End of our addition. <<< */ .interp : { *(.interp) } .note.gnu.build-id : { *(.note.gnu.build-id) }
Comme vous pouvez le voir, nous définissons
__stack_top
Ă
l' aide de
la commande __stack_top
. Le symbole sera accessible depuis n'importe quel programme associé à ce script (en supposant que le programme lui-même ne déterminera pas quelque chose avec le nom
__stack_top
). Définissez
__stack_top
sur
ORIGIN(RAM)
. Nous savons que cette valeur est
0x80000000
plus
LENGTH(RAM)
, qui est de 128 mégaoctets (
0x8000000
octets). Cela signifie que notre
__stack_top
défini sur
0x88000000
.
Par souci de concision, je ne répertorierai pas l'intégralité du fichier de l'éditeur de liens
ici ; vous pouvez le voir
ici .
Arrête ça! Hammertime! Runtime!
Nous avons maintenant tout ce dont nous avons besoin pour créer notre propre runtime C. En fait, c'est une tâche assez simple, voici tout le fichier
crt0.s
:
.section .init, "ax" .global _start _start: .cfi_startproc .cfi_undefined ra .option push .option norelax la gp, __global_pointer$ .option pop la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end
Attire immédiatement un grand nombre de lignes commençant par une période. Il s'agit d'un fichier pour assembleur en
as
. Les lignes avec des points sont appelées
directives assembleur : elles fournissent des informations à l'assembleur. Ce n'est pas du code exécutable, comme les instructions de l'assembleur RISC-V comme
jal
et
add
.
Passons en revue le fichier ligne par ligne. Nous travaillerons avec divers registres RISC-V standard, alors consultez
ce tableau , qui couvre tous les registres et leur fonction.
.section .init, "ax"
Comme indiqué dans
l'assembleur GNU «as» , cette ligne indique à l'assembleur d'insérer le code suivant dans la section
.init
, qui est allouée (
a
) et exécutable (
x
). Cette section est une autre
convention courante pour l'exécution de code dans le système d'exploitation. Nous travaillons sur du matériel pur sans OS, donc dans notre cas une telle instruction peut ne pas être absolument nécessaire, mais en tout cas c'est une bonne pratique.
.global _start _start:
.global
met le caractère suivant à la disposition de
ld
. Sans cela, le lien ne fonctionnera pas, car la commande
ENTRY(_start)
dans le script de l'éditeur de liens pointe vers le symbole
_start
comme point d'entrée vers le fichier exécutable. La ligne suivante indique à l'assembleur que nous commençons la définition du caractère
_start
.
_start: .cfi_startproc .cfi_undefined ra ...other stuff... .cfi_endproc
Ces directives
.cfi
vous informent sur la structure du cadre et comment le gérer. Les
.cfi_endproc
.cfi_startproc
et
.cfi_endproc
signalent le début et la fin d'une fonction, et
.cfi_undefined ra
indique Ă l'assembleur que le registre
ra
ne doit pas être restauré à la valeur qu'il contient avant le
_start
.
.option push .option norelax la gp, __global_pointer$ .option pop
Ces directives
.option
modifient le comportement de l'assembleur en fonction du code lorsque vous devez appliquer un ensemble d'options spécifique.
Voici une description détaillée des raisons pour lesquelles l'utilisation de
.option
dans ce segment est importante:
... puisque nous assouplissons éventuellement l'adressage des séquences à des séquences plus courtes par rapport au GP, le chargement initial du GP ne devrait pas être affaibli et devrait ressembler à ceci:
.option push .option norelax la gp, __global_pointer$ .option pop
de sorte qu'après la relaxation, vous obtenez le code suivant:
auipc gp, %pcrel_hi(__global_pointer$) addi gp, gp, %pcrel_lo(__global_pointer$)
au lieu de simple:
addi gp, gp, 0
Et maintenant la dernière partie de nos
crt0.s
:
_start: ...other stuff... la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end
Ici, nous pouvons enfin utiliser le symbole
__stack_top
, que nous avons travaillé si dur pour créer.
La pseudo-instruction la
(adresse de chargement) charge la valeur
__stack_top
dans le registre
sp
(pointeur de pile), la définissant pour une utilisation dans le reste du programme.
Ensuite,
add s0, sp, zero
ajoute les valeurs des registres
sp
et
zero
(qui est en fait le registre
x0
avec une référence matérielle à 0) et place le résultat dans le registre
s0
. Il s'agit d'un
registre spécial qui est inhabituel à plusieurs égards. Tout d'abord, il s'agit d'un «registre persistant», c'est-à -dire qu'il est enregistré lors de l'appel de la fonction. Deuxièmement,
s0
agit parfois comme un pointeur de trame, ce qui donne à chaque appel de fonction un petit espace dans la pile pour stocker les paramètres passés à cette fonction. Le fonctionnement des appels de fonction avec la pile et les pointeurs de trame est un sujet très intéressant que vous pouvez facilement consacrer à un article séparé, mais pour l'instant, sachez que dans notre runtime, il est important d'initialiser le pointeur de trame
s0
.
Ensuite, nous voyons le
jal zero, main
déclaration
jal zero, main
. Ici,
jal
signifie sauter et lier. L'instruction attend des opérandes sous la forme de
jal rd (destination register), offset_address
. Fonctionnellement,
jal
écrit la valeur de l'instruction suivante (registre
pc
plus quatre) dans
rd
, puis définit le registre
pc
sur la valeur
pc
actuelle plus l'adresse de décalage avec l'
extension de signe , «appelant» effectivement cette adresse.
Comme mentionné ci-dessus,
x0
étroitement lié à la valeur littérale 0, et y écrire est inutile.
Par conséquent, il peut sembler étrange que nous utilisions un registre comme registre de destination zero
, que les assembleurs RISC-V interprètent comme un registre x0
. Après tout, cela signifie une transition inconditionnelle vers offset_address
. Pourquoi faire cela, parce que dans d'autres architectures, il existe une instruction explicite pour une transition inconditionnelle?Ce modèle étrange jal zero, offset_address
est en fait une optimisation intelligente. La prise en charge de chaque nouvelle instruction signifie une augmentation et, par conséquent, une augmentation du coût du processeur. Par conséquent, plus l'ISA est simple, mieux c'est. Au lieu de polluer l'espace d'instructions avec deux instructions jal
et unconditional jump
, l'architecture RISC-V ne prend en charge que les jal
sauts inconditionnels jal zero, main
.RISC-V possède de nombreuses optimisations de ce type, dont la plupart prennent la forme de pseudo - instructions . Les assembleurs savent comment les traduire en instructions matérielles réelles. Par exemple, j offset_address
les assembleurs RISC-V traduisent les pseudo- instructions pour les sauts inconditionnels en jal zero, offset_address
. Pour une liste complète des pseudo instructions officiellement prises en charge, voir la spécification RISC-V (version 2.2) . _start: ...other stuff... jal zero, main .cfi_endproc .end
Notre dernière ligne est la directive assembleur .end
, qui marque simplement la fin du fichier.Déboguer mais maintenant pour de vrai
En essayant de déboguer un simple programme C sur un processeur RISC-V, nous avons résolu beaucoup de problèmes. Tout d'abord, utiliser qemu
et dtc
trouver notre mémoire dans la machine virtuelle virt
RISC-V. Ensuite, nous avons utilisé ces informations pour contrôler manuellement l'allocation de mémoire dans notre version du script par défaut de l'éditeur de liens riscv64-unknown-elf-ld
, ce qui nous a permis de déterminer avec précision le symbole __stack_top
. Ensuite, nous avons utilisé ce symbole dans notre propre version crt0.s
, qui configure notre pile et nos pointeurs globaux, et a finalement appelé la fonction main
. Vous pouvez maintenant atteindre votre objectif et commencer à déboguer notre programme simple dans GDB.Rappelons ici le programme C lui-même: cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; }
Compilation et liaison: riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections -nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld crt0.s add.c
, , , .
-ffreestanding
, , . ( ), , .
-Wl
— (
ld
).
--gc-sections
« »,
ld
.
-nostartfiles
,
-nostdlib
-nodefaultlibs
(,
crt0
), les implémentations standard du système stdlib et les bibliothèques liées par défaut du système standard. Nous avons notre propre script crt0
et éditeur de liens, il est donc important de passer ces drapeaux afin que les valeurs par défaut n'entrent pas en conflit avec nos préférences utilisateur.-T
indique le chemin vers notre script de l'éditeur de liens, qui est simple dans notre cas riscv64-virt.ld
. Enfin, nous spécifions les fichiers que nous voulons compiler, compiler et composer: crt0.s
et add.c
. Comme précédemment, le résultat est un fichier complet et prêt à fonctionner appelé a.out
.Maintenant, exécutez notre tout nouvel exécutable flambant neuf dans qemu
:
Maintenant, exécutez gdb
, n'oubliez pas de charger les symboles de débogage pour a.out
, en le spécifiant avec le dernier argument: riscv64-unknown-elf-gdb --tui a.out GNU gdb (GDB) 8.2.90.20190228-git Copyright (C) 2019 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "--host=x86_64-apple-darwin17.7.0 --target=riscv64-unknown-elf". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from a.out... (gdb)
gdb
gdb
,
qemu
:
(gdb) target remote :1234 │ Remote debugging using :1234
main:
(gdb) b main Breakpoint 1 at 0x8000001e: file add.c, line 2.
:
(gdb) c Continuing. Breakpoint 1, main () at add.c:2
, 2! , -
L
,
PC:
L2
,
PC:
—
0x8000001e
. , :

gdb
:
-s
,
info all-registers
. . … , , !
Et ensuite?
, , ! , , . , .
jal
, , ,
add.c
- RISC-V. - , - ,
.
! , !
En option
,
« : main()» CppCon2018. , . , !