FPGA開発日記

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

Binary Translation型エミュレータを作る(システムコールのサポート検討)

Rustで作るBinary Translation型のエミュレータ、基本的な機能はかなり備わってきたのだが、ここまで来るとテストパタンを動かしてみたい。RISC-Vのテストベンチとしては代表的なものにriscv-testsbenchmarksディレクトリのテストパタンが挙げられるが、これを動かすためにはまずはシステムコールをサポートしなければならない。例えば、テストパタンによってはprintf()が使用されている場合があり、このときはシステムコールのお世話になる必要がある。

riscv-testsprintf()は非常に限定的な実装になっており、put()がホストマシンに投げられているので非常に単純だ。

f:id:msyksphinz:20201111231322p:plain
  • riscv-tools/riscv-tests/benchmarks/common/syscalls.c
int printf(const char* fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);

  vprintfmt((void*)putchar, 0, fmt, ap);

このputchar()システムコールを呼び出しているだけである。つまり、syscall(SYS_write)により出力したい文字を書き込んでいる。

#undef putchar
int putchar(int ch)
{
  static __thread char buf[64] __attribute__((aligned(64)));
  static __thread int buflen = 0;

  buf[buflen++] = ch;

  if (ch == '\n' || buflen == sizeof(buf))
  {
    syscall(SYS_write, 1, (uintptr_t)buf, buflen);
    buflen = 0;
  }

このsyscallの実体は以下のようになっており、tohostアサインされているメモリ領域に対してシステムコールの必要な情報を格納したメモリアドレスを投げ込む、そしてfromhostが1になるまでそこでスピンして待ち、fromhostが1になると再度fromhostを0に戻して終わる。

static uintptr_t syscall(uintptr_t which, uint64_t arg0, uint64_t arg1, uint64_t arg2)
{
  volatile uint64_t magic_mem[8] __attribute__((aligned(64)));
  magic_mem[0] = which;
  magic_mem[1] = arg0;
  magic_mem[2] = arg1;
  magic_mem[3] = arg2;
  __sync_synchronize();

  tohost = (uintptr_t)magic_mem;
  while (fromhost == 0)
    ;
  fromhost = 0;
...

このプロトコルに則り自作エミュレータに条件を追加する。

        while loop_idx < loop_max {
            if self.m_arg_config.debug {
                println!("========= BLOCK START =========");
            }

            let tb_text_mem = match self.m_tb_text_hashmap.get(&self.m_pc[0]) {
                Some(mem_map) => {
                    if self.m_arg_config.debug {
                        
....
                        
            if self.get_mem(0x1000) != 0 {
                if self.get_mem(0x1000) & 0x01 == 1 {
                    break;
                }
                self.sys_write(self.read_mem_8byte(0x80001000));
                self.write_mem_4byte(0x80001000, 0);
                self.write_mem_4byte(0x80001040, 1);
            }

つまり0x1000(=0x8000_0000)に0以外の書き込みが発生している場合にシステムコールを呼び出す。システムコールの呼び出しにはsys_write()を呼び出している、というか現在はこのwrite()システムコールしかサポートされていない。そもそも実装としても非常に中途半端で、システムコールをサポートするプロセスとメインのループを管理するプロセスを分けるとかなんとかしてならんもんかね。

    fn sys_write(&mut self, tohost: u64) {
        let fd = self.read_mem_8byte(tohost + 8);
        let pbuf = self.read_mem_8byte(tohost + 16);
        let len = self.read_mem_8byte(tohost + 24);

        println!("sys_write() = {:x} ,tohost = {:x}", pbuf, tohost);
        for idx in 0..len {
            print!("{}", self.read_mem_1byte(pbuf.wrapping_add(idx)) as char);
        }
    }

とりあえず以下のテストコードを動かして確認した。

int main()
{
  printf("Hello, World\n");

  return 0;
}
$ make -k 
riscv64-unknown-elf-gcc -I./../env -I./common -I./median -I./qsort -I./rsort -I./towers -I./vvadd -I./multiply -I./mm -I./dhrystone -I./spmv -I./mt-vvadd -I./mt-matmul -I./pmp -I./print -march=rv64g -DPREALLOCATE=1 -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -o print.riscv ./print/print.c ./common/syscalls.c ./common/crt.S -static -nostdlib -nostartfiles -lm -lgcc -T ./common/test.ld

これを動かすと以下のようになった。無事にHello, worldが表示されたのでとりあえずは問題無いようだ。

$ cargo run -- --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/benchmarks/print.riscv
sys_write() = 80002c40 ,tohost = 80022940
Hello, World