前回に引き続き、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のメーリングリストに投げて、質問している。