FPGA開発日記

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

Binary Translation型エミュレータを作る(メモリアクセス命令のMMU実装)

Binary Translation方式の命令セットエミュレータのRust実装をしている。仮想アドレス変換機能を使ってRISC-Vのロードストア命令の実装を行っている。実装後にテストパタンを流しながらバグを取っているが、いくつか問題になった部分がある。

通常命令実行中に例外が発生した場合の処理

ロードストア命令の仮想アドレス変換を行った場合、ページテーブルが用意されていない場合には例外が発生する可能性がある。例外が発生した場合には即座に例外ハンドラに分岐しなければならないのだが、Binary Translationしている場合には少し問題が生じる。

まず、ロードストア命令が仮想アドレス変換関数を呼び出し、それに失敗した場合、その失敗したことを通知する機能が実装されていない。つまり、本来例外にジャンプしなければならないのが、命令を継続して実行してしまうのである。これは良くないので、いくつかのステップをかけて例外ハンドラへのジャンプを実装する。

  • ヘルパー関数が、アドレス変換に失敗したことを関数呼び出し元に通知する。
  • ヘルパー関数の戻り値に応じて、Binary Translationされた命令を継続して実行するか例外にジャンプするかを判定する。

この2点について実装を進めていこう。

ヘルパー関数にアドレス変換失敗を伝える戻り値を追加

これまでヘルパー関数は戻り値は定義していても常にゼロを返しており、戻り値自体にあまり意味を持っていなかった。そこで戻り値を正確に定義し、例外が発生した場合にはその要因をヘルパー関数呼び出し元に通知する。

下記のヘルパー関数では、アドレス変換に失敗した場合は戻り値として例外要因を返している。例外が発生しなければNoExcept = 0を返している。

    pub fn helper_func_load64(emu: &mut EmuEnv,rd: u64,rs1: u64,imm: u64,guest_pc: u64) -> 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(guest_pc, addr, MemAccType::Read) {
            Ok(addr) => { 
                guest_phy_addr = addr; 
                emu.m_regs[rd as usize] = emu.read_mem_8byte(guest_phy_addr) as u64;
                return MemResult::NoExcept as usize;
            }
            Err(error) => {
                emu.generate_exception(guest_pc, ExceptCode::LoadPageFault, addr as i64);
                return error as usize;
            }
        };
   }

ヘルパー関数の戻り値を受け取って例外判定を行うTCGを追加

次に、ヘルパー関数の戻り値を受け取りそれをもとに分岐を実行するTCGを組み上げる。分岐命令のTCG組み上げはすでにルーチンを作っているので特に悩むところは無いのだが、問題はヘルパー関数の戻り値を比較対象にするというTCGをサポートしていない。これまでは汎用レジスタ間の比較だったが、今回は関数の戻り値を比較対象としなければならない。

これだったら早めにTCGローカル変数のサポートを行っておけばよかったのだが、面倒なので手っ取り早くRAXの結果とレジスタの比較を行うTCGを新たに作成し、ゼロレジスタと比較することでヘルパー関数の結果と0を比較できるようにした。

    fn tcg_gen_eq_eax_64bit(emu: &EmuEnv, pc_address: u64, tcg: &TCGOp, mc: &mut Vec<u8>) -> usize {
        let arg0 = tcg.arg0.unwrap();
        let arg1 = tcg.arg1.unwrap();

        let label = match &tcg.label {
            Some(l) => l,
            None => panic!("Label is not defined."),
        };

        let mut gen_size: usize = pc_address as usize;

        // cmp    reg_offset(%rbp),%eax
        gen_size += Self::tcg_modrm_64bit_out(
            X86Opcode::CMP_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);

        gen_size = Self::tcg_gen_jcc(gen_size, X86Opcode::JE_rel16_32, mc, label);
        // je     label

        return gen_size;
    }

このTCGはRAXの値とレジスタの値を比較し、その結果に基づいてJE命令でジャンプする。ジャンプ先はラベルで指定しているのだが、このラベルは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 tcg_inst_addr = Box::new(TCGv::new_imm(inst.addr));

        let label = Rc::new(RefCell::new(TCGLabel::new()));

        let tcg_call_op = TCGOp::new_helper_call_arg4(CALL_HELPER_IDX::CALL_LOAD64_IDX as usize, *rd, *rs1, *imm, *tcg_inst_addr);

        let zero = Box::new(TCGv::new_reg(0 as u64));        
        let dummy_addr = Box::new(TCGv::new_imm(0));

        let result_cmp_op = TCGOp::new_4op(TCGOpcode::EQ_EAX_64BIT, *rs1, *zero, *dummy_addr, Rc::clone(&label));
        let exit_tb = TCGOp::new_0op(TCGOpcode::EXIT_TB);
        let tcg_set_label = TCGOp::new_label(Rc::clone(&label));

        vec![tcg_call_op, result_cmp_op, exit_tb, tcg_set_label]
}

結果として生成されるx86命令は以下のようになる。

00007FFCC93E0000 48BFD089EAC8FF7F0000 movabs    $0x7FFF_C8EA_89D0,%rdi
00007FFCC93E000A 48BE0500000000000000 movabs    $5,%rsi
00007FFCC93E0014 48BA0A00000000000000 movabs    $0xA,%rdx
00007FFCC93E001E 48B90801000000000000 movabs    $0x108,%rcx
00007FFCC93E0028 49B83C00008000000000 movabs    $0x8000_003C,%r8
00007FFCC93E0032 FF9560040000         callq     *0x460(%rbp)
00007FFCC93E0000 483B8508000000       cmp       8(%rbp),%rax
00007FFCC93E0007 0F840A000000         je        0x0000_7FFC_C93E_0017
00007FFCC93E0000 E9C5FF0800           jmp       0x0000_7FFC_C946_FFCA

callqによりヘルパー関数を呼び出した後、戻り値%raxを比較してゼロであれば実行継続、そうでなければ命令実行を終了して実行制御側に戻ってくる。

この機能を実装した結果、とりあえずはすべての仮想アドレス系命令をPASSさせることができるようになった。順調に実装が進んでいる。