私の開発したRISC-VシミュレータはLinuxを立ち上げることができる。シミュレータのデバッグ時には相当中身を読み込んだのだが、きちんと文章化していない挙句、大昔のプロジェクトなのでもう忘れかけている。
Linuxのブートの方法から各種プロセスの取り扱いまで、思い出しながらRISC-Vシミュレータを動かしていき、ちゃんと文章化しておきたいと思った。
printmにより文字が画面に表示されるまでの仕組み
init_first_hart()
内に、画面表示を行うためのprintm()
を呼び出す場所がある。
freedom-u-sdk/riscv-pk/machine/mtrap.c
void init_first_hart(uintptr_t hartid, uintptr_t dtb) { // Confirm console as early as possible query_uart(dtb); query_uart16550(dtb); query_htif(dtb); printm("bbl loader\r\n"); ...
freedom-u-sdk/riscv-pk/machine/mtrap.c
void printm(const char* s, ...) { va_list vl; va_start(vl, s); vprintm(s, vl); va_end(vl); }
vprintm
ではputstring()
を呼び出している。どうやらこれが画面に出力する機能のようだ。
freedom-u-sdk/riscv-pk/machine/mtrap.c
void vprintm(const char* s, va_list vl) { char buf[256]; vsnprintf(buf, sizeof buf, s, vl); putstring(buf); }
freedom-u-sdk/riscv-pk/machine/mtrap.c
void putstring(const char* s) { while (*s) mcall_console_putchar(*s++); }
文字を1文字ずつmcall_console_putchar(char c)
に送り込んでいることが分かる。
static uintptr_t mcall_console_putchar(uint8_t ch) { if (uart) { uart_putchar(ch); } else if (uart16550) { uart16550_putchar(ch); } else if (htif) { htif_console_putchar(ch); } return 0; }
見つかったデバイスに応じて呼び出す関数を変えている。今回の場合はhtif
が見つかっているので、htif_console_putchar()
を呼び出している。
htif
を使うルーチンは、freedom-u-sdk/riscv-pk/machine/htif.c
に纏められている。
freedom-u-sdk/riscv-pk/machine/htif.c
void htif_console_putchar(uint8_t ch) { spinlock_lock(&htif_lock); __set_tohost(1, 1, ch); spinlock_unlock(&htif_lock); }
__set_tohost
というのは、TOHOSTアドレスマップに対して値を書き込む。tohost
はアドレスマップしてあり0x80007008
にマップされている。これは特殊なアドレスマップであり、この領域へのアクセスはシミュレータにより特殊な処理が実施される。
Disassembly of section .htif: 0000000080007000 <fromhost>: ... 0000000080007008 <tohost>: ...
__set_tohost()
を観察すると、tohost
アドレスの値が0になるまで待つ(tohost)の値が1である場合、これはまだ何かしらの処理を実施しているという意味である。tohost
アドレスがReadyになると、TOHOST_CMD
によりコマンドを生成して書き込みを行う。
freedom-u-sdk/riscv-pk/machine/htif.c
static void __set_tohost(uintptr_t dev, uintptr_t cmd, uintptr_t data) { while (tohost) __check_fromhost(); tohost = TOHOST_CMD(dev, cmd, data); }
freedom-u-sdk/riscv-pk/machine/htif.h
#if __riscv_xlen == 64 # define TOHOST_CMD(dev, cmd, payload) \ (((uint64_t)(dev) << 56) | ((uint64_t)(cmd) << 48) | (uint64_t)(payload)) #else # define TOHOST_CMD(dev, cmd, payload) ({ \ if ((dev) || (cmd)) __builtin_trap(); \ (payload); }) #endif
この後はシミュレータにその処理が任される。riscv-isa-simではriscv-fesvr(フロントエンドサーバ)によりtohostへの書き込みが検出され、シグナルに応じて処理が実行される。今回は__set_tohost(1,1,ch)
なのでデバイスID=1, コマンド=1となる。
freedom-u-sdk/riscv-fesvr/fesvr/htif.cc
int htif_t::run() { start(); ... if (tohost_addr == 0) { while (true) idle(); } while (!signal_exit && exitcode == 0) { if (auto tohost = mem.read_uint64(tohost_addr)) { mem.write_uint64(tohost_addr, 0); command_t cmd(this, tohost, fromhost_callback); device_list.handle_command(cmd); } else { idle(); } ...
freedom-u-sdk/riscv-fesvr/fesvr/device.cc
void device_t::handle_command(command_t cmd)
{
command_handlers[cmd.cmd()](cmd);
}
freedom-u-sdk/riscv-fesvr/fesvr/htif.cc
htif_t::htif_t(const std::vector<std::string>& args) : mem(this), entry(DRAM_BASE), sig_addr(0), sig_len(0), tohost_addr(0), fromhost_addr(0), exitcode(0), stopped(false), syscall_proxy(this) { signal(SIGINT, &handle_signal); ... device_list.register_device(&syscall_proxy); device_list.register_device(&bcd); for (auto d : dynamic_devices) device_list.register_device(d); }
bcd
が入出力を管理するためのデバイスだ。2番目に登録されたのでデバイスID=1となっている。
freedom-u-sdk/riscv-fesvr/fesvr/device.cc
bcd_t::bcd_t() { register_command(0, std::bind(&bcd_t::handle_read, this, _1), "read"); register_command(1, std::bind(&bcd_t::handle_write, this, _1), "write"); }
書き込みを行った場合はターミナルへの出力が行われる。これで画面出力の完成だ。
freedom-u-sdk/riscv-fesvr/fesvr/device.cc
void bcd_t::handle_read(command_t cmd) { pending_reads.push(cmd); } void bcd_t::handle_write(command_t cmd) { canonical_terminal_t::write(cmd.payload()); }
freedom-u-sdk/riscv-fesvr/fesvr/term.cc
void canonical_terminal_t::write(char ch) { if (::write(1, &ch, 1) != 1) abort(); }