Binary Translation方式の命令セットエミュレータのRust実装をしている。前回まででデバッグ機能は作り切ったので、いよいよより複雑な機能の実装に着手する。まず手を付けなければならないのは仮想アドレスのサポート。これが無いとテストパタンの大半をPASSできないし、Linuxを立ち上げることもできない。この機能について実装を考えて行く。
QEMUにおいては、MMUなどの仮想アドレスから物理アドレスへの変換機能については以下のブログ記事で確認した。これを実際に自作QEMUで実装して行くことになる。
まずは簡単に、TLBを実装せずにアドレス変換の結果をキャッシュしないバージョンを作ってみる。簡単なバージョンで正しく動作することを確認して、キャッシュすることで高速化を目指すという訳だ。
仮想アドレスから物理アドレスへの変換をRustで実装する
実はこれはすでに用意してある。大昔にRustでRISC-Vシミュレータを作った時に実装した仮想アドレスから物理アドレスへの変換ルーチンがそのまま使える。
fn walk_page_table(&mut self, virtual_addr: u64, acc_type: MemAccType, init_level: u32, ppn_idx: Vec<u8>, pte_len: Vec<u8>, pte_idx: Vec<u8>, vpn_len: Vec<u8>, vpn_idx: Vec<u8>, pagesize: u32, ptesize: u32) -> Result<u64, MemResult> { let is_write_access = match acc_type { MemAccType::Write => true, _ => false, }; ... for level in range(0, init_level).rev() { let va_vpn_i: u64 = (virtual_addr >> vpn_idx[level as usize]) & ((1 << vpn_len[level as usize]) - 1); pte_addr += (va_vpn_i * (ptesize as u64)) as u64; pte_val = self.read_mem_4byte(pte_addr) as i64; println!( "<Info: VAddr = 0x{:016x} PTEAddr = 0x{:016x} : PPTE = 0x{:08x}>", virtual_addr, pte_addr, pte_val ); // 3. If pte:v = 0, or if pte:r = 0 and pte:w = 1, stop and raise a page-fault exception.
これと、あとは例外が発生した場合に処理をするためのGenerateException()
を持ってきておく。
pub fn generate_exception(&mut self, code: ExceptCode, tval: i64) { println!( "<Info: Generate Exception Code={}, TVAL={:016x} PC={:016x}>", code as u32, tval, self.m_pc[0] ); let epc: u64; epc = self.m_pc[0]; ...
この2つを使って、まずは命令フェッチのアドレス変換を実装していこう。といっても、フェッチ部分にアドレス変換の関数を挟み込むだけである。
#[allow(while_true)] while true { #[allow(unused_assignments)] let mut guest_phy_pc = 0; match self.convert_physical_address(self.m_pc[0], MemAccType::Read) { Ok(addr) => guest_phy_pc = addr, Err(error) => { print!("Fetch Error: {:?}\n", error); continue; } }; print!(" converted physical address = {:08x}\n", guest_phy_pc); let guest_inst = self.read_mem_4byte(guest_phy_pc); ...
これで基本的には実装完了となる。ただしテストパタンを動かすためにはいくつかさらに改造を加えていかなければならない。
まず1つ目は、メモリアクセスのオフセット値の削除だ。現在のテストバイナリは命令の開始位置が0x8000_0000になっており、それよりも下のアドレスには基本的にアクセスしないようになっている。この場合アドレスを32ビット空間ですべて保持してしまうのはもったいないので、メモリテーブルを作るときは0x80000000を引き算して、オフセット値を削減してメモリサイズを抑えている。
これをしてしまった場合、命令としては0x8000_xxxxとして実行しているつもりでも、実際のアクセスアドレスは0x0000_xxxxとなってしまうためいろいろと問題が生じる。命令フェッチの部分はRustで記述しているため単純に0x8000_0000を引き算するコードを書いておけばよいが、ロードストア命令がアクセスする場所についてはそうもいかない。
どうしようかといろいろ考えたのだが、結果的にTCGからx86に変換する際に無理やり0x8000_0000を引き算する命令を挿入することにした。これだとこのTCG変換関数はRISC-V専用になってしまうが、どうせ仮想アドレス→物理アドレスをきちんと実装するとこの辺も作り直しになってしまうのでとりあえずは良いだろう。
// Execute Load // GPR value + Memory Head Address Self::tcg_modrm_64bit_out( X86Opcode::ADD_GV_EV, X86ModRM::MOD_11_DISP_RCX, X86TargetRM::RAX, mc, ); // Address Calculation : Sub Bias 0x8000_0000 gen_size += Self::tcg_64bit_out(X86Opcode::ADD_EAX_IV, mc); gen_size += Self::tcg_out(0x8000_0000 as u64, 4, mc);
最後の2行が、実計算アドレスに対して0x8000_0000
を減算するコードとなっている。実際、確認すると以下のようなx86命令が生成されている。
Guest PC Address = 80002914 <Convert_Virtual_Address. virtual_addr=0000000080002914 : vm_mode = 0, priv_mode = 3> converted physical address = 80002914 6ed8b823 : sd a3, 1776(a7) tb_address = 0x7f02e69a0000 00007F02E69A0000 48B8000099E6027F0000 movabs $0x7F02_E699_0000,%rax 00007F02E69A000A 488BC8 mov %rax,%rcx 00007F02E69A000D 488B8D90000000 mov 0x90(%rbp),%rcx 00007F02E69A0014 4803C1 add %rcx,%rax 00007F02E69A0017 480500000080 add $0xFFFF_FFFF_8000_0000,%rax 00007F02E69A001D 488B8D70000000 mov 0x70(%rbp),%rcx 00007F02E69A0024 488988F0060000 mov %rcx,0x6F0(%rax) 00007F02E69A0000 E9DFFF0000 jmp 0x0000_7F02_E69A_FFE4
途中にadd $0xFFFF_FFFF_8000_0000,%rax
という命令が挿入された。これが当該命令に相当する。
この辺まで実装できたら、一応rv64ui-v-simple
を動かして動作を確認する。
$ cargo run -- --debug --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-v-simple
Guest PC Address = 800000c0 <Convert_Virtual_Address. virtual_addr=00000000800000c0 : vm_mode = 0, priv_mode = 3> converted physical address = 800000c0 10200073 : sret tb_address = 0x7f1fe7920000 00007F1FE7920000 48BFA89FC4C0FF7F0000 movabs $0x7FFF_C0C4_9FA8,%rdi 00007F1FE792000A FF9558040000 callq *0x458(%rbp) 00007F1FE7920000 E9FAFF0000 jmp 0x0000_7F1F_E792_FFFF 00007F1FE7920000 E9F5FF0000 jmp 0x0000_7F1F_E792_FFFA helper_sret(emu) is called! PC = 000000002ac8 x00 = 0000000000000000 x01 = 0000000000000000 x02 = 0000000000000000 x03 = 0000000000000000 x04 = 0000000000000000 x05 = 0000000000000000 x06 = 0000000000000000 x07 = 0000000000000000 x08 = 0000000000000000 x09 = 0000000000000000 x10 = 0000000000000000 x11 = 0000000000000000 x12 = 0000000000000000 x13 = 0000000000000000 x14 = 0000000000000000 x15 = 0000000000000000 x16 = 0000000000000000 x17 = 0000000000000000 x18 = 0000000000000000 x19 = 0000000000000000 x20 = 0000000000000000 x21 = 0000000000000000 x22 = 0000000000000000 x23 = 0000000000000000 x24 = 0000000000000000 x25 = 0000000000000000 x26 = 0000000000000000 x27 = 0000000000000000 x28 = 0000000000000000 x29 = 0000000000000000 x30 = 0000000000000000 x31 = 0000000000000000
SRET
が実行され、スーパーバイザモードに移行する。
Guest PC Address = 00002ac8 <Convert_Virtual_Address. virtual_addr=0000000000002ac8 : vm_mode = 8, priv_mode = 0> <Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080003000 : PPTE = 0x20001001> <Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080004000 : PPTE = 0x20001801> <Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080006010 : PPTE = 0x00000000> <Page Table Error : 0x0000000080006010 = 0x00000000 is not valid Page Table. Generate Exception> <Info: Generate Exception Code=13, TVAL=0000000000002ac8 PC=0000000000002ac8> <Info: Exception. ChangeMode from 0 to 1> <Info: Set Program Counter = 0xffffffffffe000c4> Fetch Error: TlbError <Convert_Virtual_Address. virtual_addr=ffffffffffe000c4 : vm_mode = 8, priv_mode = 1> <Info: VAddr = 0xffffffffffe000c4 PTEAddr = 0x0000000080003ff8 : PPTE = 0x20001401> <Info: VAddr = 0xffffffffffe000c4 PTEAddr = 0x0000000080005ff8 : PPTE = 0x200000cf>
アドレス変換が始まった。仮想アドレス0x00002ac8
に対してアドレス変換を行ったのだが、3番目のアドレス変換でテーブルの中身が何も入っておらず例外となった。例外により新たなプログラムカウンタが設定されたが、こちらもテーブルが用意されておらず例外となった。最終的に0x8000_00c4にジャンプしてトラップハンドラを起動している。上手く行っているようだ。
次に考えなければならないのはロードストア命令によるメモリアクセスのアドレス変換だ。少しずつやっていこう。