Teil 6: MemTest86 + nach RISC-V portieren


Wahrscheinlich müssen nur wenige IT-Mitarbeiter erklären, was Memtest86 + ist - vielleicht ist es bereits mehr oder weniger zum Standard beim Testen von RAM auf einem PC geworden. Als ich in einem der vorherigen Teile auf eine kaputte Speicherleiste stieß, die mit dem Board geliefert wurde, schien dies (zusammen mit einem DDR2-fähigen Netbook) eine offensichtliche Lösung zu sein. Eine andere Frage ist, dass dort der instabile Betrieb des Systems im Prinzip mit bloßem Auge sichtbar war. In schwierigeren Fällen habe ich gehört, dass dieses Tool zusätzlich zum banalen „Abhören“ von Speicherzellen ins Unendliche einige spezielle Datenmuster verwendet, bei denen Fehler im DDR-Betrieb mit größerer Wahrscheinlichkeit erkannt werden. Im Allgemeinen eine wunderbare Sache, es ist schade, dass auch im Namen heißt: 86 - "Nur für x86-kompatible Systeme." Oder nicht?


Unter dem Schnitt sehen Sie meine Versuche, MemTest86 + v5.1 auf RISC-V und die Zwischensumme zu portieren. Spoiler: Es bewegt sich!


HAFTUNGSAUSSCHLUSS: Das resultierende Projekt wurde von mir auf einer bestimmten RocketChip-Baugruppe auf einer bestimmten Platine nur minimal getestet. Genauigkeit und Sicherheit (insbesondere bei anderen Systemen) werden nicht garantiert. Benutzung auf eigenes Risiko. Insbesondere werden momentan reservierte Speicherbereiche in keiner Weise verarbeitet, wenn sie in den RAM-Bereich fallen.


Wie ich bereits sagte, habe ich vor nicht allzu langer Zeit ein Motherboard mit Cyclone IV auf AliExpress gekauft, aber der Speicher darin war fehlerhaft. Glücklicherweise war eines der wichtigen Merkmale dieses Boards die Verwendung herkömmlicher DDR2-SO-DIMM-Module - genau wie bei meinem alten Netbook. Trotzdem wäre es interessant, sozusagen eine selbst gehostete Lösung zum Testen von Speichermodulen (und tatsächlich auch des Controllers) zu bekommen. Die Aussicht, meine Fehler unter den Bedingungen eines schlechten Gedächtnisses zu beseitigen, war irgendwie überhaupt nicht erfreulich. Vor allem, weil ich nicht auf eine schnelle Lösung gehofft hatte und mich mental darauf vorbereitete, das komplette Umschreiben in einem anderen Assembler auf unbestimmte Zeit zu verschieben, schlug ich einen Wikipedia-Artikel über Memtest86 + auf und sah plötzlich „Geschrieben in: C und Assembler“ auf der Karte. Hmm, das heißt, er ist zwar "... 86", aber nicht ganz in Assembler geschrieben? Das ist ermutigend. Es bleibt nur die Beziehung zu verstehen.


Gehen Sie also zu memtest.org und laden Sie die Version 5.01 unter der GPL2 herunter. Zur Erleichterung der Entwicklung habe ich es auf GitHub neu geladen . Zum Glück werden wir direkt im Quellarchiv von der Datei README.background mit dem Titel begrüßt


Die Anatomie und Physiologie von Memtest86-SMP

Es erklärt ausführlich (und sogar mit Bildern in Form von ASCII-Kunst) die Funktionsweise des Codes auf hoher Ebene. Ganz am Anfang des Dokuments sehen wir ein setup.o Layout , bestehend aus bootsect.o , setup.o , head.o und einigen memtest_shared . Es ist leicht zu erkennen, dass diese drei Objektdateien aus den entsprechenden Assembler-Quellen stammen. Alles andere ist auf den ersten Blick in C geschrieben! Nicht schlecht, nicht schlecht ...


