FPGA開発日記

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

独自RISC-V CPUコアの実装 (VerilatorのFST出力の切り替えオプション実装)

自作RISC-Vコアを作り始めてしばらく経っているが、リグレッションテストのときは波形の取得を行わないように変更したい。 デフォルトでは波形を出力する様になっているのだがこれをオプションで変更しよう。

まずはVerilatorのC++実装に-dオプションを追加して、波形の取得のためのdump_fst_enable変数を有効にする。

  while (1) {
    static struct option long_options[] = {
      {"elf",  no_argument, 0, 'e' },
      {"dump", no_argument, 0, 'd' },
      {"help", no_argument, 0, 'h' }
    };

    int option_index = 0;
    int c = getopt_long(argc, argv, "-e:h:d", long_options, &option_index);

    if (c == -1) break;
 retry:
    switch (c) {
      // Process long and short EMULATOR options
      case 'h': usage(argv[0]);             return 1;
      case 'd':
        dump_fst_enable = true;
        break;
      case 'e': {
        g_memory   = std::unique_ptr<Memory> (new Memory ());

        filename = (char*)malloc(strlen(optarg) + 1);
        strcpy(filename, optarg);
        load_binary("", optarg, true);

        break;
      }
    }
  }

-dオプションを有効にしているときは、FSTの出力を有効にする。

  // Trace DUMP ON
  VerilatedFstC* tfp = NULL;
  if (dump_fst_enable) {
    Verilated::traceEverOn(true);
    tfp = new VerilatedFstC;

    dut->trace(tfp, 100);  // Trace 100 levels of hierarchy
    tfp->open("simx.fst");

さらに、dump_fst_enableが有効の時はfstのダンプを出力する様にしている。

  while (time_counter < 100) {
    dut->eval();
    if (dump_fst_enable) tfp->dump(time_counter);
    time_counter++;
  }

これでVerilatorの-dオプションでFSTの出力を制御できるようになった。

RISC-Vが32ビットと64ビットの仕様を明確に分離するのは何故なのか

f:id:msyksphinz:20210325001324p:plain

RISC-V ISAマニュアルの最初の方を読んでいて、小噺的に書いてあったのを読んでちょっと面白そうと思った、というか自分でもまだイメージが上手くできていない部分があるのでメモしてみる。

github.com

RISC-Vでは32ビットと64ビットのモードを明確に分離し別々のISAとして定義している。なぜRV32はRV64の厳密なサブセットにしなかったのか、という理由が説明されている。 MIPS32とMIPS64は完全にスーパーセットであるように設計されているという説明を見たのだが、ググってみると確かに以下の文章で似たような記述があるのでそれは間違いないのだろう(中森章さんの文章)。

  • 『はじめて読む MIPS(リローデッド)』 by 中森章

https://www.cqpub.co.jp/interface/TechI/Vol39/app/mips_asm.pdf

ここでいう互換性がないというのがあまりイメージできていなかったのだが、詳細に調べるとMIPSの場合はMIPS32とMIPS64において同一命令の動作は完全に同一らしい。 例えばADD命令は、MIPS32とMIPS64ではどちらでも「32ビット加算」として定義されている(64ビット加算はMIPS64においてDADDとして定義されている)。

f:id:msyksphinz:20210324235809p:plain

f:id:msyksphinz:20210324235853p:plain

一方でRISC-Vの場合はADD命令はRV32では32ビット加算、RV64では64ビット加算となる。なるほど、これが「RV32とRV64で互換性がない」という説明の意味するところか。 MIPSの方式ではMIPS64のハードウェアでMIPS32のバイナリを厳密に実行できる可能性があるが、RISC-Vの場合はそうはならない。これが「命令セットを分離する」というところの意味合いとなっているのだろう。

まあとりあえずこの辺の前置きはすっ飛ばすとして、RV32とRV64を明確に分離した理由として一つは、各ISAのベースコンフィグレーションに実装を最適化できるという点が挙げられている。 さらに、どちらにしろRV32IをRV64Iでエミュレートするような状況でも、微妙な仕様の違いにより不正命令例外などの制御の違いによりどちらにしろハードウェアの支援なしに完全なエミュレーションは出来ないという結論に至ったらしい(結局MIPSもそうだったのだろうか、最後にそれっぽいメモが入っている)。

We note that newer SPARC and MIPS ISA revisions have deprecated support for running 32-bit code unchanged on 64-bit systems.

さらに、RV32とRV64でのABIの仕様の違いもあり完全に互換性をもって設計することは出来ない、という所からもRV32IとRV64Iを分離して設計した、という理由になっている。

このような議論により、「RV32/RV64にはADD命令を定義し、RV64には追加的にADDW」という命令を定義する、という設計方針に繋がっている。 これは命令の互換性というよりも「RV64で32ビット演算をしたい」という目的から作られた命令であり、現にRV32ではADDWはサポートされていない。


こうして改めてMIPSの仕様を見てみると、MIPS64でも「AND」命令の仕様は「do a bitwise logical AND」となっておりビット幅の定義が載っていない。加算命令ではADDとDADDを厳密に分離したのに、ANDの場合は胡麻化しているように見えないこともない。これを良しとするかは別問題として、RISC-Vはこの辺の命名統一性まで考慮して命令をデザインしたということなんだろうか。

XuantieのRISC-V Vector向けリポジトリを試す (OpenBLASをベアメタルで動かす試行1)

OpenBLASのコンパイルをベアメタルで実行したいのだが、かなり壁があるようだ。メモリアクセスに関する関数がベアメタルだと上手く行かないので、その辺を何とかごまかす必要がある。

Pthreadのサポートなども使えないので、その辺を取り除いて試行しているが、まだ上手く行かない。

diff --git a/Makefile.prebuild b/Makefile.prebuild
index d6395da7..e774625b 100644
--- a/Makefile.prebuild
+++ b/Makefile.prebuild
@@ -42,7 +42,7 @@ TARGET_FLAGS = -mips64r6
 endif

 ifeq ($(TARGET), C910V)
-TARGET_FLAGS = -march=rv64gcvxthead -mabi=lp64v
+TARGET_FLAGS = -march=rv64gcvxthead -mabi=lp64
 endif

 all: getarch_2nd
diff --git a/Makefile.riscv64 b/Makefile.riscv64
index 15d7b059..6c3bacbd 100644
--- a/Makefile.riscv64
+++ b/Makefile.riscv64
@@ -1,4 +1,4 @@
 ifeq ($(CORE), C910V)
-CCOMMON_OPT += -march=rv64gcvxthead -mabi=lp64v
-FCOMMON_OPT += -march=rv64gcvxthead -mabi=lp64v -static
+CCOMMON_OPT += -march=rv64gcv -mabi=lp64
+FCOMMON_OPT += -march=rv64gcv -mabi=lp64 -static
 endif
diff --git a/common.h b/common.h
index 2825407c..c6e353cb 100644
--- a/common.h
+++ b/common.h
@@ -123,9 +123,9 @@ extern "C" {
 #undef  GOTO_ATOM
 #endif
 #else
-#include <sys/mman.h>
+    // #include <sys/mman.h>
 #ifndef NO_SYSV_IPC
-#include <sys/shm.h>
+    // #include <sys/shm.h>
 #endif
 #include <sys/time.h>
 #include <time.h>

メモリアクセス関連のインクルードファイルをひたすら取り除く。最終的にmemory.cのコンパイルを止めた。

diff --git a/driver/others/Makefile b/driver/others/Makefile
index d09444f5..250ee444 100644
--- a/driver/others/Makefile
+++ b/driver/others/Makefile
@@ -1,7 +1,7 @@
 TOPDIR = ../..
 include ../../Makefile.system

-COMMONOBJS      = memory.$(SUFFIX) xerbla.$(SUFFIX) c_abs.$(SUFFIX) z_abs.$(SUFFIX) openblas_set_num_threads.$(SUFFIX) openblas_get_num_threads.$(SUFFIX) openblas_get_num_procs.$(SUFFIX) openblas_get_config.$(SUFFIX) openblas_get_parallel.$(SUFFIX) openblas_error_handle.$(SUFFIX) openblas_env.$(SUFFIX)
+COMMONOBJS      = xerbla.$(SUFFIX) c_abs.$(SUFFIX) z_abs.$(SUFFIX) openblas_set_num_threads.$(SUFFIX) openblas_get_num_threads.$(SUFFIX) openblas_get_num_procs.$(SUFFIX) openblas_get_config.$(SUFFIX) openblas_get_parallel.$(SUFFIX) openblas_error_handle.$(SUFFIX) openblas_env.$(SUFFIX)

 #COMMONOBJS    += slamch.$(SUFFIX) slamc3.$(SUFFIX) dlamch.$(SUFFIX)  dlamc3.$(SUFFIX)

これでもまだリンクに到達できない。ベアメタルのRISC-Vコンパイラではベクトル系のマクロが登録されていないため、エラーとなってしまっている。

../kernel/riscv64/amax_vector.c: In function 'samax_k':
../kernel/riscv64/amax_vector.c:34:19: error: unknown type name 'float32xm8_t'
   34 | #define FLOAT_V_T float32xm8_t
      |                   ^~~~~~~~~~~~
../kernel/riscv64/amax_vector.c:64:9: note: in expansion of macro 'FLOAT_V_T'
   64 |         FLOAT_V_T v0, v1, v_max;
      |         ^~~~~~~~~
../kernel/riscv64/amax_vector.c:38:16: error: unknown type name 'e32xm8_t'
   38 | #define MASK_T e32xm8_t
      |                ^~~~~~~~
../kernel/riscv64/amax_vector.c:66:9: note: in expansion of macro 'MASK_T'
   66 |         MASK_T mask0, mask1;
      |         ^~~~~~
../kernel/riscv64/amax_vector.c:69:23: warning: implicit declaration of function 'vsetvli' [-Wimplicit-function-declaration]
   69 |                 gvl = vsetvli(n, RVV_EFLOAT, RVV_M);
      |                       ^~~~~~~
../kernel/riscv64/amax_vector.c:32:20: error: 'RVV_E32' undeclared (first use in this function); did you mean 'RVV_M'?
   32 | #define RVV_EFLOAT RVV_E32
      |                    ^~~~~~~
../kernel/riscv64/amax_vector.c:69:34: note: in expansion of macro 'RVV_EFLOAT'
   69 |                 gvl = vsetvli(n, RVV_EFLOAT, RVV_M);
      |                                  ^~~~~~~~~~
../kernel/riscv64/amax_vector.c:32:20: note: each undeclared identifier is reported only once for each function it appears in
   32 | #define RVV_EFLOAT RVV_E32
      |                    ^~~~~~~
../kernel/riscv64/amax_vector.c:69:34: note: in expansion of macro 'RVV_EFLOAT'
   69 |                 gvl = vsetvli(n, RVV_EFLOAT, RVV_M);
      |                                  ^~~~~~~~~~
../kernel/riscv64/iamax_vector.c: In function 'isamax_k':
../kernel/riscv64/iamax_vector.c:59:19: error: unknown type name 'float32xm8_t'

XuantieのRISC-V Vector向けリポジトリを試す

OpenBLASのコンパイルにはこのページで参照していたXuantiue向けのRISC-V GCCが必要となる。

msyksphinz.hatenablog.com

このリポジトリに格納されているGCCを使って、OpenBLASのLinux版は一応コンパイルできる。

make HOSTCC=gcc TARGET=C910V CC=riscv64-unknown-linux-gnu-gcc FC=riscv64-unknown-linux-gnu-gfortran

とりあえずやってみたが、Fortran向けのバイナリコンパイルの所でこけてしまう。これは大丈夫なのか?

make[2]: Leaving directory '/home/msyksphinz/work/riscv/openblas/OpenBLAS/lapack-netlib/LAPACKE'
make[1]: Leaving directory '/home/msyksphinz/work/riscv/openblas/OpenBLAS/lapack-netlib'
touch libopenblas_c910vp-r0.3.12.dev.a
make -j 8 -C test all
make[1]: Entering directory '/home/msyksphinz/work/riscv/openblas/OpenBLAS/test'
riscv64-unknown-linux-gnu-gfortran -O2 -Wall  -march=rv64gcvxthead -mabi=lp64v -static  -o sblat1 sblat1.o ../libopenblas_c910vp-r0.3.12.dev.a
riscv64-unknown-linux-gnu-gfortran -O2 -Wall  -march=rv64gcvxthead -mabi=lp64v -static  -o dblat1 dblat1.o ../libopenblas_c910vp-r0.3.12.dev.a
riscv64-unknown-linux-gnu-gfortran -O2 -Wall  -march=rv64gcvxthead -mabi=lp64v -static  -o cblat1 cblat1.o ../libopenblas_c910vp-r0.3.12.dev.a
riscv64-unknown-linux-gnu-gfortran -O2 -Wall  -march=rv64gcvxthead -mabi=lp64v -static  -o zblat1 zblat1.o ../libopenblas_c910vp-r0.3.12.dev.a
riscv64-unknown-linux-gnu-gfortran -O2 -Wall  -march=rv64gcvxthead -mabi=lp64v -static  -o sblat2 sblat2.o ../libopenblas_c910vp-r0.3.12.dev.a
riscv64-unknown-linux-gnu-gfortran -O2 -Wall  -march=rv64gcvxthead -mabi=lp64v -static  -o dblat2 dblat2.o ../libopenblas_c910vp-r0.3.12.dev.a
riscv64-unknown-linux-gnu-gfortran -O2 -Wall  -march=rv64gcvxthead -mabi=lp64v -static  -o cblat2 cblat2.o ../libopenblas_c910vp-r0.3.12.dev.a
riscv64-unknown-linux-gnu-gfortran -O2 -Wall  -march=rv64gcvxthead -mabi=lp64v -static  -o zblat2 zblat2.o ../libopenblas_c910vp-r0.3.12.dev.a
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: /home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/..
/lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/lib/libgfortran.a(fpu.o): in function `.L42':
(.text._gfortrani_set_fpu_trap_exceptions+0x74): warning: fedisableexcept is not implemented and will always fail
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: /home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/..
/lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/lib/libgfortran.a(fpu.o): in function `.L41':
(.text._gfortrani_set_fpu_trap_exceptions+0x64): warning: feenableexcept is not implemented and will always fail
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: /home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/..
/lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/lib/libgfortran.a(fpu.o): in function `_gfortrani_get_fpu_trap_exceptions':
(.text._gfortrani_get_fpu_trap_exceptions+0x12): warning: fegetexcept is not implemented and will always fail
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: ../libopenblas_c910vp-r0.3.12.dev.a(caxpy.o): can't link double-float modules with soft-floa
t modules
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: failed to merge target specific data of file ../libopenblas_c910vp-r0.3.12.dev.a(caxpy.o)
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: ../libopenblas_c910vp-r0.3.12.dev.a(cswap.o): can't link double-float modules with soft-floa
t modules
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: failed to merge target specific data of file ../libopenblas_c910vp-r0.3.12.dev.a(cswap.o)
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: ../libopenblas_c910vp-r0.3.12.dev.a(ccopy.o): can't link double-float modules with soft-floa
t modules
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: failed to merge target specific data of file ../libopenblas_c910vp-r0.3.12.dev.a(ccopy.o)
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: ../libopenblas_c910vp-r0.3.12.dev.a(cscal.o): can't link double-float modules with soft-floa
t modules
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: failed to merge target specific data of file ../libopenblas_c910vp-r0.3.12.dev.a(cscal.o)
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: ../libopenblas_c910vp-r0.3.12.dev.a(csscal.o): can't link double-float modules with soft-flo
at modules
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: failed to merge target specific data of file ../libopenblas_c910vp-r0.3.12.dev.a(csscal.o)
/home/msyksphinz/work3/work/riscv/xuantie-vector-demos/toolchain/riscv64-linux-x86_64/bin/../lib/gcc/riscv64-unknown-linux-gnu/8.1.0/../../../../riscv64-unknown-linux-gnu/bin/ld: ../libopenblas_c910vp-r0.3.12.dev.a(cdotc.o): can't link double-float modules with soft-floa
t modules

benchmark ディレクトリのコンパイルもこのままでは通らなかった。Fortranコンパイルがどうもおかしい気がするので、これをスキップしてみる。

make HOSTCC=gcc TARGET=C910V CC=riscv64-unknown-linux-gnu-gcc

これだと上手く行った。

-Wl,--whole-archive ../libopenblas_c910vp-r0.3.12.dev.a -Wl,--no-whole-archive \
-Wl,-soname,libopenblas.so.0 -lm -lpthread -lm -lpthread
riscv64-unknown-linux-gnu-gcc -O2 -DMAX_STACK_ALLOC=2048 -Wall -DF_INTERFACE_GFORT -fPIC -DNO_LAPACK -DNO_LAPACKE -DSMP_SERVER -DNO_WARMUP -DMAX_CPU_NUMBER=8 -DMAX_PARALLEL_NUMBER=1 -DBUILD_SINGLE=1 -DBUILD_DOUBLE=1 -DBUILD_COMPLEX=1 -DBUILD_COMPLEX16=1 -DVERSION=\"0.3.12.dev\" -march=rv64gcvxthead -mabi=lp64v -UASMNAME -UASMFNAME -UNAME -UCNAME -UCHAR_NAME -UCHAR_CNAME -DASMNAME= -DASMFNAME=_ -DNAME=_ -DCNAME= -DCHAR_NAME=\"_\" -DCHAR_CNAME=\"\" -DNO_AFFINITY -I..  -w -o linktest linktest.c ../libopenblas_c910vp-r0.3.12.dev.so  && echo OK.
OK.
rm -f linktest
make[1]: Leaving directory '/home/msyksphinz/work/riscv/openblas/OpenBLAS/exports'
 OpenBLAS build complete. (BLAS CBLAS)

  OS               ... Linux
  Architecture     ... riscv64
  BINARY           ... 64bit
  C compiler       ... GCC  (cmd & version : riscv64-unknown-linux-gnu-gcc (C-SKY RISCV Tools V1.9.1-910v B20200521) 8.1.0)
  Library Name     ... libopenblas_c910vp-r0.3.12.dev.a (Multi-threading; Max num-threads is 8)

To install the library, you can run "make PREFIX=/path/to/your/installation install".
f:id:msyksphinz:20210323000820p:plain

benchmarkをコンパイルしてみる。これも上手く行った。ただしこの段階ではLinux向けのバイナリなので上手くシミュレーションできない。いくつか修正が必要だろう。

cd benchmark
make HOSTCC=gcc TARGET=C910V CC=riscv64-unknown-linux-gnu-gcc

RISC-V Testに実装されているページテーブルの実装を調査する (3. 仮想アドレスからのジャンプを調査する)

riscv-testsには、もう一つ、vm.cevict()という関数が用意されている。evict()はどうもECALLが呼び出されたときに実行されるようだ。ECALLはテストパタンが終了したときに呼ばれるので、最後の処理に使用されるようだ。

void handle_trap(trapframe_t* tf)
{
  if (tf->cause == CAUSE_USER_ECALL)
  {
    int n = tf->gpr[10];

    for (long i = 1; i < MAX_TEST_PAGES; i++)
      evict(i*PGSIZE);

    terminate(n);
  }

最後にterminate()が呼び出されるので、終了前の処理であることは間違いない。evict()なので読みだしたページテーブルを吐き出しているのだろうけども、良く分からないのがmemcpyの方向がページテーブル割り当ての時と同じこと。カーネルモードのアドレスと、ユーザモードのアドレスが一致している場合にはmemcpy()は実行しないが一致していない場合にはmemcpy()でデータをコピーする。

static void evict(unsigned long addr)
{
  assert(addr >= PGSIZE && addr < MAX_TEST_PAGES * PGSIZE);
  addr = addr/PGSIZE*PGSIZE;

  freelist_t* node = &user_mapping[addr/PGSIZE];
  if (node->addr)
  {
    // check accessed and dirty bits
    assert(user_l3pt[addr/PGSIZE] & PTE_A);
    uintptr_t sstatus = set_csr(sstatus, SSTATUS_SUM);
    if (memcmp((void*)addr, uva2kva(addr), PGSIZE)) {
      assert(user_l3pt[addr/PGSIZE] & PTE_D);
      memcpy((void*)addr, uva2kva(addr), PGSIZE);
    }
    write_csr(sstatus, sstatus);
    /* ... 中略 ... */
      

ログを見てみると、0xffffから始まるユーザモードから、0x0000から始まるカーネルモードへのコピーをしているようだ。

     13430:S:Sv39:ffffffffffe027b0:P0000800027b0:000d8593:addi       x11,x27,0x000        :x27=>ffffffffffe04000 x11<=ffffffffffe04000 
     13431:S:Sv39:ffffffffffe027b4:P0000800027b4:00040513:addi       x10,x08,0x000        :x08=>0000000000004000 x10<=0000000000004000 
     13432:S:Sv39:ffffffffffe027b8:P0000800027b8:849ff0ef:jal        x01,43231            :x01<=ffffffffffe027bc pc<=ffffffffffe02000 
     // x10 = カーネルモードでのメモリ領域
     // x11 = ユーザモードでのメモリ領域

新たに割り当てたNew Pageからカーネルの用意しているページへデータを書き戻しているものと思われる、が、新たに確保したページ側に書き戻しているのはなんでだ?

     13441:S:Sv39:ffffffffffe02044:P000080002044:ff85b703:ld         x14,0xff8(x11)       :x11=>ffffffffffe04008 (0000000080004000)=>deadbeefdeadbeef x14<=deadbeefdeadbeef 
     13443:S:Sv39:ffffffffffe0204c:P00008000204c:fee7bc23:sd         x14,0xff8(x15)       :x15=>0000000000004008 x14=>deadbeefdeadbeef (0000000080064000)<=deadbeefdeadbeef 

RISC-V Testに実装されているページテーブルの実装を調査する (2. 仮想アドレスからのジャンプを調査する)

pop_tf()によるジャンプで、仮想アドレスで言う所の0x2ac8へのジャンプとなる。これによりユーザモードに移るわけだが、この時のアドレス計算を見てみよう。ジャンプ先は0x2ac8(仮想アドレス)なので、以下のような計算となる。

  • SATP0x8000000000080004なので、0x8000_4000が仮想アドレス計算の起点となっている(これはページテーブルの起点:ptの場所となっている )
  • 0x80004000pt[0]の起点なので、以下のコードによりpt[1]の先頭へのジャンプとなっている。
    • 0x8000_5000へのページテーブルのジャンプとなっている。
  • 0x80005000pt[1]の先頭なので、pt[3]へのジャンプとなっている。
  • ところがpt[3]は初期化されていないので、ページテーブル例外が発生する。
f:id:msyksphinz:20210320230939p:plain

ページテーブル例外を処理するのは、handle_trap()である。さらにページテーブル例外のためのhandle_fault()を呼び出す。

このhandle_fault()medelegによりスーパーバイザーモードで処理される。スーパーバイザーモードの例外での飛び先はstvecにより指定されている。

void handle_trap(trapframe_t* tf)
{
  if (tf->cause == CAUSE_USER_ECALL)
  /* ... 中略 ... */
  else if (tf->cause == CAUSE_ILLEGAL_INSTRUCTION)
  /* ... 中略 ... */
  else if (tf->cause == CAUSE_FETCH_PAGE_FAULT || tf->cause == CAUSE_LOAD_PAGE_FAULT || tf->cause == CAUSE_STORE_PAGE_FAULT)
    handle_fault(tf->badvaddr, tf->cause);
  else
    assert(!"unexpected exception");

  pop_tf(tf);
}

stvecの計算方法は以下の通り。pa2kva()を使っているのでkernel_l2pt()を経由してジャンプする仕組みになっている。

  write_csr(stvec, pa2kva(trap_entry));
f:id:msyksphinz:20210320231008p:plain

handle_fault()の仕組みは以下のようになっている。

f:id:msyksphinz:20210320231100p:plain

すでにページが割り当てられていない場合には、新たなページをfreelistから割り当て、PTEにそのページを設定する。

f:id:msyksphinz:20210320231127p:plain

さらにそこから先のuser_mappingへの割り当ては、さらに何をやっているのか良く分からない。

RISC-V Testに実装されているページテーブルの実装を調査する

ハイパーバイザーの勉強をしている中で、2段ページテーブルの実装の中でそもそも私は普通のスーパーバイザーでのページテーブルの仕組みをしっかり理解できていないことに気が付いた。 ここでは、riscv_testsに実装されているページテーブルの実装と仮想アドレスを読みながら、その仕組みを理解していこうと思う。

f:id:msyksphinz:20210320001918p:plain

  • vm_boot()での動作
  // map user to lowermost megapage
  l1pt[0] = ((pte_t)user_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;

l1ptl2ptというのが定義されているが、これはptの配列の1つの目と2つ目が指定されているらしい。

#define l1pt pt[0]
#define user_l2pt pt[1]

pte_t pt[NPT][PTES_PER_PT] __attribute__((aligned(PGSIZE)));

#if __riscv_xlen == 64
# define NPT 4
#define kernel_l2pt pt[2]
# define user_l3pt pt[3]
#else
# define NPT 2
# define user_l3pt user_l2pt
#endif

// riscv_test.h
typedef unsigned long pte_t;
#define LEVELS (sizeof(pte_t) == sizeof(uint64_t) ? 3 : 2)
#define PTIDXBITS (PGSHIFT - (sizeof(pte_t) == 8 ? 3 : 2))
#define VPN_BITS (PTIDXBITS * LEVELS)
#define VA_BITS (VPN_BITS + PGSHIFT)
#define PTES_PER_PT (1UL << RISCV_PGLEVEL_BITS)
#define MEGAPAGE_SIZE (PTES_PER_PT * PGSIZE)

RV64の場合は、

pte_t pt[NPT][PTES_PER_PT] __attribute__((aligned(PGSIZE)));
unsigned long pt[4][1UL << 9] __attribute__((aligned(PGSIZE)));

という構成になる。ptが4つから構成されており、それぞれがVPNのサイズだけ定義されている。ptの1つのエントリに1つのPTEが格納されるという感じだろうか。ダンプしてみるとptは16KiBだけアサインされているので、1ブロックが4KiBで、512エントリ、ということは1エントリが8バイトということになる。

    12: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS vm.c
    13: 0000000080002230    44 FUNC    LOCAL  DEFAULT    3 terminate
    14: 000000008000218c    28 FUNC    GLOBAL HIDDEN     3 strcpy
    15: 00000000800021a8   136 FUNC    GLOBAL HIDDEN     3 atol
    16: 0000000080003000 16384 OBJECT  GLOBAL HIDDEN     5 pt   // 16KiBアサインされている
    17: 0000000080002000    92 FUNC    GLOBAL HIDDEN     3 memcpy
    18: 0000000080007000  1008 OBJECT  GLOBAL HIDDEN     5 freelist_nodes
    19: 00000000800028d4   500 FUNC    GLOBAL HIDDEN     3 vm_boot
    20: 000000008000259c   824 FUNC    GLOBAL HIDDEN     3 handle_trap

4ブロックがそれぞれ

  • l1pt
  • user_l2pt
  • kernel_l2pt
  • user_l3pt

となっているが、今のところ意味が分からない。

という所でマップをもう一度読み直してみると、最初のマップは、user_l2ptへのポインタとなっているので、これは普通にジャンプするためのページテーブルとなっている。

  // map user to lowermost megapage
  l1pt[0] = ((pte_t)user_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;

user_l2ptの先頭はuser_l3ptへのジャンプとなっている。

l1ptの最後のエントリは、kernel_l2ptのへのジャンプとなっている。

さらにkernel_l2ptの最後のエントリはDRAM_BASE(つまり0x80000000)へのテーブルとなっており、これはLeafテーブルとなっている。

#if __riscv_xlen == 64
  l1pt[PTES_PER_PT-1] = ((pte_t)kernel_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;
  kernel_l2pt[PTES_PER_PT-1] = (DRAM_BASE/RISCV_PGSIZE << PTE_PPN_SHIFT) | PTE_V | PTE_R | PTE_W | PTE_X | PTE_A | PTE_D;
  user_l2pt[0] = ((pte_t)user_l3pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;
  uintptr_t vm_choice = SATP_MODE_SV39;
#else

SATP(sptbr)にl1ptの先頭アドレスを格納されることで仮想アドレス変換の先頭アドレスとして使用されるようになる。

    write_csr(sptbr, ((uintptr_t)l1pt >> PGSHIFT) |
                     (vm_choice * (SATP_MODE & ~(SATP_MODE<<1))));
f:id:msyksphinz:20210320001951p:plain

スーパーバイザー向けのトラップは以下のようになっていた。これはどういう計算だ?

  // set up supervisor trap handling
  write_csr(stvec, pa2kva(trap_entry));
  write_csr(sscratch, pa2kva(read_csr(mscratch)));
  write_csr(medeleg,
    (1 << CAUSE_USER_ECALL) |
    (1 << CAUSE_FETCH_PAGE_FAULT) |
    (1 << CAUSE_LOAD_PAGE_FAULT) |
    (1 << CAUSE_STORE_PAGE_FAULT));
  // FPU on; accelerator on; allow supervisor access to user memory access
  write_csr(mstatus, MSTATUS_FS | MSTATUS_XS);
  write_csr(mie, 0);
#define pa2kva(pa) ((void*)(pa) - DRAM_BASE - MEGAPAGE_SIZE)

#define MEGAPAGE_SIZE (PTES_PER_PT * PGSIZE)

trap_entryは0x0800000c4に配置されているので、0x800000c4 - 0x80000000 - 512 x 4KiB = 0x8000000c4 - (0x80000000 + 512 x 16KiB)となり、 0xFFFF_FFFF_FFE0_00C4となる。この計算の仕組みはどういうことだろう?0x8000_0000から512番目のページテーブルにページを配置するということかな?

次に、malloc()で割り当てるメモリマップを確保しているものと思われる。

  freelist_head = pa2kva((void*)&freelist_nodes[0]);
  freelist_tail = pa2kva(&freelist_nodes[MAX_TEST_PAGES-1]);
  for (long i = 0; i < MAX_TEST_PAGES; i++)
  {
    freelist_nodes[i].addr = DRAM_BASE + (MAX_TEST_PAGES + random)*PGSIZE;
    freelist_nodes[i].next = pa2kva(&freelist_nodes[i+1]);
    random = LFSR_NEXT(random);
  }
  freelist_nodes[MAX_TEST_PAGES-1].next = 0;

最後にtrapframetest_addr - DRAM_BASEを設定している。test_addruserstartに設定されているので、0x80002ac8 - 0x80000000 = 0x2ac8が設定されてトラップフレームを経由してスーパーバイザーモードに移行する。

    22: 000000008000225c    16 FUNC    GLOBAL HIDDEN     3 wtf
    23: 00000000800077e0     8 OBJECT  GLOBAL HIDDEN     5 freelist_tail
    24: 0000000080002ac8     0 NOTYPE  GLOBAL DEFAULT    3 userstart
    25: 00000000800077e8     8 OBJECT  GLOBAL HIDDEN     5 freelist_head
    26: 0000000080003000     0 NOTYPE  GLOBAL DEFAULT    4 begin_signature