はじめに
追記しました(3/28)
題名がもうなんだかわからなくなってきたが、とりあえずCLINT編の続き。
前回、michaeljclark/riscv-probeの割り込みベクタとハンドラの部分を調査した。
実際にこの部分を動かしているサンプルであるprobe.cを実行してみたところ、割り込みの発生要因がIllegal instruction
しかなかった。
自分で任意にソフトウェア割り込みを発生させて、それをトラップしてみたいと思ったので実験することにした。
割り込みベクタの処理を読み直す
前回は、trap_vectorの処理をさらっと読みすぎて重要なことを素通りしてしまった。
sxsp/lxspって何?
trap_vector: # Save registers. addi sp, sp, -CONTEXT_SIZE sxsp ra, 0 sxsp a0, 1 sxsp a1, 2 sxsp a2, 3 sxsp a3, 4 sxsp a4, 5 sxsp a5, 6 sxsp a6, 7 sxsp a7, 8 sxsp t0, 9 sxsp t1, 10 sxsp t2, 11 sxsp t3, 12 sxsp t4, 13 sxsp t5, 14 sxsp t6, 15 # Invoke the handler. mv a0, sp csrr a1, mcause csrr a2, mepc jal trap_handler # Restore registers. lxsp ra, 0 lxsp a0, 1 lxsp a1, 2 lxsp a2, 3 lxsp a3, 4 lxsp a4, 5 lxsp a5, 6 lxsp a6, 7 lxsp a7, 8 lxsp t0, 9 lxsp t1, 10 lxsp t2, 11 lxsp t3, 12 lxsp t4, 13 lxsp t5, 14 lxsp t6, 15 addi sp, sp, CONTEXT_SIZE # Return mret
スタックポインタに書き込む命令なんだと思ったが、Google先生に聞いても何一つ情報が見つからない。
それもそのはず、自前のマクロだった。
riscv-probe/env/common/rv32/macros.s
とriscv-probe/env/common/rv64/macros.s
にそれぞれ定義があった。
v32のmacros.sを示す。
.equ REGBYTES, 4 .macro lx a, b lw \a, \b .endm .macro sx a, b sw \a, \b .endm .macro lxsp a, b lw \a, ((\b)*REGBYTES)(sp) .endm .macro sxsp a, b sw \a, ((\b)*REGBYTES)(sp) .endm .macro .ptr a .4byte \a .endm
次にrv64のmacro.s
.equ REGBYTES, 8 .macro lx a, b ld \a, \b .endm .macro sx a, b sd \a, \b .endm .macro lxsp a, b ld \a, ((\b)*REGBYTES)(sp) .endm .macro sxsp a, b sd \a, ((\b)*REGBYTES)(sp) .endm .macro .ptr a .8byte \a .endm
要するに32/64のビット幅を吸収している。
やっている事自体は、スタックにレジスタを退避/復帰しているってことで間違いはない。
自分でソフトウェア割り込みを発生する
トラップの部分はriscv-probeをそのまま使用できるので、割り込みを自分で発生させる方法を調べた。
処理手順としては次のような感じ。
msipのLSBを1に設定すると割り込みが発生する。
この処理を追加するのはどこでも良かったのだが、とりあえずriscv-probe/env/qemu-sifive_e/crt.s
に
こんな感じで処理を追加した。
.equ RISCV_MSTATUS_MIE, 0x08 .equ RISCV_MIE_MSIE, 0x08 .text .globl soft_intr .type soft_intr,@function soft_intr: li t0, RISCV_MSTATUS_MIE csrrs zero, mstatus, t0 /* mstatus.mie = 1 */ li t0, RISCV_MIE_MSIE csrrs zero, mie, t0 /* mie.msie = 1 */ lui t0, 0x02000 /* t0 = 0x02000000(msip for hart0)*/ li t1, 0x1 /* t1 = 1 */ sw t1, (t0) /* *(0x02000000) = 1 */ ret
超ベタ書き。
割り込みハンドラの方で、要因をクリアしないと割り込みが入り続けるので次のようにした。
static void trap_save_cause(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc) { unsigned int mie; printf("."); mie = read_csr(mie); /* mie読み込み */ mie &= ~0x8; /* mie.msieクリア */ write_csr(mie, mie); /* mie書き込み */ write_csr(mepc, mepc + 4); }
csrrc
を使えば1発でクリアできるので、少し冗長だが今回は実験なのでこれでよし。
これは間違い。これでは次にmie.msieに1をセットした時点で割り込みが発生してしまう。 ただ割り込みを許可しただけで次の割り込みが発生するのはおかしい。
あと、復帰時点のPCを設定するためのwrite_csr(mepc, mepc + 4);
の行も不要。これがあるとおかしくなる。
何もしなくてもちゃんと割り込み時点のpcの次の命令から実行してくれるっぽい。
最終的にはこんな感じ。
static void trap_save_cause(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc) { unsigned int mie; volatile unsigned int *msip = (volatile unsigned int *)0x2000000; printf(":(0x%08x):\n", *msip); *msip = 0; write_csr(mcause, 0x0); clear_csr(mie, 0x8); /*write_csr(mepc, mepc + 4);*/ }
感触としてはmcauseのクリアは不要な気がする。 次の割り込みが入った時点で上書きされるだろうし。
CLINT.msipをクリアしてやる必要があるというの正しい答えだと思われる。
実験に使ったメイン関数。
int main(int argc, char **argv) { char buf[32]; unsigned int mcause; printf("isa: %s\n", isa_string(buf, sizeof(buf))); set_trap_fn(trap_save_cause); mcause = read_csr(mcause); printf("01 mcause(%08x)\n", mcause); soft_intr(); mcause = read_csr(mcause); printf("02 mcause(%08x)\n", mcause); write_csr(mie, 0x8); printf("done\n"); return 0; }
実行結果はこうなる。
isa: rv32imacu 01 mcause(00000000) :(0x00000001): 02 mcause(00000000) done
write_csr(mie, 0x8)
の行で割り込みが発生していない。
まとめ
CLINT完全に理解してなかった。 写経だけでは不十分なこともある。