8.0 KiB
RISC-V Emulator
Dokumentation für den Emulator
- Das Programm kann mit
./riscv-emulator <ASSEMBLY.txt>gestartet werden. - Dabei ist <ASSEMBLY.txt> ein absoluter oder relativer Pfad zum Quellcode eines RV32I (RISC-V) Programmes. Wie die Syntax aussehen muss, ist in der Dokumentation für den Assembly Code beschrieben.
- Der Emulator führt dann Zeile für Zeile den Quellcode aus.
- Dabei wird nach jeder berechneten Zeile diese ausgegeben und nach weiteren Anweisungen gefragt.
Es gibt folgende Anweisungen:
Befehl Abkürzung für Aktion s sprint Führt den Sourcecode bis zum Ende aus l line Führe die nächste Zeile des Sourcecodes aus e end Beende den Emulator direkt m memory Gibt einen Memorydump in der Konsole aus r register Gibt einen Registerdump in der Konsole aus - Am Ende des Sourcecodes stehen nur noch
e,m,rzur Verfügung. - Der Emulator wurde von und für Linux implementiert. Das Verhalten auf Windows wurde nicht getestet und kann undefiniert sein.
- Es wurden folgende angeforderte Befehle implementiert:
- add
- sub
- and
- or
- xor
- addi
- andi
- ori
- lw
- sw
- beq
- bne
- jal
- jalr
Folgende Befehle sind zusätzlich im Hinblick auf die Beispiel Programme implementiert worden: - j
- slli
- Die Beispielprogramme sind im entsprechenden Verzeichnis.
Dokumentation für den Assembly Code
- Der Assembly Code Parser wurde auf Grundlage des Cheatsheets von Project F implementiert und stellen die Syntax einer RISC-V (RV32I) Architektur dar.
- Die Syntax hat damit folgender Struktur zu folgen:
<Befehl> <Register/Label>, [Register/Label/Immediate(Register)], [Register/Label/Immediate] - Es gilt folgende Legende:
<>: Verpflichtend
/: Entweder oder
[]: Optional - Leerzeichen zwischen z.B. Register und Komma (
x5 , x3) müssen vermieden werden und führen zu undefiniertem Verhalten. - Leerzeilen sind erlaubt.
- Kommentare sind mit
#beginnend, auch inline, erlaubt. - Der Assemblycode muss in einer
.txtDatei gespeichert werden. - Speziell für Beispielprogramm 8: In diesem wird in Zeile 12 der Wert des Pointers auf das Array um 4 erhöht, um somit zum nächsten Element zu springen. Aufgrund der Funktionsweise dieses Emulators ist das nicht nötig und der Wert braucht lediglich um 1 erhöht zu werden. In den beigefügten Beispielprogrammen ist das bereits berücksichtigt.
Funktionsweise
- Der Emulator ist Objektorientiert programmiert und implementiert die einzelnen Komponenten in Klassen.
- Folgende Klassen sind mit folgenden Komponenten implementiert:
| Klasse | Komponente | Funktion |
|---|---|---|
| ALU | ALU | Die haupt Rechneneinheit, in der die Berechnungen stattfinden. |
| Memory | RAM | Speichert den Inhalt des RAM in einem Vektor. |
| Register | Register | Speichert den Inhalt der Register in einem Vektor. |
| Manager | Program counter | Sorgt für die schrittweise Bearbeitung des Programmes und kombiniert die Nutzereingaben. |
| ProgramLoader | Parst die einzelnen Codezeilen und indexiert das Programm nach Labeln |
- zusätzlich gibt es folgende Dateien:
| Datei | Funktion |
|---|---|
| main.cpp | Der Emulator-Starter. Startet die Indexierung und die Emulation. |
Der Emulator arbeitet nach folgender Funktionsweise:
Anstatt eines virtuellen program counters wird dieser als Zeilennummern implementiert. Hierbei wird die Funktionsweise des Einlesen von Dateien in der C++ standard library genutzt. In dieser wird in dem file Objekt die aktuelle Position des Lese/ Schreibkopfes genau gespeichert, Zeile + aktuelles Zeichen. Wird also der PC um 4 erhöht (es wird zum nächsten Befehl gesprungen), wird lediglich der Lesekopf um eine Zeile nach vorne verschoben. Für Sprünge zu Labels kann der Lesekopf direkt auf eine angegebene Position gesetzt werden.
- Der Manager startet eine Indexierung der Datei in ProgramLoader. Dabei werden die Positionen, an denen der Lesekopf auf ein Label stößt, in einer Map gespeichert.
- Nachdem der Lesekopf wieder auf den Anfang der Datei gesetzt wurde, wird der Quellcode, vom Manager gesteuert, Zeile für Zeile bis zum Ende der Datei im ProgramLoader eingelesen.
- Dieser wandelt den Befehl und seine Argumente in einen Vektor um, damit die ALU später einheitlich auf alle Daten zugreifen kann.
- Die ALU beginnt nun mit dem Aufbereiten der Argumente. Dabei werden führende
xvon Registern entfernt und Labels sowie immediate values erkannt. - Nun wird mithilfe eines großen (unschönen) if/else Block zwischen den verschiedenen Befehlen unterschieden und diese
dann ausgeführt. Dabei kann mithilfe von Getter- und Setter-Methoden der Inhalt des RAM und der Register gelesen und
verändert werden.
Sprünge werden mithilfe der in Schritt 1 erstellen Map durchgeführt werden. Eine Angabe der Sprungweite durch einen immediate value wird mithilfe eines einfachen for-loops mit dem mehrmaligen Springen in die nächste Zeile des Quellcodes
bewerkstelligt. Eine einfache Addition der Zahl auf die aktuelle Position des Lesekopfes ist nicht möglich, da dessen Position wie oben erwähnt auch das Zeilenoffset enthält.
Aufgrund der Funktionsweise des Emulators kann dieser nicht mit sehr großen Assembly Dateien umgehen. Das liegt daran, dass Sprungadressen nicht als program counter sondern als offset in der jeweiligen Quellcodedatei gespeichert werden. Bei sehr großen Dateien könnte das integer Limit überschritten werden.
Kompilierung
Der Emulator wurde mithilfe von JetBrains CLion geschrieben. Für einen einfachen Kompilierprozess, code insights und weitere Hilfestellungen wird die Verwendung (von zumindest einer C++ fähigen IDE) empfohlen.
Kompilierung in CLion:
- Beim öffnen des Projektordners sollte Clion automatisch das CMake buildsystem erkennen, andernfalls nach der Initialisierung fragen.
- Nachdem CLion das Projekt ge-indext hat, ist das Kompilieren des Emulators in der rechten, oberen Programmecke mit dem Hammer Symbol möglich.
- Der fertige Emulator ist nun in, je nach build mode,
./cmake-build-(debug/release)zu finden.
Kompilierung mit CMake
Für die Kompilierung wird CMake benötigt. Das ist ein buildsystem für C/C++ und übernimmt das Kompilieren von großen
Projekten mit vielen Quellcodedateien. Evtl. muss cmake manuell installiert werden, falls es nicht bereits vorhanden
ist.
Hierfür wird das cmake Paket benötigt. make ist in den meisten Fällen bereits vorinstalliert.
- Im Projektordner einen build Ordner erstellen:
mkdir build && cd build - CMake konfigurieren:
cmake .. - Projekt kompilieren:
make
Kompilierung mit G++ (nicht empfohlen)
Bei der manuellen Kompilierung mit g++ müssen alle Quellcodedateien manuell angegeben werden. Aufgrund der Unübersichtlichkeit und Fehleranfälligkeit ist dies nicht empfohlen!
- Im Projektordner einen build Ordner erstellen:
mkdir build && cd build - Projekt kompilieren:
g++ main.cpp Manager.cpp ProgramLoader.cpp ./components/Alu.cpp ./components/Memory.cpp ./components/Register.cpp
J. Anders @HS Kempten für VL Rechnerarchitektur, 2025