Hello Worldのプログラムを動かしながら、RISC-V Spikeシミュレータのログを追っていき、RISC-Vのブートシーケンスを追っていく、その2。 今回はRISC-Vプログラムのシステムコールの呼び出し部分
Spikeシミュレータの構造を調べているのだが、printf()
などの部分はすべてシステムコールを読んで実現するようになっている。
そして、システムコールの部分はriscv-pk/pk/syscalls.cc
で実現されている。HTIFで受け取ったシステムコールをriscv-fesvrで処理している。
簡単に言うとHTIFで受け取ったシステムコールを、シミュレータに接続されたsystem_proxy関数で処理している。
システムコールはいつ呼ばれるのかというと、Spikeの命令セットを実行するエンジンとは横に、デバイスを動作させている部分があり、この部分がデバイスリストを1サイクル毎にtick()
で呼び出している。これにより関数コールが検知されるというわけだ。
int htif_t::run() { start(); auto enq_func = [](std::queue<reg_t>* q, uint64_t x) { q->push(x); }; std::queue<reg_t> fromhost_queue; std::function<void(reg_t)> fromhost_callback = std::bind(enq_func, &fromhost_queue, std::placeholders::_1); 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(mem, tohost, fromhost_callback); device_list.handle_command(cmd); } else { idle(); } device_list.tick(); if (!fromhost_queue.empty() && mem.read_uint64(fromhost_addr) == 0) { mem.write_uint64(fromhost_addr, fromhost_queue.front()); fromhost_queue.pop(); } } stop();
void htif_t::register_devices() { device_list.register_device(&syscall_proxy); device_list.register_device(&bcd); for (auto d : dynamic_devices) device_list.register_device(d); }
デバイスリストにsyscall_proxy
とbcd
が登録されている。このうち、syscall_proxy
がシステムコールを処理している。
このsyscall_proxy
を探ってみると、以下で定義されている。
htif_t::htif_t() : mem(this), entry(DRAM_BASE), sig_addr(0), sig_len(0), tohost_addr(0), fromhost_addr(0), exitcode(0), stopped(false), syscall_proxy(this) // syscall_proxyを登録 { ...
class htif_t : public chunked_memif_t { public: ... syscall_t syscall_proxy; ... };
syscall_t クラスはシステムコールの関数群をテーブルとして保管している。このtable
メンバがシステムコールとして使用する関数を保存しているわけだ。
class syscall_t : public device_t { public: syscall_t(htif_t*); void set_chroot(const char* where); private: const char* identity() { return "syscall_proxy"; } htif_t* htif; memif_t* memif; std::vector<syscall_func_t> table; fds_t fds; ...
システムコール関数は以下のようにテーブルに格納される。
syscall_t::syscall_t(htif_t* htif) : htif(htif), memif(&htif->memif()), table(2048) { table[17] = &syscall_t::sys_getcwd; table[25] = &syscall_t::sys_fcntl; table[34] = &syscall_t::sys_mkdirat; table[35] = &syscall_t::sys_unlinkat; table[37] = &syscall_t::sys_linkat; table[38] = &syscall_t::sys_renameat; table[46] = &syscall_t::sys_ftruncate; table[48] = &syscall_t::sys_faccessat; table[49] = &syscall_t::sys_chdir; table[56] = &syscall_t::sys_openat; table[57] = &syscall_t::sys_close; table[62] = &syscall_t::sys_lseek; table[63] = &syscall_t::sys_read; table[64] = &syscall_t::sys_write; table[67] = &syscall_t::sys_pread; table[68] = &syscall_t::sys_pwrite; table[79] = &syscall_t::sys_fstatat; table[80] = &syscall_t::sys_fstat; table[93] = &syscall_t::sys_exit; table[1039] = &syscall_t::sys_lstat; table[2011] = &syscall_t::sys_getmainvars; ...
ファイルディスクリプタは、同様にhtif
で定義されている。fds_t
により、stdin, stdout, stderrが定義される。
syscall_t::syscall_t(htif_t* htif) : htif(htif), memif(&htif->memif()), table(2048) { ... int stdin_fd = dup(0), stdout_fd0 = dup(1), stdout_fd1 = dup(1); if (stdin_fd < 0 || stdout_fd0 < 0 || stdout_fd1 < 0) throw std::runtime_error("could not dup stdin/stdout"); fds.alloc(stdin_fd); // stdin -> stdin fds.alloc(stdout_fd0); // stdout -> stdout fds.alloc(stdout_fd1); // stderr -> stdout ...
例えば、printf()
を実行するとputs()
が実行され、その結果システムコールとしてはsys_write
が呼ばれる。
reg_t syscall_t::sys_write(reg_t fd, reg_t pbuf, reg_t len, reg_t a3, reg_t a4, reg_t a5, reg_t a6) { std::vector<char> buf(len); memif->read(pbuf, len, &buf[0]); reg_t ret = sysret_errno(write(fds.lookup(fd), &buf[0], len)); return ret; }
write(fds.lookup(fd), &buf[0], len)
により、stdoutに対して文字が出力されるという構造だ。
RISC-V fesvrを改造して動作を確認
上記の解析を確認するために、riscv-fesvrにstd::cout
を挿入して動作を確認してみた。
$ git diff diff --git a/fesvr/syscall.cc b/fesvr/syscall.cc index 6e8baf6..d892851 100644 --- a/fesvr/syscall.cc +++ b/fesvr/syscall.cc @@ -149,6 +149,11 @@ reg_t syscall_t::sys_pread(reg_t fd, reg_t pbuf, reg_t len, reg_t off, reg_t a4, reg_t syscall_t::sys_write(reg_t fd, reg_t pbuf, reg_t len, reg_t a3, reg_t a4, reg_t a5, reg_t a6) { + std::cout << "<Info: sys_write (" << std::hex << fd << ", " << std::hex << pbuf << ", " + << std::hex << len << ", " << std::hex << a3 << ", " + << std::hex << a4 << ", " << std::hex << a5 << ", " + << std::hex << a6 << ");>\n"; + std::vector<char> buf(len); memif->read(pbuf, len, &buf[0]); reg_t ret = sysret_errno(write(fds.lookup(fd), &buf[0], len)); @@ -346,6 +351,8 @@ void syscall_t::dispatch(reg_t mm) if (n >= table.size() || !table[n]) throw std::runtime_error("bad syscall #" + std::to_string(n)); + std::cout << "<Info: table[" << std::dec << n << "] is calling>\n"; + magicmem[0] = (this->*table[n])(magicmem[1], magicmem[2], magicmem[3], magicmem[4], magicmem[5], magicmem[6], magicmem[7]); memif->write(mm, sizeof(magicmem), magicmem);
改めて作ったプログラムを見てみる。
cat test_output.c
#include <stdio.h> int main () { printf ("Hello World, C\n"); return 0; }
実行結果。Hello World, C
を出力するより前に、いくつかシステムコールが呼ばれている。
sys_write
システムコールでは、stdout
に対して文字列を出力していることが分かる。
spike pk test_output_c &> test_output_c.log $ <Info: table[2011] is calling> <Info: table[56] is calling> <Info: table[67] is calling> <Info: table[67] is calling> <Info: table[67] is calling> <Info: table[67] is calling> <Info: table[67] is calling> <Info: table[57] is calling> <Info: table[80] is calling> <Info: table[64] is calling> <Info: sys_write (1, 80829030, f, 0, 0, 0, 0);> Hello World, C <Info: table[93] is calling>