Hello Worldのプログラムを動かしながら、RISC-V Spikeシミュレータのログを追っていき、RISC-Vのブートシーケンスを追っていく、その2。
今回はRISC-Vプログラムのシステムコールの呼び出し部分
Spikeシミュレータの構造を調べているのだが、printf()
などの部分はすべてシステムコールを読んで実現するようになっている。
そして、システムコールの部分はriscv-pk/pk/syscalls.cc
で実現されている。HTIFで受け取ったシステムコールをriscv-fesvrで処理している。
github.com
簡単に言うと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)
{
...
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);
fds.alloc(stdout_fd0);
fds.alloc(stdout_fd1);
...
例えば、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);
改めて作ったプログラムを見てみる。
#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>