FPGA開発日記

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

Binary Translation型エミュレータを作る(ロードストア命令のMMU実装)

Binary Translation方式の命令セットエミュレータのRust実装をしている。前回まででデバッグ機能は作り切ったので、いよいよより複雑な機能の実装に着手する。まず手を付けなければならないのは仮想アドレスのサポート。前回までにとりあえずRustによる仮想アドレス変換の実装を行った。仮想アドレス変換はロードストア命令に対しても実装しなければならない。しかし現在のTCGの構成のままロードストア命令にアドレス変換を実装するといきなり実装が複雑になりデバッグが大変になるので、一度ロードストア命令をすべてヘルパー関数による実装に変更し、メモリアクセスをすべてヘルパー関数に置き換えることにした。これによりメモリアクセスのエミュレーション速度は著しく低下するが、確実にデバッグできる環境になるのでこれでまずはテストパタンを通していきたい。

f:id:msyksphinz:20201005124734p:plain

これまで、メモリアクセス命令は以下のようなTCGを生成していた。

    pub fn translate_ld(inst: &InstrInfo) -> Vec<TCGOp> {
        Self::translate_rri(TCGOpcode::LOAD_64BIT, inst)
    }
    pub fn translate_lw(inst: &InstrInfo) -> Vec<TCGOp> {
        Self::translate_rri(TCGOpcode::LOAD_32BIT, inst)
    }

単純なTCGであるが、TCGOpcode::LOAD_64BITなどのオペコードはx86への変換部分でネイティブなコードに落とし込んでいた。

    fn tcg_gen_load(
        emu: &EmuEnv,
        pc_address: u64,
        tcg: &TCGOp,
        mc: &mut Vec<u8>,
        mem_size: MemOpType,
        target_reg: RegisterType,
    ) -> usize {
        let mut gen_size: usize = pc_address as usize;

        let arg0 = tcg.arg0.unwrap();
...
        // Load Guest Memory Head into EAX
        let guestcode_addr = emu.calc_guest_data_mem_address();
        gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RAX, guestcode_addr as u64, mc);

        // Move Guest Memory from EAX to ECX
        gen_size += Self::tcg_modrm_64bit_out(
            X86Opcode::MOV_GV_EV,
            X86ModRM::MOD_11_DISP_RAX,
            X86TargetRM::RCX,
            mc,
        );

        // Load value from rs1
        gen_size += Self::tcg_modrm_64bit_out(
            X86Opcode::MOV_GV_EV,
            X86ModRM::MOD_10_DISP_RBP,
            X86TargetRM::RAX,
            mc,
        );
        gen_size += Self::tcg_out(emu.calc_gpr_relat_address(arg1.value) as u64, 4, mc);
...

これを、以下のようにTCGへの変換部分をヘルパー関数呼び出しに置き換えてしまった。

    pub fn translate_ld(inst: &InstrInfo) -> Vec<TCGOp> {
        let rs1_addr: usize = get_rs1_addr!(inst.inst) as usize;
        let imm_const: u64 = ((inst.inst as i32) >> 20) as u64;
        let rd_addr: usize = get_rd_addr!(inst.inst) as usize;

        let rs1 = Box::new(TCGv::new_reg(rs1_addr as u64));
        let imm = Box::new(TCGv::new_imm(imm_const));
        let rd = Box::new(TCGv::new_reg(rd_addr as u64));

        let op = TCGOp::new_helper_call_arg3(CALL_HELPER_IDX::CALL_LOAD64_IDX as usize, *rd, *rs1, *imm);
        vec![op]
    }

ヘルパー関数は、この例ではCALL_LOAD64_IDXを呼び出している。これは以下のように実装している。

    pub fn helper_func_load16(
        emu: &mut EmuEnv,
        rd: u32,
        rs1: u32,
        imm: u32,
        _dummy: u32,
    ) -> usize {
        let rs1_data = emu.m_regs[rs1 as usize];
        let addr = rs1_data.wrapping_add(imm as i32 as u64);

        #[allow(unused_assignments)]
        let mut guest_phy_addr:u64 = 0;
        match emu.convert_physical_address(addr, MemAccType::Read) {
            Ok(addr) => guest_phy_addr = addr,
            Err(error) => {
                panic!("Read Error: {:?}\n", error);
            }
        };
        emu.m_regs[rd as usize] = emu.read_mem_4byte(guest_phy_addr) as u64;
        return 0;
    }

もろにRust実装を呼び出している。convert_physical_address()は前回実装したものだ。ネイティブコードを使わずRustの実装により物理アドレスを求め、メモリアクセスを行ってその結果をレジスタに代入する。

まずはこの実装でデグレードが無いことを確認する。

$ cargo run -- --debug --dump-gpr --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-p-add
Guest PC Address = 80000040
<Convert_Virtual_Address. virtual_addr=0000000080000040 : vm_mode = 0, priv_mode = 3>
  converted physical address = 80000040
  00001f17 : auipc   t5, 0x1
<Convert_Virtual_Address. virtual_addr=0000000080000044 : vm_mode = 0, priv_mode = 3>
  converted physical address = 80000044
  fc3f2023 : sw      gp, -64(t5)
<Convert_Virtual_Address. virtual_addr=0000000080000048 : vm_mode = 0, priv_mode = 3>
  converted physical address = 80000048
  ff9ff06f : j       pc + 0xffff8
tb_address  = 0x7ffa59cb0000

上手く行っているようだ。次は仮想アドレスの確認。

$ cargo run -- --debug --dump-gpr --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-v-add
x20 = 0000000000000000  x21 = 0000000000000000  x22 = 0000000000000000  x23 = 0000000000000000
x24 = 0000000000000000  x25 = 0000000000000000  x26 = 0000000000000000  x27 = 0000000000000000
x28 = 0000000000000000  x29 = 0000000000000000  x30 = 0000000000000000  x31 = 0000000000000000

Result: MEM[0x1000] = 00000000
...

あれ、テストが成功しなかった。問題を確認していく。