Infolgedessen habe ich das Makefile nach Makefile.arch kopiert und angefangen, alles neu zu schreiben und zu versuchen, das herauszuwerfen, was nicht übereinstimmt. Zuallererst brauchte ich natürlich eine Toolchain für RISC-V, die zum Glück seit den vorherigen Experimenten immer noch bei mir ist. Zuerst dachte ich daran, einen Port für die 32-Bit-Architektur zu riscv64- , aber dann fiel mir ein, dass ein 64-Bit-Prozessor auf das Board hochgeladen wurde, und ich hatte die riscv64- mit dem Präfix riscv64- .


Lyrischer Exkurs: Natürlich war das erste, was die Frage der Kompatibilität von 32- und 64-Bit-Code untersuchte. Infolgedessen lautet die Spezifikation für den nichtprivilegierten Teil der ISA (Instruction Set Architecture) in Abschnitt 1.3 RISC-V ISA Overview :


Der Hauptvorteil der expliziten Trennung von Basis-ISAs besteht darin, dass jede Basis-ISA für ihre Anforderungen optimiert werden kann, ohne dass alle für andere Basis-ISAs erforderlichen Vorgänge unterstützt werden müssen. Beispielsweise kann RV64I Anweisungen und CSRs auslassen, die nur für die Verarbeitung der engeren Register in RV32I erforderlich sind. Die RV32I-Optionen können Codierungsspeicher verwenden, der ansonsten für Anweisungen reserviert ist, die nur von breiteren Adressraumvarianten benötigt werden.

Ich möchte auch darauf hinweisen, dass die Toolchain mit dem Präfix riscv64- wahrscheinlich problemlos 32-Bit-Code sammelt, wenn die Zielarchitektur richtig ausgewählt ist - dazu später mehr.


Beim Portieren ist es sinnvoll, die folgenden Dokumente griffbereit zu halten:



Setup erstellen


Lassen Sie uns zunächst zustimmen: Ich möchte einen Port erhalten, der für die weitere Portierung auf andere Architekturen als x86 und RISC-V geeignet ist. Ich schlage auch vor, Bootdisketten und andere x86-Besonderheiten aus dem plattformübergreifenden Build zu werfen.


Was wir letztendlich haben: Es gibt drei Assembler-Dateien: bootsect.S , setup.S und head.S Die ersten beiden werden nur beim Start benötigt und die dritte wird später beim Umzug in einen anderen Speicherbereich benötigt. Tatsache ist, dass, um den Speicher "unter sich" zu testen, der Testcode zuerst an einen neuen Ort verschoben werden muss. Sich-Dateien werden in ELF gesammelt, aus denen dann Codeabschnitte, Daten usw. entnommen werden. Darüber hinaus wird es in Form von PIC (Position Independent Code) gesammelt - zunächst war ich sogar überrascht: Obwohl der Code freistehend ist (dh ohne Kernel, libc usw.), verwendet er solche erweiterten Funktionen.


Außerdem werden die Parameter, die die Architektur definieren, regelmäßig im Makefile -march=i486 : -march=i486 , -m32 und dergleichen. Ich muss so etwas schreiben, und dann wie ein Trottel . Die Situation mit der RISC-V-Architektur rv32 rv64 so aus: Es gibt rv32 und rv64 (zum Beispiel gibt es immer noch die am stärksten verkürzten eingebetteten und für die Zukunft reservierten rv128, aber wir sind nicht sehr daran interessiert), und der ISA-Name wird durch Zuweisen von Buchstaben zu diesem Präfix gebildet erweiterungen: i - der grundlegende ganzzahlige Befehlssatz, m - ganzzahlige Multiplikation und Division, ... Natürlich würde ich gerne rv64i , aber Memtest86 wird ohne Multiplikation kaum leicht auf die Architektur portiert werden können. Es ist richtig, dass der Compiler einfach Funktionsaufrufe anstelle von „problematischen“ Anweisungen generiert. Es besteht jedoch die Gefahr, dass die Leistung stark reduziert bleibt (ganz zu schweigen davon, dass diese Funktionen irgendwo geschrieben oder ausgeführt werden müssen).


