Partie 6: Portage de MemTest86 + vers RISC-V


Peu de spécialistes informatiques ont probablement besoin d'expliquer ce qu'est Memtest86 + - peut-être est-il déjà devenu plus ou moins la norme pour tester la RAM sur un PC. Lorsque dans l'une des parties précédentes, j'ai rencontré une barre de mémoire cassée fournie avec la carte, elle (avec un netbook compatible DDR2) semblait une solution évidente. Une autre question est que là, en principe, le fonctionnement instable du système était visible à l'œil nu. Dans des cas plus délicats, j'ai entendu qu'en plus du «tapotement» banal des cellules de mémoire à l'infini, cet outil utilise des modèles de données spéciaux sur lesquels les erreurs de fonctionnement DDR sont plus susceptibles d'être détectées. En général, une chose merveilleuse, il est dommage que même dans le nom, il est dit: 86 - "Uniquement pour les systèmes compatibles x86." Ou pas?


Sous la coupe, vous verrez mes tentatives de portage de MemTest86 + v5.1 sur RISC-V et le sous-total. Spoiler: ça bouge!


AVERTISSEMENT: le projet résultant a été testé de manière minimale spécifiquement par moi sur un assemblage RocketChip spécifique sur une carte spécifique. La précision et la sécurité (en particulier sur d'autres systèmes) ne sont pas garanties. Utilisez à vos risques et périls. En particulier, les zones de mémoire actuellement réservées ne sont en aucun cas traitées si elles tombent dans la plage de RAM.


Comme je l'ai déjà dit, il n'y a pas si longtemps, j'ai acheté une carte mère avec Cyclone IV sur AliExpress, mais sa mémoire était boguée. Heureusement, l'une des caractéristiques importantes de cette carte était l'utilisation de modules SO-DIMM DDR2 conventionnels - les mêmes que dans mon ancien netbook. Néanmoins, il serait intéressant d'obtenir, pour ainsi dire, une solution auto-hébergée pour tester les modules de mémoire (et, en fait, également le contrôleur). La perspective de déboguer mes erreurs dans des conditions de mauvaise mémoire n'était pas du tout agréable. N'espérant surtout pas une solution rapide et me préparant mentalement à reporter la réécriture complète dans un autre assembleur pour une durée indéfinie, j'ai ouvert un article Wikipedia sur Memtest86 + et j'ai soudainement vu «Written in: C and assembly» dans la carte. Hmm, c'est-à-dire lui, bien que "... 86", mais n'est pas écrit entièrement en assembleur? C'est encourageant. Il ne reste plus qu'à comprendre la relation.


Alors, allez sur memtest.org et téléchargez la version 5.01 sous GPL2. Pour faciliter le développement, je l'ai rechargé sur GitHub. Heureusement, directement dans l'archive source, nous sommes accueillis par le fichier README.background , intitulé


L'anatomie et la physiologie de Memtest86-SMP

Il explique en détail (et même avec des images sous forme d'art ASCII) le fonctionnement de haut niveau du code. Au tout début du document, nous voyons une disposition binaire , composée de bootsect.o , setup.o , head.o et quelques memtest_shared . Il est facile de voir que ces trois fichiers objets sont obtenus à partir des sources d'assembleur correspondantes. À première vue, tout le reste est écrit en C! Pas mal, pas mal ...


En conséquence, j'ai copié le Makefile dans Makefile.arch et j'ai commencé à tout réécrire et à essayer de jeter ce qui ne correspond pas. Tout d'abord, bien sûr, j'avais besoin d'une chaîne d'outils pour RISC-V, qui, heureusement, est toujours avec moi depuis les expériences précédentes. Au début, j'ai pensé à créer un port pour l'architecture 32 bits, mais ensuite je me suis souvenu qu'un processeur 64 bits avait été téléchargé sur la carte, et j'avais la riscv64- d' riscv64- avec le préfixe riscv64- .


Digression lyrique: bien sûr, la première chose a été d'étudier la question de la compatibilité du code 32 et 64 bits. En conséquence, la spécification pour la partie non privilégiée de l'ISA (Instruction Set Architecture) se trouve au paragraphe 1.3 RISC-V ISA Overview déclaration 1.3 RISC-V ISA Overview :


Le principal avantage de la séparation explicite des ISA de base est que chaque ISA de base peut être optimisée pour ses besoins sans nécessiter de prendre en charge toutes les opérations nécessaires pour les autres ISA de base. Par exemple, RV64I peut omettre des instructions et des CSR qui ne sont nécessaires que pour gérer les registres plus étroits dans RV32I. Les options RV32I peuvent utiliser l'espace de codage autrement réservé aux instructions requises uniquement par des variantes d'espace d'adressage plus larges.

Je tiens également à noter que la chaîne d'outils avec le préfixe riscv64- est susceptible de collecter facilement du code 32 bits si l'architecture cible est correctement sélectionnée - plus à ce sujet plus tard.


Lors du portage, il est judicieux de garder ces documents à portée de main:



Configuration de la construction


Commençons par accepter: je veux obtenir un port adapté pour un portage ultérieur vers des architectures autres que x86 et RISC-V. Je propose également de supprimer les disquettes de démarrage et autres spécificités x86 de la construction multiplateforme.


Ce que nous avons finalement: il y a trois fichiers assembleur: bootsect.S , setup.S et head.S Les deux premiers sont nécessaires uniquement au démarrage, et le troisième est nécessaire plus tard lors du déplacement vers une autre zone de mémoire. Le fait est que pour tester la mémoire "sous soi", le code de test doit d'abord se déplacer vers un nouvel endroit. Ces fichiers sont collectés dans ELF, à partir desquels des sections de code, de données, etc. sont ensuite extraites. De plus, il est collecté sous forme de PIC (Position Independent Code) - au début, j'ai même été surpris: bien que le code soit autonome (c'est-à-dire sans noyau, libc, etc.), il utilise de telles fonctionnalités avancées.


De plus, les paramètres qui définissent l'architecture se rencontrent périodiquement dans le Makefile: -march=i486 , -m32 et similaires. J'ai besoin d'écrire quelque chose comme ça, puis comme une ventouse . La situation avec l'architecture RISC-V est quelque chose comme ceci: il existe des rv64 rv32 et rv64 (comme, il y a toujours le plus tronqué embarqué et rv128 réservé pour l'avenir, mais nous ne sommes pas très intéressés par elles), et le nom ISA est formé en assignant des lettres à ce préfixe extensions: i - l'ensemble d'instructions de base entier, m - multiplication et division d'entiers, ... Bien sûr, je voudrais faire rv64i , mais Memtest86 ne sera pas facilement porté sur l'architecture sans multiplication. Certes, il semble que le compilateur génère simplement des appels de fonction au lieu d'instructions «problématiques», mais il y a un risque de rester avec des performances considérablement réduites (sans parler du fait que ces fonctions devront être écrites ou prises quelque part).


Vous aurez également besoin de la ligne ABI. En principe, les bases de la convention d'appel sont déjà décrites dans le Volume I spécifié dans le "Manuel du programmeur d'assemblage RISC-V", donc je vais faire quelque chose comme


 $ riscv64-linux-gnu-gcc-9 -mabi=help riscv64-linux-gnu-gcc-9: error: unrecognized argument in option '-mabi=help' riscv64-linux-gnu-gcc-9: note: valid arguments to '-mabi=' are: ilp32 ilp32d ilp32e ilp32f lp64 lp64d lp64f riscv64-linux-gnu-gcc-9: fatal error: no input files compilation terminated. 

Et sans réfléchir à lp64 , je prendrai lp64 . Pour l'avenir, je dirai qu'avec cet ABI, les fichiers d'en-tête de la bibliothèque standard ne fonctionnaient pas, j'ai donc pris lp64f et ARCH «mis à niveau» vers rv64imf . Sans panique, je ne compte pas vraiment utiliser de virgule flottante dans mon port.


Étant donné que je ne voulais pas approfondir l'écriture de scripts de l'éditeur de liens multiplateforme - et que je ne pouvais donc pas trouver immédiatement les clés de ld , j'ai décidé de m'en sortir avec le fichier head.S l'assembleur, accroché au reste des fonctions en utilisant memtest_shared.arch.lds . J'ai jeté une indication du format de sortie et de l'architecture à partir de celui-ci (après tout, il est plus facile de le changer à partir d'une variable dans le Makefile), et j'ai également temporairement commenté DISCARD à la fin, ne pouvant pas déterminer quelles sections spécifiques des informations de débogage dont j'avais besoin. (Pour l'avenir: de bonnes informations de débogage, mais .rela a dû être ajouté) De manière générale, la version x86 a souligné la nécessité de tenir en 64k - j'espère que cela est en quelque sorte lié aux fonctionnalités du mode réel et ne nous concerne pas sur RISC-V . En conséquence, l'objet partagé avec le PIC sera collecté, comme dans l'original, le code et les données qui seront chargés en mémoire en seront mordus.


Nous collectons ... et la compilation tombe sur le premier fichier reloc.c - apparemment, il est tiré de certains ld-linux.so et est responsable de la prise en charge de la table de décalage global, etc. selon les conventions d'appel pour x86. Il s'est avéré qu'il fallait travailler directement avec les registres à l'aide d'inserts d'assembleur. Mais nous sommes à RISC-V - il a été initialement conçu pour prendre en charge nativement PIC, alors n'hésitez pas à lancer reloc.c . De plus, il y avait encore des encarts, parfois assez longs. Heureusement, ils étaient soit dans le code de test immédiatement après le code C commenté, qu'ils optimisent (à partir d'eux, j'ai à nouveau créé des morceaux de code à part entière commutés par la directive du préprocesseur) ou quelque chose de dépendant de la plate-forme, sans lequel, dans les cas extrêmes, je peux (probablement) faire (comme activer / désactiver le cache, soustraire le CPUID, etc.). Enfin, il y avait des choses comme l'appel rdtsc , que moi rdtsc , sans gros problèmes, j'ai mis dans un en-tête dépendant de la plate-forme et l'avons implémenté conformément à la documentation sur RISC-V.


En conséquence, nous avons obtenu le répertoire arch/i386 , où une grande quantité de code de support PCI a été déplacée, a lu les informations des chipsets, les définitions spécifiques à la plate-forme des adresses mappées en mémoire, etc. De plus, le début de la fonction test_start est test_start , qui est le point d'entrée de setup.S au code C. Combien de temps, court, mais commentant tout ce qui est possible et réalisant tout ce qui ne peut pas être commenté sous RISC-V (comme setup.S et le code pour travailler avec port série dans l'implémentation SiFive), j'ai obtenu le arch/riscv , avec lequel tout a été plus ou moins compilé.


Ici, je suis obligé de préciser que les expériences elles-mêmes ont été partiellement réalisées avant la rédaction de l'article, de sorte qu'une séquence spécifique d' actions peut contenir une certaine quantité de «fiction artistique». Cependant, j'essaie au moins de mener la présentation de telle manière qu'elle représente en tout cas l'un des chemins possibles (je suis programmeur, je m'en souviens) . Voyons donc comment tout démarrer.


Courir sur le fer


Depuis les expériences passées, j'ai toujours un «support» poussiéreux du Raspberry Pi, câblé à la carte de débogage. Les fils fournissent UART, JTAG et un adaptateur avec une carte SD. Un certain processeur RV64 avec un contrôleur DDR2 est cousu dans la mémoire de configuration. Comme par le passé, j'allume la «framboise», j'ouvre deux sessions SSH avant celle-ci, dont l'une transmet le port TCP 3333 pour connecter gdb à OpenOCD. Dans l'une des sessions, je démarre minicom pour surveiller UART, dans un autre - openocd pour déboguer à partir de l'hôte via JTAG. J'allume la puissance de la carte - et les messages dans la console sur la façon dont elle charge les données de la SD ont couru.


Vous pouvez maintenant exécuter la commande:


 riscv64-unknown-elf-gdb \ -ex 'target remote 127.0.0.1:3333' \ -ex 'restore /path/to/memtest_shared.bin binary 0x80010000' \ -ex 'add-symbol-file /path/to/memtest_shared 0x80010000' -ex 'set $pc=0x80010000' 

les options -ex gdb de prétendre que l'utilisateur a entré ces commandes depuis la console:


  • le premier établit une connexion avec OpenOCD
  • le second copie le contenu du fichier hôte spécifié à l'adresse indiquée
  • le troisième explique à gdb que les informations sur le code source doivent être extraites de ce fichier, en tenant compte du fait qu'il a été téléchargé à cette adresse (et non de ce qui y est indiqué)
    • note: on prend les caractères du fichier ELF, et on charge le binaire "brut"
  • enfin, le quatrième traduit de force le pointeur de commande actuel en notre code

Malheureusement, tout ne se déroule pas parfaitement, et bien que les lignes de code du débogueur s'affichent correctement, mais dans toutes les variables globales - les zéros. En fait, si nous exécutons une commande de la forme p &global_var dans gdb, nous voyons, hélas, l'adresse conformément à l'adresse de téléchargement initiale (j'ai 0x0 ), qui n'est pas spécifiée en utilisant add-symbol-file . Comme béquille, mais une solution très simple, j'ai simplement ajouté 0x80010000 à l'adresse spécifiée manuellement et j'ai regardé le contenu de la mémoire via x/x 0xADDR . En fait, il serait possible d'indiquer temporairement l'adresse de départ correcte dans le script de l'éditeur de liens, qui coïncide actuellement avec l'adresse de téléchargement dans cette configuration de test .


Caractéristiques de la relocalisation sur des architectures modernes


Eh bien, comment télécharger le code d'une manière ou d'une autre, nous l'avons compris - nous le commençons. Ne marche pas. Le débogage étape par étape montre que nous tombons pendant le fonctionnement de la fonction switch_to_main_stack - il semble qu'il essaie toujours d'utiliser la valeur non liée de l'adresse du symbole correspondant à la pile de travail.


Tout de même, le premier volume de documentation nous parle de différentes pseudo-instructions et de leur travail avec et hors tension PIC:


Quelques pseudo-instructions RISC-V


Comme vous pouvez le voir, le principe général est que les adresses en mémoire sont comptées à partir de l'instruction en cours, la première ajoutant le haut de l'offset et la suivante add polissant les bits de poids faible. Il est difficile de déclarer une variable globale comme


 struct vars * const v = &variables; 

Par conséquent, nous prenons la documentation psABI RISC-V ELF avec des descriptions des types de délocalisations et écrivons la partie spécifique à la plate-forme pour reloc.c . Ici, il convient de noter que le fichier d'origine, apparemment, a été extrait du code multiplateforme. Là, même au lieu de spécifier une profondeur de bits spécifique, des macros du type ElfW(Addr) , qui sont développées dans Elf32_Addr ou Elf64_Addr . Pas partout, cependant, c'est pourquoi nous les ajoutons là où ils ne sont pas dans le code général (ainsi que dans le arch/riscv/reloc.inc.c - après tout, pour RISC-V, il n'y a pas de sens particulier à être lié à une profondeur de bits spécifique, où il n'est pas requis).


En conséquence, switch_to_main_stack commencé à passer (pas sans instructions d'assembleur dépendant de la plate-forme, bien sûr). Le débogueur affiche toujours des variables globales de manière tordue. Eh bien, d'accord :(


Définition du matériel


Bien sûr, pour les tests, il serait possible d'utiliser des constantes codées en dur au lieu du code de définition d'équipement qui a été jeté, mais pour chaque assemblage de processeur spécifique, la reconstruction de memtest est même trop coûteuse selon les normes de mon application. Par conséquent, nous agirons "comme des adultes sérieux". Heureusement, sur RISC-V (et probablement sur la plupart des architectures modernes), il est habituel que le chargeur de démarrage transmette un code au Device Tree Blob , qui est une version compilée de la description DTS comme ceci:


zeowaa-1gb.dts
 /dts-v1/; / { #address-cells = ^_^lt gt^_^; #size-cells = ^_^lt gt^_^; compatible = "freechips,rocketchip-unknown-dev"; model = "freechips,rocketchip-unknown"; chosen { bootargs = "console=ttySIF0,125200 debug loglevel=7"; }; firmware { sifive,uboot = "YYYY-MM-DD"; }; L16: aliases { serial0 = &L8; }; L15: cpus { #address-cells = ^_^lt gt^_^; #size-cells = ^_^lt&#0;gt^_^; timebase-frequency = ^_^lt󴉀gt^_^; L5: cpu@0 { device_type = "cpu"; clock-frequency = ^_^lt&#0;gt^_^; compatible = "sifive,rocket0", "riscv"; d-cache-block-size = ^_^lt gt^_^; d-cache-sets = ^_^lt@gt^_^; d-cache-size = ^_^ltကgt^_^; d-tlb-sets = ^_^lt gt^_^; d-tlb-size = ^_^lt gt^_^; i-cache-block-size = ^_^lt gt^_^; i-cache-sets = ^_^lt@gt^_^; i-cache-size = ^_^ltကgt^_^; i-tlb-sets = ^_^lt gt^_^; i-tlb-size = ^_^lt gt^_^; mmu-type = "riscv,sv39"; next-level-cache = <&L10>; reg = <0x0>; riscv,isa = "rv64imafdc"; status = "okay"; timebase-frequency = ^_^lt󴉀gt^_^; tlb-split; L3: interrupt-controller { #interrupt-cells = ^_^lt gt^_^; compatible = "riscv,cpu-intc"; interrupt-controller; }; }; }; L10: ram@80000000 { device_type = "memory"; reg = <0x0 0x80000000 0x0 0x40000000>; reg-names = "mem"; }; L14: soc { #address-cells = ^_^lt gt^_^; #size-cells = ^_^lt gt^_^; compatible = "freechips,rocketchip-unknown-soc", "simple-bus"; ranges; L1: clint@2000000 { compatible = "riscv,clint0"; interrupts-extended = <&L3 3 &L3 7>; reg = <0x2000000 0x10000>; reg-names = "control"; }; L2: debug-controller@0 { compatible = "sifive,debug-013", "riscv,debug-013"; interrupts-extended = <&L3 65535>; reg = <0x0 0x1000>; reg-names = "control"; }; L9: gpio@64002000 { #gpio-cells = ^_^lt gt^_^; #interrupt-cells = ^_^lt gt^_^; compatible = "sifive,gpio0"; gpio-controller; interrupt-controller; interrupt-parent = <&L0>; interrupts = <3 4 5 6 7 8>; reg = <0x64002000 0x1000>; reg-names = "control"; }; L0: interrupt-controller@c000000 { #interrupt-cells = ^_^lt gt^_^; compatible = "riscv,plic0"; interrupt-controller; interrupts-extended = <&L3 11 &L3 9>; reg = <0xc000000 0x4000000>; reg-names = "control"; riscv,max-priority = ^_^lt gt^_^; riscv,ndev = ^_^lt gt^_^; }; L6: rom@10000 { compatible = "sifive,maskrom0"; reg = <0x10000 0x2000>; reg-names = "mem"; }; L8: serial@64000000 { compatible = "sifive,uart0"; interrupt-parent = <&L0>; clocks = <&tlclk>; interrupts = ^_^lt gt^_^; reg = <0x64000000 0x1000>; reg-names = "control"; }; L7: spi@64001000 { #address-cells = ^_^lt gt^_^; #size-cells = ^_^lt&#0;gt^_^; compatible = "sifive,spi0"; interrupt-parent = <&L0>; interrupts = ^_^lt gt^_^; reg = <0x64001000 0x1000>; clocks = <&tlclk>; reg-names = "control"; L12: mmc@0 { compatible = "mmc-spi-slot"; disable-wp; reg = <0x0>; spi-max-frequency = ^_^lt gt^_^; voltage-ranges = <3300 3300>; }; }; tlclk: tlclk { #clock-cells = ^_^lt&#0;gt^_^; clock-frequency = ^_^lt gt^_^; clock-output-names = "tlclk"; compatible = "fixed-clock"; }; }; }; 

J'avais l'habitude d'analyser des fichiers ELF, mais maintenant je suis à nouveau convaincu par FDT (arbre de l'appareil plat): ces spécifications aimables sont écrites par de bonnes personnes attentionnées (Pourtant, ils analysent ensuite eux-mêmes!) et l'analyse de ces fichiers (au moins jusqu'à ce que vous ayez besoin de traiter des entrées non fiables) ne pose aucun problème particulier. Voici donc: au début du fichier, il y a une structure d'en-tête simple contenant le nombre magique 0xd00dfeed et quelques autres champs. Nous nous intéressons au décalage de "l'arbre plat" off_dt_struct et de la table de lignes off_dt_strings . En fait, vous devez également traiter off_mem_rsvmap , qui énumère les zones de mémoire qu'il off_mem_rsvmap mieux éviter. Je les ignore toujours (ils ne sont pas sur ma planche), mais ne répétez pas cela à la maison .


En principe, le traitement n'est pas particulièrement difficile: il suffit de marcher sur un arbre plat en respectant les jetons. Il existe trois jetons clés :


  • FDT_BEGIN_NODE - dans les données supplémentaires qui le suivent, vient le nom de l'élément de sous-arbre sous la forme d'une chaîne terminée par un caractère nul. Ajoutez simplement le nom à la pile
  • FDT_END_NODE - la sous-arborescence est terminée, supprimez l'élément de la pile
  • FDT_PROP - voici un peu plus compliqué: il est suivi d'une structure, suivi de len octets de données supplémentaires. Le nom de la «variable» se situe à l'offset nameoff dans la table des chaînes
     struct { uint32_t len; uint32_t nameoff; } 

Eh bien, en général, c'est tout: nous parcourons cette section, sans oublier d'observer l'alignement sur 4 octets. Oh oui, une mouche dans la pommade: les nombres en FDT sont au grand format endian, donc on fait une fonction simple


 static inline uint32_t be32(uint32_t x) { return (x << 24) | (x >> 24) | ((x & 0xff0000) >> 8) | ((x & 0xff00) << 8); } 

Par conséquent, dans riscv_entry première chose à faire est d'analyser FDT, et la partie de head.S qui est chargée de transférer le contrôle à riscv_entry ressemble à quelque chose comme


  .globl startup_32 #  --    ... startup_32: lla sp, boot_stack_top mv s0, a0 # s0, s1 -- callee-saved mv s1, a1 # ...  .bss #   jal _dl_start #      mv a0, s0 mv a1, s1 j riscv_entry 

Dans le registre a0 hart id nous est transmis (hart est quelque chose comme un flux matériel dans la terminologie RISC-V) - Je ne l'utilise pas encore, je devrais le comprendre dans un cas à un seul thread. En a1 chargeur de démarrage place un pointeur sur le FDT. Nous le passons à la fonction void riscv_entry(ulong hartid, uint8_t *fdt_address) .


Maintenant, avec l'avènement de la parsilka FDT dans mon code, la séquence de chargement de la carte est devenue comme ceci:


  • allumez le pouvoir
  • attendre la console U-boot
  • entrez des commandes pour préparer le FDT correct. En particulier, la /chosen/bootargs command /chosen/bootargs stocke la ligne de commande du noyau. Tout ce que je retire de FDT - plage RAM, adresse UART, ... - peut et doit être laissé tel quel
     run fdtsetup fdt set /chosen bootargs "console=ttyS0 btrace" 
  • à l'aide de la commande fdt addr , recherchez l'adresse de téléchargement FDT, si vous n'avez pas regardé

Et du côté gdb, la commande est ajoutée


  • -ex 'set $a1=0xfdtaddr'

Sortie d'informations sur l'écran


Il s'est avéré qu'en plus des insertions d'assembleur, il existe également des adresses de mémoire connues. Par exemple SCREEN_ADR (exactement comme ça, avec un D ), qui pointe vers la zone correspondant à ce qui est affiché à l'écran. Quand je suis tombé sur cela, j'ai simplement placé d'un geste large tout ce qui s'y réfère sous #if HAS_SCREEN , puis #if HAS_SCREEN débogué aveuglément pendant longtemps. Je pensais déjà manuellement une fois de temps en temps pour vider tout cela sur la console, mais j'ai remarqué que le même code douloureusement de nombreuses séquences d'échappement sortaient sur le port série. Il s'est avéré que tout avait déjà été écrit avant nous, il vous suffit de placer les définitions plus précisément - et le voici, l'interface familière (bien qu'en noir et blanc) dans la fenêtre minicom! (Pour le moment, HAS_SCREEN n'est pas utilisé du tout - je viens de démarrer le tableau dummy_con pour changer le code d'origine au minimum.)


Débogage sur QEMU


J'ai donc tout débogué sur un vrai tableau, et depuis un certain temps maintenant - même pas aveuglément. Mais tout ralentit sur JTAG - l'horreur! Eh bien, au final, tout devrait fonctionner sur du vrai matériel, mais ce serait bien de déboguer sur QEMU. Après un certain nombre d'expériences, quelque chose s'est avéré être une béquille, mais très similaire au travail avec une planche:


 $ qemu-system-riscv64 -M help Supported machines are: none empty machine sifive_e RISC-V Board compatible with SiFive E SDK sifive_u RISC-V Board compatible with SiFive U SDK spike_v1.10 RISC-V Spike Board (Privileged ISA v1.10) (default) spike_v1.9.1 RISC-V Spike Board (Privileged ISA v1.9.1) virt RISC-V VirtIO Board (Privileged ISA v1.10) 

Nous regardons quelles cartes QEMU est prête à émuler. Je suis intéressé par le matériel compatible sifive_u .


 $ qemu-system-riscv64 -M sifive_u,dumpdtb -m 1g # - QEMU      on --  strace   $ ls -l on -rw-rw-r-- 1 trosinenko trosinenko 1923  19 20:14 on $ dtc -I dtb < on > on.dts #   $ vim on.dts #  bootargs $ dtc < on.dts > on.dtb <stdout>: Warning (clocks_property): /soc/ethernet@100900fc:clocks: cell 0 is not a phandle reference <stdout>: Warning (clocks_property): /soc/ethernet@100900fc:clocks: cell 1 is not a phandle reference <stdout>: Warning (clocks_property): /soc/ethernet@100900fc:clocks: cell 2 is not a phandle reference <stdout>: Warning (interrupts_extended_property): /soc/interrupt-controller@c000000:interrupts-extended: cell 0 is not a phandle reference <stdout>: Warning (interrupts_extended_property): /soc/interrupt-controller@c000000:interrupts-extended: cell 2 is not a phandle reference <stdout>: Warning (interrupts_extended_property): /soc/clint@2000000:interrupts-extended: cell 0 is not a phandle reference <stdout>: Warning (interrupts_extended_property): /soc/clint@2000000:interrupts-extended: cell 2 is not a phandle reference 

Nous avons maintenant un blob d'arbre de périphérique «fixe». Sans changer la configuration de la VM (béquilles!), Lancez:


 qemu-system-riscv64 \ -M sifive_u -m 1g \ -serial stdio \ -s -S 

-serial stdio redirige le port série vers la console, car les séquences d'échappement seront activement utilisées. Les options -s -S augmentent respectivement gdbserver et créent une machine virtuelle à suspendre. Vous pouvez télécharger le code à l'aide du loader , mais vous devez ensuite redémarrer QEMU à chaque fois.


Vous pouvez vous connecter en utilisant


 riscv64-unknown-elf-gdb \ -ex 'target remote 127.0.0.1:1234' \ -ex 'restore /path/to/on.dtb binary 0x80100000' \ -ex 'restore /path/to/memtest_shared.bin binary 0x80020000' \ -ex 'add-symbol-file memtest_shared 0x80100000' \ -ex 'set $a1=0x80020000' \ -ex 'set $pc=0x80100000' 

En conséquence, tout fonctionne plus que intelligemment!


Principe général de travail


, , , Memtest86+ btrace , , ( , QEMU):


mode btrace


, , memtest . , (, trap): , , QEMU - ! «» Illegal instruction , . mcause (?), — mepc (?), — mtval ( ?), .


Instruction illégale


, :


head.S:


 #       #   = 0 ---   ,   #  ,    ,     ... lla t1, _trap_entry csrw mtvec, t1 # ... _trap_entry: csrr a0, mcause csrr a1, mepc csrr a2, mtval jal riscv_trap_entry 

, calling convention, . memtest, HiFive_U-Boot, Volume II :


arch.c:


 static const char *errors[] = { "Instruction address misaligned", "Instruction access fault", "Illegal instruction", "Breakpoint", "Load address misaligned", "Load access fault", "Store/AMO address misaligned", "Store/AMO access fault", ^_^quot quot^_^, ^_^quot quot^_^, ^_^quot quot^_^, ^_^quot quot^_^, "Instruction page fault", "Load page fault", ^_^quot quot^_^, "Store/AMO page fault", }; void riscv_trap_entry(ulong cause, ulong epc, ulong tval) { char buf[32]; cprint(12, 0, "EXCP: "); if (cause < sizeof(errors) / sizeof(errors[0])) { cprint(12, 8, errors[cause]); } else { itoa(buf, cause); cprint(12, 8, buf); } cprint(13, 0, "PC: "); hprint3(13, 8, epc, 8); cprint(14, 0, "Addr: "); hprint3(14, 8, tval, 8); HALT(); } 

— « » . , «» , , , .


: . , memtest : : « , , . ». : do_test main.c 2, ( ), — «» , memtest. , run_at , memtest _start _end ( «» ), - spinlock' goto *addr; . , , «» , «».


, bss_dl_start , riscv_entry , trap entry. , : L1I-, . , fence.i .


, Memtest86+ — , barrier_s . , . , , .



, : . : . : , - (Own Address, ) . , , . . - . , x86 , , uint64_t 0x80000002 . , : , load/store x86 , — . , QEMU , « , ».


, , — unaligned access ..


, , RocketChip, — QEMU, , , RocketChip — unaligned access trap, QEMU « ».
«misaligned» ,


Changed description of misaligned load and store behavior. The specification now allows visible misaligned address traps in execution environment interfaces, rather than just mandating invisible handling of misaligned loads and stores in user mode. Also, now allows access exceptions to be reported for misaligned accesses (including atomics) that should not be emulated.

, , — , user-mode code , . . , , . , — - machine mode . , rdtsc (x86) rdtime (rv64), trap, . , , memory-mapped .


: , low_test_addr ( ), , fdt . , , low_test_addr , , 2 high_test_adr … , — : head.S initial_load_addr , riscv_entry move_to_correct_addr :


 static void move_to_correct_addr(void) { uintptr_t cur_start = (uintptr_t)&_start; uintptr_t cur_end = (uintptr_t)&_end; if (cur_start == low_test_addr || cur_start == high_test_adr) { //  ,     return; } if (cur_start == initial_load_addr && (cur_start - low_test_addr) < (cur_end - cur_start) ) { //   " ":   , //           //     ,    ,   //     ... serial_echo_print("FIRST STARTUP RELOCATION...\n"); void *temp_addr = (((uintptr_t)&_end >> 12) + 1) << 12; run_at(temp_addr, 0); } else { // ,    --- ,  . serial_echo_print("FINAL STARTUP RELOCATION...\n"); run_at(low_test_addr, 0); } } 

, — , memtest , RAM - . RISC-V , v->plim_lower .


, «» , -, — test.c ulong ( unsigneg long ), 32- x86 uint32_t , « 64 » uint64_t . «!!! Good: ffffffff Real: ffffffff Bad bits: 00000000». ? - -1, 32 1. , , 0… , : , ulong ( uint32_t ), ( uintptr_t ). , . , uint64_t 4. RISC-V , C, , — UB. memtest UBSan. , , UBSan trap-on-error JTAG.



, memtest - , , U-Boot.


: mkimage U-Boot Linux :


 mkimage -A riscv -O linux -T kernel -C none \ -a 0x80000000 -e 0x80000000 \ -n memtest -d memtest.bin memtest.uboot 

SD-


 run mmcsetup; run fdtsetup; fdt set /chosen bootargs "console=ttyS0"; fatload mmc 0:1 82000000 memtest.uboot; bootm fdt; bootm 82000000 - ${fdtaddr} 

( , run — ).


: FDT: 0xbffb7c80 . , : ffffffff , . , ( ), : HiFive_U-Boot :


  theKernel(machid, (unsigned long)images->ft_addr); 

,


  void (*theKernel)(int arch, uint params); 

, , , , 32 , head.S :


  li t0, 0xffffffffL and a1, a1, t0 


, , - , , , :


  • x86. — review
  • SMP RISC-V
  • arch/ -
  • test.c RISC-V ( -O0 !)

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


All Articles