FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages/

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (1. RISC-V バイナリの解析)

RISC-V Spikeシミュレータでは printf()std::cout を使ってもコンソールで出力することができる。

  • test_output.c
#include <stdio.h>
int main ()
{
  printf ("Hello World, C\n");
  return 0;
}
  • test_output.cpp
#include <iostream>
int main ()
{
  std::cout << "Hello World, C++\n";
  return 0;
}
all: test_output_cpp test_output_c
run: run_cpp run_c

test_output_cpp: test_output.cpp Makefile
        riscv64-unknown-elf-g++ -O3 $< -o $@

test_output_c: test_output.c Makefile
        riscv64-unknown-elf-gcc -O3 $< -o $@

run_cpp: test_output_cpp Makefile
        spike -l pk $< > run_output_cpp.log

run_c: test_output_c Makefile
        spike -l pk $< > run_output_c.log
$ make run
Hello World, C
Hello World, C++

このprintf()が正しく実行されるためには、riscv-pk (Proxy Kernel) という仕組みを利用していて、Host とのインタフェースを取り持つモジュール(HTIF)が接続されている。

$ spike pk run_output_cpp
$ spike pk run_output_c

命令のトレースログを取っていき、どのような仕組みで printf() が実現されているのかを探していく。

だいたい以下のようになっていき、途中からecall命令で特権モードに移動するようだ。

main() (00000000000100b0)
  - puts () (00000000000103ac)
    - _puts_r () (0000000000010332)
    - strlen ()
    - __sfvwrite_r () (00000000000106c2)
  • run_output_cpp.log
...
core   0: 0x0000000000011e22 (0x00004781) li      a5, 0
0 0x0000000000011e22 (0x4781) x15 0x0000000000000000
core   0: 0x0000000000011e24 (0x05000893) li      a7, 80
0 0x0000000000011e24 (0x05000893) x17 0x0000000000000050
core   0: 0x0000000000011e28 (0x00000073) ecall   <-- printf() が呼ばれる際のEcall
core   0: exception trap_user_ecall, epc 0x0000000000011e28
core   0: 0x0000000080001854 (0x14011173) csrrw   sp, sscratch, sp
1 0x0000000080001854 (0x14011173) x 2 0x0000000080025000
core   0: 0x0000000080001858 (0x00011463) bnez    sp, pc + 8
1 0x0000000080001858 (0x00011463)
core   0: 0x0000000080001860 (0x00007129) addi    sp, sp, -320
1 0x0000000080001860 (0x7129) x 2 0x0000000080024ec0
core   0: 0x0000000080001862 (0x0000e406) sd      ra, 8(sp)
1 0x0000000080001862 (0xe406)
core   0: 0x0000000080001864 (0x0000ec0e) sd      gp, 24(sp)
1 0x0000000080001864 (0xec0e)
core   0: 0x0000000080001866 (0x0000f012) sd      tp, 32(sp)
1 0x0000000080001866 (0xf012)
core   0: 0x0000000080001868 (0x0000f416) sd      t0, 40(sp)
1 0x0000000080001868 (0xf416)
core   0: 0x000000008000186a (0x0000f81a) sd      t1, 48(sp)
1 0x000000008000186a (0xf81a)
core   0: 0x000000008000186c (0x0000fc1e) sd      t2, 56(sp)
1 0x000000008000186c (0xfc1e)
core   0: 0x000000008000186e (0x0000e0a2) sd      s0, 64(sp)
1 0x000000008000186e (0xe0a2)
...

2018/06/11追記。libpkではなく ${RISCV}/riscv64-unknown-elf/bin/pk だった。このダンプ結果は無意味。

ここから先はriscv-pkに渡っていく。${RISCV}/riscv64-unknown-elf/lib/riscv-pk/libpk.a を見てみる。riscv64-unknown-elf-objdump でダンプしていく。

  • libpk.a のダンプ結果
セクション .text の逆アセンブル:

0000000000000000 <trap_entry-0x2>:
   0:   0001                    nop

0000000000000002 <trap_entry>:
   2:   14011173                csrrw   sp,sscratch,sp  <-- ここにジャンプされる?
   6:   00011463                bnez    sp,e <.L1^B1>
   a:   14002173                csrr    sp,sscratch

000000000000000e <.L1^B1>:
   e:   7129                    addi    sp,sp,-320
  10:   e406                    sd      ra,8(sp)
  12:   ec0e                    sd      gp,24(sp)
  14:   f012                    sd      tp,32(sp)
  16:   f416                    sd      t0,40(sp)
  18:   f81a                    sd      t1,48(sp)
  1a:   fc1e                    sd      t2,56(sp)
  1c:   e0a2                    sd      s0,64(sp)
  1e:   e4a6                    sd      s1,72(sp)

このとき、 riscv-pk/pk/entry.S を追いかけていく。handle_trap() に渡っていく。

trap_entry :entry.S
  - handle_trap : riscv-pk/pk/handlers.c

handlers.c を参照した。handle_syscall() が呼ばれているのかな。

  • riscv-pk/pk/handlers.c