Sie benötigen auch die Linie ABI. Grundsätzlich sind die Grundlagen der Aufrufkonvention bereits in dem angegebenen Volume I im "Handbuch für RISC-V-Assembly-Programmierer" beschrieben. Ich werde also einfach so etwas tun


 $ 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. 

Und ohne nachzudenken, nehme ich lp64 . Mit Blick auf die lp64f werde ich sagen, dass mit dieser ABI die Header-Dateien aus der Standardbibliothek nicht funktionierten, also habe ich lp64f und ARCH auf rv64imfrv64imf . Ohne Panik habe ich nicht vor, Gleitkommazahlen in meinem Port zu verwenden.


Da ich irgendwie nicht in plattformübergreifende Linker-Skripte head.S - und daher die Schlüssel für ld nicht sofort finden konnte, beschloss ich, mit der Assembler-Datei head.S und mich mit head.S an die restlichen Funktionen zu memtest_shared.arch.lds . Ich habe daraufhin einen Hinweis auf das Ausgabeformat und die Architektur ausgegeben (schließlich ist es einfacher, es von einer Variablen im Makefile zu ändern) und am Ende vorübergehend DISCARD , da DISCARD nicht herausfinden konnte, welche spezifischen Abschnitte der Debugging-Informationen ich benötigte. (Vorausschauend: feine Debugging-Informationen, aber .rela musste hinzugefügt werden) Generell betonte die x86-Version die Notwendigkeit, in 64k zu passen - ich hoffe, dass dies nur irgendwie mit den Funktionen des Real-Modus zusammenhängt und uns in RISC-V nicht betrifft . Infolgedessen wird das gemeinsam genutzte Objekt mit dem PIC wie im Original gesammelt, und der Code und die Daten, die in den Speicher geladen werden, werden daraus herausgebissen.


Wir sammeln ... und die Kompilierung fällt auf die erste reloc.c Datei - sie stammt anscheinend aus ld-linux.so und ist für die Unterstützung der globalen ld-linux.so usw. verantwortlich. gemäß Aufrufkonventionen für x86. Es stellte sich heraus, dass es erforderlich war, mit Assembler-Einfügungen direkt mit Registern zu arbeiten. Aber wir sind bei RISC-V - es wurde ursprünglich entwickelt, um PIC zu unterstützen, also reloc.c , reloc.c zu werfen. Außerdem gab es immer noch Beilagen, manchmal ziemlich lang. Zum Glück befanden sie sich entweder im Testcode unmittelbar nach dem auskommentierten C-Code, den sie optimieren (daraus habe ich wieder vollwertige Codeteile gemacht, die von der Präprozessor-Direktive umgeschaltet wurden) oder etwas plattformabhängiges, ohne das ich im Extremfall (wahrscheinlich) kann tun (wie das Ein- / Ausschalten des Cache, das Subtrahieren der CPUID usw.). Schließlich gab es einige Dinge wie den rdtsc Aufruf, den ich rdtsc ohne große Probleme in einen plattformabhängigen Header gesteckt und gemäß der Dokumentation zu RISC-V implementiert habe.


Als Ergebnis haben wir das Verzeichnis arch/i386 , in das eine große Menge von PCI-Unterstützungscode verschoben wurde, Informationen von den Chipsätzen gelesen wurden, plattformspezifische Definitionen von Speicheradressen usw. Dort blieb auch der Anfang der Funktion test_start , der der Einstiegspunkt von setup.S zum C-Code ist. Wie lang, kurz, aber alles setup.S , was möglich ist, und alles realisieren, was unter RISC-V nicht setup.S (wie setup.S und der Code für die Arbeit mit serielle Schnittstelle in der SiFive-Implementierung) habe ich das arch/riscv , mit dem alles mehr oder weniger kompiliert wurde.


