FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Binary Translation型エミュレータを作る(MMU機能の実装検討)

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にジャンプしてトラップハンドラを起動している。上手く行っているようだ。

次に考えなければならないのはロードストア命令によるメモリアクセスのアドレス変換だ。少しずつやっていこう。

f:id:msyksphinz:20201003011457p:plain