RISC-Vシミュレータで最も信頼できる実装はSpikeシミュレータである。 SpikeシミュレータはC++で書かれており、比較的簡単に解析ができるが、RTLとの実装の違いを確認したり、ソフトウェアの動作を確認したい場合に適用できるTipsがいろいろある。
各命令でアップデートされたレジスタ値をログに残したい
ビルド時に以下のオプションを追加することでレジスタ書き込みのログを残すことができる。
--enable-commitlog=yes
をオプションに追加することで、ログを増やすことが可能だ。
build.sh
diff --git a/build-spike-pk.sh b/build-spike-pk.sh index 02f3202..09e1512 100755 --- a/build-spike-pk.sh +++ b/build-spike-pk.sh @@ -14,7 +14,7 @@ fi echo "Starting RISC-V Toolchain build process" build_project riscv-fesvr --prefix=$RISCV -build_project riscv-isa-sim --prefix=$RISCV --with-fesvr=$RISCV +build_project riscv-isa-sim --prefix=$RISCV --with-fesvr=$RISCV --enable-commitlog=yes CC= CXX= build_project riscv-pk --prefix=$RISCV --host=riscv64-unknown-elf echo -e "\\nRISC-V Toolchain installation completed!"
ついでに、sed等でログを処理したい場合に備えて、レジスタアドレスを2桁で0埋めしておく。
riscv-isa-sim/riscv/execute.cc
@@ -52,7 +54,7 @@ static void commit_log_print_insn(state_t* state, reg_t pc, insn_t insn) bool fp = reg.addr & 1; int rd = reg.addr >> 1; int size = fp ? flen : xlen; - fprintf(stderr, ") %c%2d ", fp ? 'f' : 'x', rd); + fprintf(stderr, ") %c%02d ", fp ? 'f' : 'x', rd); commit_log_print_value(size, reg.data.v[1], reg.data.v[0]);
--enable-commitlog=no
(ログ無し版)
core 0: 0xffffffe00092bfd8 (0x00001101) addi sp, sp, -32 core 0: 0xffffffe00092bfda (0x0000ec06) sd ra, 24(sp) core 0: 0xffffffe00092bfdc (0x0000e822) sd s0, 16(sp) core 0: 0xffffffe00092bfde (0x0000e426) sd s1, 8(sp) core 0: 0xffffffe00092bfe0 (0x00001000) addi s0, sp, 32 core 0: 0xffffffe00092bfe2 (0x003d5497) auipc s1, 0x3d5 core 0: 0xffffffe00092bfe6 (0x69e48493) addi s1, s1, 1694 core 0: 0xffffffe00092bfea (0x0000e088) sd a0, 0(s1) core 0: 0xffffffe00092bfec (0x6e6980ef) jal pc + 0x986e6
--enable-commitlog=yes
(ログあり版)
core 0: 143191650: 0xffffffe00092bfd8 (0x00001101) addi sp, sp, -32 1 0xffffffe00092bfd8 (0x1101) x02 0xffffffe079a51ea0 core 0: 143191651: 0xffffffe00092bfda (0x0000ec06) sd ra, 24(sp) 1 0xffffffe00092bfda (0xec06) core 0: 143191652: 0xffffffe00092bfdc (0x0000e822) sd s0, 16(sp) 1 0xffffffe00092bfdc (0xe822) core 0: 143191653: 0xffffffe00092bfde (0x0000e426) sd s1, 8(sp) 1 0xffffffe00092bfde (0xe426) core 0: 143191654: 0xffffffe00092bfe0 (0x00001000) addi s0, sp, 32 1 0xffffffe00092bfe0 (0x1000) x08 0xffffffe079a51ec0 core 0: 143191655: 0xffffffe00092bfe2 (0x003d5497) auipc s1, 0x3d5 1 0xffffffe00092bfe2 (0x003d5497) x09 0xffffffe000d00fe2 core 0: 143191656: 0xffffffe00092bfe6 (0x69e48493) addi s1, s1, 1694 1 0xffffffe00092bfe6 (0x69e48493) x09 0xffffffe000d01680 core 0: 143191657: 0xffffffe00092bfea (0x0000e088) sd a0, 0(s1) 1 0xffffffe00092bfea (0xe088) core 0: 143191658: 0xffffffe00092bfec (0x6e6980ef) jal pc + 0x986e6 1 0xffffffe00092bfec (0x6e6980ef) x01 0xffffffe00092bff0 core 0: 143191659: 0xffffffe0009c46d2 (0x00007139) addi sp, sp, -64 1 0xffffffe0009c46d2 (0x7139) x02 0xffffffe079a51e60 core 0: 143191660: 0xffffffe0009c46d4 (0x0000f822) sd s0, 48(sp) 1 0xffffffe0009c46d4 (0xf822)
ちなみに、追加されたログの意味であるが、
1 0xffffffe00092bfd8 (0x1101) x02 0xffffffe079a51ea0 - ------------------ ------ --- ------------------ | | | | | | | | | +--------- レジスタ書き込みデータ | | | +-------------------- レジスタ書き込みアドレス | | +-------------------------- 命令Hex | +---------------------------------------- 命令アドレス +--------------------------------------------------- プロセッサの実行モード(0:User, 1:Supervisor, 3:Machine)
仮想アドレスから物理アドレスへの変換過程を追いかける
これはmmu.c
に実装されている。walk()
関数を見てみよう。
riscv-isa-sim/riscv/mmu.cc
reg_t mmu_t::walk(reg_t addr, access_type type, reg_t mode) { vm_info vm = decode_vm_info(proc->max_xlen, mode, proc->get_state()->satp); if (vm.levels == 0) return addr & ((reg_t(2) << (proc->xlen-1))-1); // zero-extend from xlen ... reg_t value = (ppn | (vpn & ((reg_t(1) << ptshift) - 1))) << PGSHIFT; return value; } } fail: switch (type) { case FETCH: throw trap_instruction_page_fault(addr); case LOAD: throw trap_load_page_fault(addr); ...
時によって物理アドレスへの変換が行われていない?
それはTLBが実装されているから。TLBを使用する、Load/Store系の命令は以下で定義されている。
riscv-isa-sim/riscv/mmu.h
#define load_func(type) \ inline type##_t load_##type(reg_t addr) { \ if (unlikely(addr & (sizeof(type##_t)-1))) \ return misaligned_load(addr, sizeof(type##_t)); \ reg_t vpn = addr >> PGSHIFT; \ if (likely(tlb_load_tag[vpn % TLB_ENTRIES] == vpn)) \ return *(type##_t*)(tlb_data[vpn % TLB_ENTRIES].host_offset + addr); \ if (unlikely(tlb_load_tag[vpn % TLB_ENTRIES] == (vpn | TLB_CHECK_TRIGGERS))) { \ type##_t data = *(type##_t*)(tlb_data[vpn % TLB_ENTRIES].host_offset + addr); \ if (!matched_trigger) { \ matched_trigger = trigger_exception(OPERATION_LOAD, addr, data); \ if (matched_trigger) \ throw *matched_trigger; \ } \ return data; \ } \ type##_t res; \ load_slow_path(addr, sizeof(type##_t), (uint8_t*)&res); \ return res; \ } // load value from memory at aligned address; zero extend to register width load_func(uint8) load_func(uint16) load_func(uint32) load_func(uint64)
したがって、#define load_func
から、TLB関係の処理をすべて抜けば、仮想アドレスから物理アドレスの変換が常に行われることになる。
Trapはどのように実装されているの?
それでは、タイマ割り込みを見てみる。
RISC-Vにおいて、MIE(Machine Mode Interrupt Enable)とMIP(Machine Mode Interrupt Pending)レジスタで、同じビットが両方とも1が立っていれば割り込みが発生することになる。
実装はriscv/processor.cc
に記述されている。
riscv-isa-sim/riscv/processor.cc
void processor_t::take_interrupt(reg_t pending_interrupts) { ... throw trap_t(((reg_t)1 << (max_xlen-1)) | ctz(enabled_interrupts)); } }
最後にトラップが発生していることが分かる。
このトラップはriscv/execute.cc
でCatchされる。take_trapで実際の割り込み処理が実行される。
riscv-isa-sim/riscv/execute.cc
// fetch/decode/execute loop void processor_t::step(size_t n) { if (state.dcsr.cause == DCSR_CAUSE_NONE) { if (halt_request) { enter_debug_mode(DCSR_CAUSE_DEBUGINT); ... catch(trap_t& t) { take_trap(t, pc); n = instret; if (unlikely(state.single_step == state.STEP_STEPPED)) { state.single_step = state.STEP_NONE; enter_debug_mode(DCSR_CAUSE_STEP); } }