Hier muss ich klarstellen, dass die Experimente selbst teilweise vor dem Schreiben des Artikels durchgeführt wurden, sodass eine bestimmte Abfolge von Aktionen eine gewisse Menge an „künstlerischer Fiktion“ enthalten kann. Ich versuche jedoch, die Präsentation zumindest so zu gestalten, dass sie auf jeden Fall einen der möglichen Wege darstellt (ich bin Programmierer, daran erinnere ich mich) . Also mal sehen, wie man alles anfängt.


Laufen auf Eisen


Seit früheren Experimenten habe ich immer noch einen staubigen "Stand" vom Raspberry Pi, der mit dem Debugboard verbunden ist. Die Drähte versorgen UART, JTAG und einen Adapter mit einer SD-Karte. Ein bestimmter RV64-Prozessor mit einem DDR2-Controller ist in den Konfigurationsspeicher eingenäht. Wie in früheren Zeiten schalte ich die "Himbeere" ein, öffne zwei SSH-Sitzungen davor, von denen eine den 3333-TCP-Port für die Verbindung von GDB mit OpenOCD weiterleitet. In einer Sitzung starte ich minicom, um nach UART zu suchen, in einer anderen - openocd, um über JTAG vom Host aus zu debuggen. Ich schalte die Stromversorgung des Boards ein - und Meldungen in der Konsole darüber, wie es Daten vom SD lädt, liefen.


Jetzt können Sie den Befehl ausführen:


 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' 

Die Optionen -ex gdb an, so zu tun, als hätte der Benutzer die folgenden Befehle über die Konsole eingegeben:


  • der erste stellt eine Verbindung mit OpenOCD her
  • Die zweite kopiert den Inhalt der angegebenen Hostdatei an die angegebene Adresse
  • der dritte erklärt gdb, dass Informationen über den Quellcode aus dieser Datei entnommen werden müssen, unter Berücksichtigung der Tatsache, dass sie unter dieser Adresse heruntergeladen wurden (und nicht, was darin selbst angegeben ist).
    • Hinweis: Wir nehmen die Zeichen aus der ELF-Datei und laden die Binärdatei "raw"
  • Schließlich übersetzt der vierte zwangsweise den aktuellen Befehlszeiger in unseren Code

Leider läuft nicht alles absolut reibungslos und die Codezeilen im Debugger werden zwar korrekt angezeigt, aber in allen globalen Variablen - Nullen. Wenn wir in gdb einen Befehl in der Form p &global_var ausführen, sehen wir die Adresse leider in Übereinstimmung mit der ursprünglichen Download-Adresse (ich habe 0x0 ), die nicht mit add-symbol-file . Als Krücke, aber eine sehr einfache Lösung, habe ich einfach 0x80010000 manuell zur angegebenen Adresse hinzugefügt und den Inhalt des Speichers über x/x 0xADDR . Tatsächlich könnte im Linkerskript vorübergehend die richtige Startadresse angegeben werden, die im Moment mit der Downloadadresse in dieser Testkonfiguration übereinstimmt.


Merkmale der Verlagerung auf moderne Architekturen


Nun, wie man den Code herunterlädt, haben wir herausgefunden - wir fangen an. Das ____ funktioniert nicht. Das schrittweise Debuggen zeigt, dass wir während des Betriebs der switch_to_main_stack Funktion switch_to_main_stack - es scheint, dass immer noch versucht wird, den nicht zusammenhängenden Wert der Adresse des Symbols zu verwenden, das dem Arbeitsstapel entspricht.


Der erste Teil der Dokumentation enthält Informationen zu verschiedenen Pseudobefehlen und ihrer Arbeit mit PIC on und off:


Einige RISC-V-Pseudobefehle


Wie Sie sehen können, ist das allgemeine Prinzip, dass die Adressen im Speicher vom aktuellen Befehl an gezählt werden, wobei der erste den oberen Rand des Offsets add und der nächste die niederwertigen Bits poliert. Es hilft kaum, eine globale Variable wie zu deklarieren


 struct vars * const v = &variables; 

