前回の続きで、どうしても自作ISSとSpike-ISSの動作結果が一致しないところがあり、何が悪いんだろうと調べていた。
まず、spikeにはRISCV_ENABLE_COMMITLOG
というログ出力用のフォーマットがある事は紹介した。
これだけでは情報としては不十分で、例えば実際にどのアドレスに対してメモリアクセスを発行したか等の情報を取得したい。
これらの情報はdefineで切ってあるわけではないので、自分で追加する必要がある。
RISC-V Spike-ISS の命令実行の仕組み
まず、RISC-V Spike-ISSはどのようにして命令をシミュレートしているのか、その仕組みを解き明かしてみる。
例えば、lw命令を実行する記述はどこに書いてあるかというと、grepしてみると以下のようなファイルを見つけることが出来る。
riscv/insns/c_lw.h WRITE_RVC_RS2S(MMU.load_int32(RVC_RS1S + insn.rvc_lw_imm()));
一見、C++には見えないのだが、これはdefineが切りまくってあるC++だ。よく見ると、RSフィールドで指定される汎用レジスタに対して、RS1フィールド+即値フィールドを加算したアドレスでメモリアクセスした結果のデータを格納しているのが分かるだろう。
ここが気になった理由は、RISC-V 自作ISSとSpike-ISSの違いが、アドレス0x0000100cのメモリデータの違いとなって現れたからだ。
- Spike-ISSの結果
core 0: 0x000000008000117c (0x0007a783) lw a5, 0(a5) 3 0x000000008000117c (0x0007a783) x15 0x0000000000001020
- 自作ISSの結果
210822:M:MBar:[8000117c][P8000117c] 0007a783 : lw r15,0x000(r15) r15=>000000000000100c (000000000000100c)=>80001178 r15<=ffffffff80001178
という訳で、ロード結果はどのようにして取り扱われるのかを調べていたのだが、Spikeにおけるメモリアクセスの記述は、以下だ。
- mmu.h
// template for functions that load an aligned value from memory #define load_func(type) \ inline type##_t load_##type(reg_t addr) { \ if (addr & (sizeof(type##_t)-1)) \ throw trap_load_address_misaligned(addr); \ reg_t vpn = addr >> PGSHIFT; \ if (likely(tlb_load_tag[vpn % TLB_ENTRIES] == vpn)) \ return *(type##_t*)(tlb_data[vpn % TLB_ENTRIES] + addr); \ if (unlikely(tlb_load_tag[vpn % TLB_ENTRIES] == (vpn | TLB_CHECK_TRIGGERS))) { \ type##_t data = *(type##_t*)(tlb_data[vpn % TLB_ENTRIES] + 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) // load value from memory at aligned address; sign extend to register width load_func(int8) load_func(int16) load_func(int32) load_func(int64)
メチャメチャdefineが切ってあるが、C++言語だ。つまり、アクセスサイズごとにメモリ関数が定義されており、
- アドレスアラインが間違っていれば、トラップ発生
- TLBに引っかかれば、TLB変換
- そうでない場合は、通常アクセス (
load_slow_path
)
load_slow_path()
の中を見てみよう。
void mmu_t::load_slow_path(reg_t addr, reg_t len, uint8_t* bytes) { reg_t paddr = translate(addr, LOAD); if (sim->addr_is_mem(paddr)) { memcpy(bytes, sim->addr_to_mem(paddr), len); if (tracer.interested_in_range(paddr, paddr + PGSIZE, LOAD)) tracer.trace(paddr, len, LOAD); else refill_tlb(addr, paddr, LOAD); } else if (!sim->mmio_load(paddr, len, bytes)) { throw trap_load_access_fault(addr); } if (!matched_trigger) { reg_t data = reg_from_bytes(len, bytes); matched_trigger = trigger_exception(OPERATION_LOAD, addr, data); if (matched_trigger) throw *matched_trigger; } }
printfデバッグ(笑)をしていくと、どうやら0x100cはaddr_is_mem
には引っかからないらしい。つまり特殊アドレスとして処理されるということか?
else節でMMIO_Loadが呼ばれている。
bool sim_t::mmio_load(reg_t addr, size_t len, uint8_t* bytes) { if (addr + len < addr) return false; return bus.load(addr, len, bytes); }
これはどこからロードされているんだろう?別のデバイスから引かれているんだろうか。調査要。
次に引っかかった、reg_from_bytes()
を調べていくと、
reg_t reg_from_bytes(size_t len, const uint8_t* bytes) { switch (len) { case 1: return bytes[0]; case 2: return bytes[0] | (((reg_t) bytes[1]) << 8); case 4: return bytes[0] | (((reg_t) bytes[1]) << 8) | (((reg_t) bytes[2]) << 16) | (((reg_t) bytes[3]) << 24); case 8: return bytes[0] | (((reg_t) bytes[1]) << 8) | (((reg_t) bytes[2]) << 16) | (((reg_t) bytes[3]) << 24) | (((reg_t) bytes[4]) << 32) | (((reg_t) bytes[5]) << 40) | (((reg_t) bytes[6]) << 48) | (((reg_t) bytes[7]) << 56); } abort(); }
なんでこれで、mmioからロードされた値が並べられる、ということか。