RISC-V à partir de zéro

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


  1. Qu'est-ce que RISC-V?
  2. Configuration des outils QEMU et RISC-V
  3. Salut RISC-V!
  4. Approche naĂŻve
  5. Lever le rideau -v
  6. Rechercher dans notre pile
  7. Disposition
  8. Arrête ça! Hammertime! Runtime!
  9. Déboguer mais maintenant pour de vrai
  10. Et ensuite?
  11. 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:

# also available via MacPorts - `sudo port install qemu` brew install qemu 

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 .

 # I put these two exports directly in my ~/.zshenv file - you may have to do something else. export RISCV_OPENOCD_PATH="$HOME/usys/riscv/openocd-<date>-<version>" export RISCV_PATH="$HOME/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>" # Reload .zshenv with our new environment variables. Restarting your shell will have a similar effect. source ~/.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 .

 # Symbolically link our gcc executable into /usr/local/bin. Repeat this process for any other executables you want to quickly access. ln -s ~/usys/riscv/riscv64-unknown-elf-gcc-8.2.0-<date>-<version>/bin/riscv64-unknown-elf-gcc /usr/local/bin 

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.

 # -O0 to disable all optimizations. Without this, GCC might optimize # away our infinite addition since the result 'c' is never used. # -g to tell GCC to preserve debug info in our executable. riscv64-unknown-elf-gcc add.c -O0 -g 

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 :

 # -machine tells QEMU which among our list of available machines we want to # run our executable against. Run qemu-system-riscv64 -machine help to list # all available machines. # -m is the amount of memory to allocate to our virtual machine. # -gdb tcp::1234 tells QEMU to also start a GDB server on localhost:1234 where # TCP is the means of communication. # -kernel tells QEMU what we're looking to run, even if our executable isn't # exactly a "kernel". qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out 

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:

 # --tui gives us a (t)extual (ui) for our GDB session. # While we can start GDB without any arguments, specifying 'a.out' tells GDB # to load debug symbols from that file for the newly created session. riscv64-unknown-elf-gdb --tui a.out 

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 # this is the line within the forever-while loop. int c = a + b; Breakpoint 2 at 0x1019a: file add.c, line 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:

  1. 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é.
  2. GDB ne voit pas la ligne d'exécution actuelle ( L?? ) et affiche le compteur 0x0 ( PC: 0x0 ).
  3. 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).

 # Go to the ~/usys/riscv folder we created before and create a new dir # for our machine information. cd ~/usys/riscv && mkdir machines cd machines # Use qemu to dump info about the 'virt' machine in dtb (device tree blob) # format. # The data in this file represents hardware components of a given # machine / device / board. qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb 

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.

 # I'm running MacOS, so I use Homebrew to install this. If you're # running another OS you may need to do something else. brew install dtc # Convert our .dtb into a human-readable .dts (device tree source) file. dtc -I dtb -O dts -o riscv64-virt.dts riscv64-virt.dtb 

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 # Make a new dir for custom linker scripts out RISC-V CPUs may require. mkdir ld && cd ld # Copy the default linker script into riscv64-virt.ld riscv64-unknown-elf-ld --verbose > riscv64-virt.ld 

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_addressest 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 jalet unconditional jump, l'architecture RISC-V ne prend en charge que les jalsauts 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_addressles 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 qemuet dtctrouver notre mémoire dans la machine virtuelle virtRISC-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 crt0et é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.

-Tindique 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.set 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:

 # -S freezes execution of our executable (-kernel) until we explicitly tell # it to start with a 'continue' or 'c' from our gdb client qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -S -kernel a.out 

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. , . , !

Source: https://habr.com/ru/post/fr454208/


All Articles