(Gott, wer hat .iris so programmiert, dass es nicht committet werden kann?)
Ein textbasiertes RPG, geschrieben in RISC-V 32-Bit-Assembler, simuliert über QEMU.
"Das Universum hat entschieden, dass ich nichts Besseres tun soll, wie vielleicht ein Leben zu bekommen."
Abhängigkeiten
riscv32-elf-as— RISC-V-Assemblerriscv32-elf-ld— RISC-V-Linkerqemu-riscv32— RISC-V-Benutzermodus-Emulator
Auf Arch Linux:
sudo pacman -S riscv64-elf-binutils qemu-user
Bauen & Ausführen
make build # assemblieren und zu main.bin linken
make run # bauen + ausführen über qemu-riscv32
make debug # ausführen mit -strace zur Inspektion von Syscalls
Projektstruktur
Extremis/
├── main.s # Einstiegspunkt (_start), ruft run_chapter_1 auf
├── src/
│ ├── include/
│ │ └── function.s # startF / endF Stack-Frame-Makros
│ ├── engine/
│ │ ├── print.s # print — gibt einen einzelnen nullterminierten String aus (a0)
│ │ └── utils.s # printl — gibt eine Liste von Strings aus (a0=addr Tabelle, a1=Anzahl)
│ ├── chapters/
│ │ └── chapter_1.s # Kapitel 1 Logik; lädt Intro-Strings und ruft printl auf
│ └── dialogue/
│ └── intro.s # String-Daten für die Intro-Sequenz
└── build/ # Kompilierte .o-Dateien (spiegelt src/-Struktur wider)
Architektur
Die Engine ist reiner RISC-V 32-Bit-Assembler, der über QEMU-Benutzermodus-Emulation auf das Linux-ABI abzielt.
Stack-Frame-Konvention
Jede Funktion verwendet die Makros startF / endF aus src/include/function.s, um ra, s0, s1, s2 auf dem Stack zu sichern und wiederherzustellen:
startF # push: 16 Bytes allozieren, ra/s0/s1/s2 sichern
...
endF # pop: ra/s0/s1/s2 wiederherstellen, 16 Bytes freigeben
ret
Syscalls
Warnung: RARS/MARS-Umgebungsaufrufnummern funktionieren nicht unter
qemu-riscv32. Dieses Projekt verwendet Linux-RISC-V-ABI-Syscallnummern.
Die Syscallnummer kommt in a7, aufgerufen mit ecall. Der Rückgabewert kommt in a0 zurück.
Ausgabe
| Zweck | RARS a7 |
Linux a7 |
a0 |
a1 |
a2 |
Hinweise |
|---|---|---|---|---|---|---|
| String ausgeben | 4 |
64 |
1 (stdout fd) |
buf addr | Länge | Linux stoppt NICHT bei Null — Byte-Länge übergeben |
| Zeichen ausgeben | 11 |
64 |
1 |
char buf | 1 |
Zeichen im Speicher speichern, Adresse übergeben |
| Ganzzahl ausgeben | 1 |
— | Ganzzahl | — | — | kein Linux-Äquivalent; zuerst in String umwandeln |
| Hex-Ganzzahl ausg. | 34 |
— | Ganzzahl | — | — | kein Linux-Äquivalent; manuell umwandeln |
| Unsigned int ausg. | 36 |
— | Ganzzahl | — | — | kein Linux-Äquivalent; manuell umwandeln |
Eingabe
| Zweck | RARS a7 |
Linux a7 |
a0 |
a1 |
a2 |
Hinweise |
|---|---|---|---|---|---|---|
| String lesen | 8 |
63 |
0 (stdin fd) |
buf addr | max Bytes | Linux gibt rohe Bytes inkl. Zeilenumbruch zurück |
| Zeichen lesen | 12 |
63 |
0 |
buf addr | 1 |
gibt a0 = gelesene Bytes zurück; Zeichen in buf |
| Ganzzahl les. | 5 |
— | — | — | — | kein Linux-Äquivalent; String lesen, parsen |
Prozess
| Zweck | RARS a7 |
Linux a7 |
a0 |
Hinweise |
|---|---|---|---|---|
| Beenden | 10 |
93 |
— | RARS ignoriert a0; Linux liest es als Exit-Status |
| Beenden(Code) | 17 |
93 |
Exit-Code | Linux exit_group |
Speicher
| Zweck | RARS a7 |
Linux a7 |
a0 |
Rückgabe | Hinweise |
|---|---|---|---|---|---|
| Allozieren (sbrk) | 9 |
214 |
Bytes (RARS) / neue brk-Adresse (Linux) | a0 = Start des allozierten Blocks |
Linux brk funktioniert anders — man setzt die neue Spitze, nicht eine Größe |
Datei-E/A
| Zweck | RARS a7 |
Linux a7 |
a0 |
a1 |
a2 |
a3 |
Hinweise |
|---|---|---|---|---|---|---|---|
| Datei öffnen | 13 |
56 |
dirfd (-100=CWD) |
Dateiname addr | Flags | Modus | Linux verwendet openat |
| Datei lesen | 14 |
63 |
fd | buf addr | max Bytes | — | gibt gelesene Bytes zurück |
| Datei schreiben | 15 |
64 |
fd | buf addr | Byte-Anzahl | — | gibt geschriebene Bytes zurück |
| Datei schließen | 16 |
57 |
fd | — | — | — | gibt 0 bei Erfolg zurück |
Zeit
| Zweck | RARS a7 |
Linux a7 |
a0 |
a1 |
Hinweise |
|---|---|---|---|---|---|
| Zeit | 30 |
113 |
clock id (1=REALTIME) |
timespec* buf |
RARS gibt geteilt lo/hi in a0/a1 zurück; Linux schreibt Struct in buf |
| Schlafen | 32 |
115 |
clock id | timespec* |
RARS nimmt Millisekunden in a0; Linux verwendet clock_nanosleep mit einem Struct |
Inhalte hinzufügen
- Neue Dialog-Strings kommen in
src/dialogue/. - Neue Kapitel kommen in
src/chapters/und werden mit.includein die Kapiteldatei eingebunden, die sie benötigt. - Das Makefile erkennt automatisch alle
.s-Dateien untersrc/(außersrc/include/).
