Extremis
Assembly

Extremis

RPG-приключение, написанное на ассемблере RISC-V

( боже, кто запрограммировал .iris так, чтобы его нельзя было закоммитить? )

Текстовая RPG, написанная на ассемблере RISC-V 32-bit, симулируемая через QEMU.

«Вселенная решила, что не хочет, чтобы я занимался чем-то лучшим, например, обзавелся жизнью.»

Зависимости

  • riscv32-elf-as — ассемблер RISC-V
  • riscv32-elf-ld — компоновщик RISC-V
  • qemu-riscv32 — эмулятор пользовательского режима RISC-V

На Arch Linux:

sudo pacman -S riscv64-elf-binutils qemu-user

Сборка и запуск

make build   # ассемблировать и скомпоновать в main.bin
make run     # собрать + запустить через qemu-riscv32
make debug   # запустить с -strace для просмотра системных вызовов

Структура проекта

Extremis/
├── main.s                      # Точка входа (_start), вызывает run_chapter_1
├── src/
│   ├── include/
│   │   └── function.s          # Макросы стекового фрейма startF / endF
│   ├── engine/
│   │   ├── print.s             # print  — печать одной строки с нулевым завершителем (a0)
│   │   └── utils.s             # printl — печать списка строк (a0=адрес таблицы, a1=количество)
│   ├── chapters/
│   │   └── chapter_1.s         # Логика главы 1; загружает строки вступления и вызывает printl
│   └── dialogue/
│       └── intro.s             # Строковые данные для вступительной сцены
└── build/                      # Скомпилированные .o файлы (повторяет структуру src/)

Архитектура

Движок написан на чистом 32-битном ассемблере RISC-V, ориентированном на ABI Linux через эмуляцию пользовательского режима QEMU.

Соглашение о стековом фрейме

Каждая функция использует макросы startF / endF из src/include/function.s для сохранения и восстановления ra, s0, s1, s2 в стеке:

startF    # push: выделить 16 байт, сохранить ra/s0/s1/s2
...
endF      # pop: восстановить ra/s0/s1/s2, освободить 16 байт
ret

Системные вызовы

Предупреждение: Номера системных вызовов среды RARS/MARS не работают под qemu-riscv32. В этом проекте используются номера системных вызовов ABI Linux для RISC-V.

Номер системного вызова помещается в a7, вызов выполняется инструкцией ecall. Возвращаемое значение приходит в a0.

Вывод

Назначение RARS a7 Linux a7 a0 a1 a2 Примечания
Печать строки 4 64 1 (stdout fd) адрес буфера длина Linux НЕ останавливается на нуле — передавайте длину в байтах
Печать символа 11 64 1 буфер символа 1 сохраните символ в памяти, передайте его адрес
Печать целого числа 1 целое число нет аналога в Linux; сначала преобразуйте в строку
Печать шестнадцатеричного числа 34 целое число нет аналога в Linux; преобразуйте вручную
Печать беззнакового целого 36 целое число нет аналога в Linux; преобразуйте вручную

Ввод

Назначение RARS a7 Linux a7 a0 a1 a2 Примечания
Чтение строки 8 63 0 (stdin fd) адрес буфера макс. байт Linux возвращает сырые байты, включая перевод строки
Чтение символа 12 63 0 адрес буфера 1 возвращает a0 = прочитано байт; символ в буфере
Чтение целого числа 5 нет аналога в Linux; прочитайте строку, распарсите её

Процесс

Назначение RARS a7 Linux a7 a0 Примечания
Выход 10 93 RARS игнорирует a0; Linux читает его как код выхода
Выход(код) 17 93 код выхода Linux exit_group

Память

Назначение RARS a7 Linux a7 a0 Возвращает Примечания
Выделение (sbrk) 9 214 байты (RARS) / новый адрес brk (Linux) a0 = начало выделенного блока Linux brk работает иначе — вы устанавливаете новую вершину, а не размер

Файловый ввод/вывод

Назначение RARS a7 Linux a7 a0 a1 a2 a3 Примечания
Открыть файл 13 56 dirfd (-100=CWD) адрес имени файла флаги режим Linux использует openat
Чтение файла 14 63 fd адрес буфера макс. байт возвращает количество прочитанных байт
Запись в файл 15 64 fd адрес буфера количество байт возвращает количество записанных байт
Закрыть файл 16 57 fd возвращает 0 при успехе

Время

Назначение RARS a7 Linux a7 a0 a1 Примечания
Время 30 113 id часов (1=REALTIME) timespec* buf RARS возвращает разделенные lo/hi в a0/a1; Linux записывает структуру в buf
Сон 32 115 id часов timespec* RARS принимает миллисекунды в a0; Linux использует clock_nanosleep со структурой

Добавление контента

  • Новые строки диалогов помещаются в src/dialogue/.
  • Новые главы помещаются в src/chapters/ и подключаются через .include в файл главы, который их использует.
  • Makefile автоматически находит все .s файлы в src/ (исключая src/include/).