FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (5. システムコールの呼び出し)

Hello Worldのプログラムを動かしながら、RISC-V Spikeシミュレータのログを追っていき、RISC-Vのブートシーケンスを追っていく、その2。 今回はRISC-Vプログラムのシステムコールの呼び出し部分

f:id:msyksphinz:20180616132641p:plain

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_proxybcdが登録されている。このうち、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>