FPGA開発日記

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

RISC-Vテストパタン VMモードの解析

前回に引き続き、Sv32で動作させるときのテストパタンの動作について解析している。

RISC-Vのテストパタンは、基本的なテストを行うxxx.Sのアセンブラリストと、それを囲むプロローグとエピローグから構成されている。

中心となる検証用アセンブラリストは、以下のようなものだ。

riscv-tests/add.S at master · riscv/riscv-tests · GitHub

  #-------------------------------------------------------------
  # Arithmetic tests
  #-------------------------------------------------------------

  TEST_RR_OP( 2,  add, 0x00000000, 0x00000000, 0x00000000 );
  TEST_RR_OP( 3,  add, 0x00000002, 0x00000001, 0x00000001 );
  TEST_RR_OP( 4,  add, 0x0000000a, 0x00000003, 0x00000007 );

  TEST_RR_OP( 5,  add, 0xffffffffffff8000, 0x0000000000000000, 0xffffffffffff8000 );
  TEST_RR_OP( 6,  add, 0xffffffff80000000, 0xffffffff80000000, 0x00000000 );
  TEST_RR_OP( 7,  add, 0xffffffff7fff8000, 0xffffffff80000000, 0xffffffffffff8000 );

  TEST_RR_OP( 8,  add, 0x0000000000007fff, 0x0000000000000000, 0x0000000000007fff );
  TEST_RR_OP( 9,  add, 0x000000007fffffff, 0x000000007fffffff, 0x0000000000000000 );
  TEST_RR_OP( 10, add, 0x0000000080007ffe, 0x000000007fffffff, 0x0000000000007fff );

  TEST_RR_OP( 11, add, 0xffffffff80007fff, 0xffffffff80000000, 0x0000000000007fff );
  TEST_RR_OP( 12, add, 0x000000007fff7fff, 0x000000007fffffff, 0xffffffffffff8000 );

こんな感じのテストパタンが、ずらっと並んでいる。これを囲むのがプロローグ関数とエピローグ関数なのだが、まずはプロローグから見ていこう。

Sv32の変換モードを設定するプロローグ関数vm_boot()

env/v/vm.cに記述されているvm_boot()は、仮想メモリアドレスの変換設定をしている中心となコードだ。

void vm_boot(uintptr_t test_addr)
{
...

  write_csr(sptbr, (uintptr_t)l1pt >> PGSHIFT);
  // map kernel to uppermost megapage
  l1pt[PTES_PER_PT-1] = ((pte_t)kernel_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;
  // map user to lowermost megapage
  l1pt[0] = ((pte_t)user_l2pt >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V;
...

sptbrレジスタを指定しており、仮想メモリアドレスモードに入ったときのテーブルウォークを行うベースアドレスを設定している。 ここでは、l1pt = pt[0]を指しており、これはページテーブルの先頭に値する。ここからページテーブルをたどって行き、目的のアドレスに到達するわけだ。

...
  // 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_FAULT_FETCH) |
    (1 << CAUSE_FAULT_LOAD) |
    (1 << CAUSE_FAULT_STORE));

mscratchレジスタの値はtrap_vectorから少し先、トラップが発生したときのアドレスを格納しており、これをsscratchにコピーしている。 この際、スーパバイザモードでは仮想アドレスモードで動作するので、あらかじめpa2kvaで物理アドレスから仮想アドレスに値を変換して格納している。

さらに、権限委譲のためのレジスタを設定しており、ユーザモードからのトラップ、メモリアクセス関連の例外はすべてスーパバイザが処理するように設定された。

この後、テストパタンがなぜか指定のページに飛ばない問題をずっと追いかけていたのだが、どうもテストパタンのスタートアドレスを指定する方式に問題があるようだった。

  trapframe_t tf;
  memset(&tf, 0, sizeof(tf));
  tf.epc = test_addr - DRAM_BASE;
  pop_tf(&tf);

これ、test_addrからDRAM_BASE`を減算するだけでなぜOKなんだろう?そもそも仮想アドレスから物理アドレスへの変換は、

riscv-test-env/vm.c at 9e219c9ca70459bfda9067d637bb8bf52c5f0326 · riscv/riscv-test-env · GitHub

#define pa2kva(pa) ((void*)(pa) - DRAM_BASE - MEGAPAGE_SIZE)

で行われるはずだ。じゃあtest_addrの変換も、同様に変換しなければならないだろう。

  trapframe_t tf;
  memset(&tf, 0, sizeof(tf));
  // tf.epc = test_addr - DRAM_BASE;
  tf.epc = pa2kva(test_addr);
  pop_tf(&tf);

が、正解じゃないの?

テストが終了した後のエピローグ関数

テストが終了すると、テストがPassしたかFailしたかを判定しなければならないのだが、それをriscv-testsではコードとして管理している。

まずテストが終了すると、ECALL(Supervisor Trap)を発行させて、マシンモードに行こうする。(mdelegで"Ecall from Supervisor"は有効になっていないので、スーパバイザモードからマシンモードに素直に移行するはずだ。

つぎに最後のコードを見て、最終判断をするわけだが、最後の判定はhandle_trap()にて行われている。

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);
  }
  else if (tf->cause == CAUSE_ILLEGAL_INSTRUCTION)
  {
    assert(tf->epc % 4 == 0);
...

ユーザモードでないとterminateが実行されない仕組みになっている。そんなことないでしょ!これまでずっとスーパバイザモードでテストしてたのに!

という訳で、

  if (tf->cause == CAUSE_USER_ECALL)

は、

  if (tf->cause == CAUSE_SUPERVISOR_ECALL)

では無かろうか?これらを変更すると、無事に自作ISSでテストパタンがパスした。 これらの問題はRISC-Vのメーリングリストに投げて、質問している。