Daher nehmen wir die RISC-V ELF psABI-Dokumentation mit Beschreibungen der Umzugstypen und schreiben den plattformspezifischen Teil für reloc.c . Hierbei ist zu beachten, dass die Originaldatei anscheinend aus plattformübergreifendem Code stammt. Dort werden, anstatt eine bestimmte Bittiefe anzugeben, Makros vom ElfW(Addr) , die in Elf32_Addr oder Elf64_Addr . Aus diesem Grund fügen wir sie nicht überall dort hinzu, wo sie nicht im allgemeinen Code (sowie im Code arch/riscv/reloc.inc.c Schließlich ist es für RISC-V nicht sinnvoll, an eine bestimmte Bittiefe gebunden zu sein, wo dies nicht der Fall ist erforderlich).


Infolgedessen begann switch_to_main_stack zu vergehen (natürlich nicht ohne plattformabhängige Assembler-Anweisungen). Der Debugger zeigt globale Variablen immer noch schief an. Na ja, okay :(


Hardware-Definition


Natürlich wäre es für Tests möglich, hartcodierte Konstanten anstelle des weggeworfenen Geräte-Definitionscodes zu verwenden, aber für jede bestimmte Prozessorbaugruppe ist das Neuerstellen von memtest für die Standards meiner Anwendung sogar zu kostspielig. Deshalb werden wir "als ernsthafte Erwachsene" auftreten. Glücklicherweise ist es in RISC-V (und wahrscheinlich in den meisten modernen Architekturen) üblich, dass der Bootloader einen Code an das Device Tree Blob übergibt , das eine kompilierte Version der DTS-Beschreibung wie folgt ist:


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"; }; }; }; 

Früher habe ich ELF-Dateien analysiert, aber jetzt bin ich wieder von FDT (Flat Device Tree) überzeugt: Diese freundlichen Spezifikationen wurden von fürsorglichen Menschen geschrieben (Trotzdem analysieren sie es dann selbst!) Das Parsen solcher Dateien (zumindest bis Sie nicht vertrauenswürdige Eingaben verarbeiten müssen) wirft keine besonderen Probleme auf. Also hier: Am Anfang der Datei befindet sich eine einfache Header-Struktur mit der magischen Nummer 0xd00dfeed und ein paar weiteren Feldern. Uns interessiert der Offset des "flat tree" off_dt_struct und der off_dt_strings . Eigentlich müssen Sie auch off_mem_rsvmap , wodurch Speicherbereiche off_mem_rsvmap , die am besten vermieden werden. Ich ignoriere sie immer noch (sie befinden sich nicht auf meinem Board), wiederhole dies aber zu Hause nicht .


Grundsätzlich ist die Verarbeitung nicht besonders schwierig: Sie müssen nur auf einem flachen Baum gemäß den Token gehen. Es gibt drei Schlüsselmarken :


  • FDT_BEGIN_NODE - In den zusätzlichen Daten, die unmittelbar darauf folgen, steht der Name des Unterbaumelements in Form einer nullterminierten Zeichenfolge. Fügen Sie einfach den Namen zum Stapel hinzu
  • FDT_END_NODE - der Teilbaum ist vorbei, entferne das Element vom Stapel
  • FDT_PROP - hier ist ein wenig kniffliger: Es folgt eine Struktur, gefolgt von len Bytes zusätzlicher Daten. Der Name der "Variablen" liegt am Offset nameoff in der String-Tabelle
     struct { uint32_t len; uint32_t nameoff; } 

Nun, im Allgemeinen ist das alles: Wir gehen diesen Abschnitt durch und vergessen nicht, die Ausrichtung um 4 Byte zu beobachten. Oh ja, eine Fliege in der Salbe: Die Zahlen in FDT sind im Big-Endian-Format, also machen wir eine einfache Funktion


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

Daher müssen Sie in riscv_entry zunächst FDT und den Teil von head.S , der für die Übertragung der Steuerung auf riscv_entry verantwortlich ist


  .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 

Im Register a0 hart id an uns übergeben (hart ist so etwas wie ein Hardware-Stream in der RISC-V-Terminologie) - ich verwende es noch nicht, ich müsste es in einem Single-Thread-Fall herausfinden. In a1 Bootloader einen Zeiger auf das FDT. Wir übergeben es an die Funktion void riscv_entry(ulong hartid, uint8_t *fdt_address) .


Mit dem Aufkommen der FDT-Parsilka in meinem Code wurde die Ladereihenfolge des Boards folgendermaßen:


  • Schalten Sie den Strom ein
  • Warten Sie auf die U-Boot-Konsole
  • Geben Sie Befehle ein, um die richtige FDT vorzubereiten. Insbesondere speichert das /chosen/bootargs command /chosen/bootargs die Kernel-Befehlszeile. Alles andere, was ich von FDT nehme - RAM-Bereich, UART-Adresse, ... - kann und soll so belassen werden, wie es ist
     run fdtsetup fdt set /chosen bootargs "console=ttyS0 btrace" 
  • fdt addr Befehl fdt addr die FDT-Download-Adresse, falls Sie nicht nachgeschaut haben

Und von GDB-Seite wird der Befehl hinzugefügt


  • -ex 'set $a1=0xfdtaddr'

Informationsausgabe auf dem Bildschirm


Wie sich herausstellte, gibt es neben Assembler-Inserts auch bekannte Speicheradressen. Zum Beispiel SCREEN_ADR (genau so, mit einem D ), das auf den Bereich zeigt, der dem entspricht, was auf dem Bildschirm angezeigt wird. Als ich darauf stieß, platzierte ich einfach mit einer breiten Geste alles, was sich darauf bezieht, unter #if HAS_SCREEN und #if HAS_SCREEN lange Zeit blind. Ich dachte schon einmal manuell, dies alles auf der Konsole abzulegen, aber dann bemerkte ich, dass derselbe Code schmerzhaft viele Escape-Sequenzen an die serielle Schnittstelle ausgibt. Es stellte sich heraus, dass bereits alles vor uns geschrieben wurde. Sie müssen nur die Definitionen genauer platzieren - und hier ist es, die vertraute Oberfläche (wenn auch schwarz und weiß) im Minicom-Fenster! (Im Moment wird HAS_SCREEN überhaupt nicht verwendet - ich habe gerade das dummy_con Array gestartet, um den ursprünglichen Code mindestens zu ändern.)


Debuggen auf QEMU


Also habe ich alles auf einem echten Board getestet, und das schon seit einiger Zeit - nicht einmal blind. Aber alles verlangsamt sich auf JTAG - Horror! Nun, am Ende sollte alles auf echter Hardware funktionieren, aber es wäre schön, auf QEMU zu debuggen. Nach einer bestimmten Anzahl von Experimenten stellte sich heraus, dass es sich um eine Krücke handelte, die jedoch der Arbeit mit einem Brett sehr ähnlich war:


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

Wir schauen uns an, welche Boards QEMU emulieren kann. Ich interessiere mich für sifive_u kompatible Hardware.


 $ 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 

Jetzt haben wir einen "festen" Gerätebaum-Blob. Führen Sie Folgendes aus, ohne die Konfiguration der VM zu ändern (Krücken!):


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

-serial stdio leitet die serielle Schnittstelle an die Konsole weiter, da Escape-Sequenzen aktiv verwendet werden. Mit den Optionen -s -S gdbserver aktiviert und eine VM erstellt, die angehalten werden soll. Sie können den Code mit dem loader herunterladen, müssen dann aber jedes Mal QEMU neu starten.


Sie können eine Verbindung herstellen mit


 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' 

Infolgedessen funktioniert alles mehr als schlau!


Allgemeines Prinzip der Arbeit


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


btrace mode


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


Illegal instruction


, :


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/de484026/


All Articles