void handle_trap(trapframe_t* tf)
{
  if ((intptr_t)tf->cause < 0)
    return handle_interrupt(tf);

  typedef void (*trap_handler)(trapframe_t*);

  const static trap_handler trap_handlers[] = {
    [CAUSE_MISALIGNED_FETCH] = handle_misaligned_fetch,
    [CAUSE_FETCH_PAGE_FAULT] = handle_fault_fetch,
    [CAUSE_ILLEGAL_INSTRUCTION] = handle_illegal_instruction,
    [CAUSE_USER_ECALL] = handle_syscall,
    [CAUSE_BREAKPOINT] = handle_breakpoint,
    [CAUSE_MISALIGNED_STORE] = handle_misaligned_store,
    [CAUSE_LOAD_PAGE_FAULT] = handle_fault_load,
    [CAUSE_STORE_PAGE_FAULT] = handle_fault_store,
  };

  kassert(tf->cause < ARRAY_SIZE(trap_handlers) && trap_handlers[tf->cause]);

  trap_handlers[tf->cause](tf);
}
  • handlers.c
static void handle_syscall(trapframe_t* tf)
{
  tf->gpr[10] = do_syscall(tf->gpr[10], tf->gpr[11], tf->gpr[12], tf->gpr[13],
                           tf->gpr[14], tf->gpr[15], tf->gpr[17]);
  tf->epc += 4;
}

次に、do_syscall() が呼ばれる。ここからどこへ進むんだ?

  • riscv-pk/pk/syscalls.c
long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, unsigned long n)
{
  const static void* syscall_table[] = {
    [SYS_exit] = sys_exit,
    [SYS_exit_group] = sys_exit,
    [SYS_read] = sys_read,
    [SYS_pread] = sys_pread,
    [SYS_write] = sys_write,
    [SYS_openat] = sys_openat,
    [SYS_close] = sys_close,
    [SYS_fstat] = sys_fstat,
    [SYS_lseek] = sys_lseek,
    [SYS_fstatat] = sys_fstatat,
    [SYS_linkat] = sys_linkat,
    [SYS_unlinkat] = sys_unlinkat,
    [SYS_mkdirat] = sys_mkdirat,
    [SYS_renameat] = sys_renameat,
    [SYS_getcwd] = sys_getcwd,
    [SYS_brk] = sys_brk,
    [SYS_uname] = sys_uname,
    [SYS_getpid] = sys_getpid,
    [SYS_getuid] = sys_getuid,
    [SYS_geteuid] = sys_getuid,
    [SYS_getgid] = sys_getuid,
    [SYS_getegid] = sys_getuid,
    [SYS_mmap] = sys_mmap,
    [SYS_munmap] = sys_munmap,
    [SYS_mremap] = sys_mremap,
    [SYS_mprotect] = sys_mprotect,
    [SYS_prlimit64] = sys_stub_nosys,
    [SYS_rt_sigaction] = sys_rt_sigaction,
    [SYS_gettimeofday] = sys_gettimeofday,
    [SYS_times] = sys_times,
    [SYS_writev] = sys_writev,
    [SYS_faccessat] = sys_faccessat,
    [SYS_fcntl] = sys_fcntl,
    [SYS_ftruncate] = sys_ftruncate,
    [SYS_getdents] = sys_getdents,
    [SYS_dup] = sys_dup,
    [SYS_readlinkat] = sys_stub_nosys,
    [SYS_rt_sigprocmask] = sys_stub_success,
    [SYS_ioctl] = sys_stub_nosys,
    [SYS_clock_gettime] = sys_stub_nosys,
    [SYS_getrusage] = sys_stub_nosys,
    [SYS_getrlimit] = sys_stub_nosys,
    [SYS_setrlimit] = sys_stub_nosys,
    [SYS_chdir] = sys_chdir,
    [SYS_set_tid_address] = sys_stub_nosys,
    [SYS_set_robust_list] = sys_stub_nosys,
  };

...

set_tohost によりこれらの処理が実現されているようだ。tohostにアクセスすることで処理が行われている?

  • riscv-pk/pk/htif.c
long frontend_syscall(long n, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5, uint64_t a6)
{
  static volatile uint64_t magic_mem[8];

  static spinlock_t lock = SPINLOCK_INIT;
  spinlock_lock(&lock);

  magic_mem[0] = n;
  magic_mem[1] = a0;
  magic_mem[2] = a1;
  magic_mem[3] = a2;
  magic_mem[4] = a3;
  magic_mem[5] = a4;
  magic_mem[6] = a5;
  magic_mem[7] = a6;

  htif_syscall((uintptr_t)magic_mem);

  long ret = magic_mem[0];

  spinlock_unlock(&lock);
  return ret;
}
  • riscv-pk/machine/htif.c
extern uint64_t __htif_base;
volatile uint64_t tohost __attribute__((section(".htif")));
volatile uint64_t fromhost __attribute__((section(".htif")));
volatile int htif_console_buf;
static spinlock_t htif_lock = SPINLOCK_INIT;
uintptr_t htif;

static void __set_tohost(uintptr_t dev, uintptr_t cmd, uintptr_t data)
{
  while (tohost)
    __check_fromhost();
  tohost = TOHOST_CMD(dev, cmd, data);
}

static void do_tohost_fromhost(uintptr_t dev, uintptr_t cmd, uintptr_t data)
{
  spinlock_lock(&htif_lock);
    __set_tohost(dev, cmd, data);

    while (1) {
      uint64_t fh = fromhost;
      if (fh) {
        if (FROMHOST_DEV(fh) == dev && FROMHOST_CMD(fh) == cmd) {
          fromhost = 0;
          break;
        }
        __check_fromhost();
      }
    }
  spinlock_unlock(&htif_lock);
}

void htif_syscall(uintptr_t arg)
{
  do_tohost_fromhost(0, 0, arg);
}