Binary Translation型エミュレータを作る(Helper Function Call機能の実装) - FPGA開発日記

FPGA開発日記

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

Binary Translation型エミュレータを作る(Helper Function Call機能の実装)

Binary Translation型エミュレータにおいてシフト命令を実装した。Binary Translation型エミュレータでは、ゲストマシンの命令(今回はRISC-V)からホストマシンの命令(今回はx86)に直接変換することで高速実行を可能にするシミュレータだ。

次に実装するのは、Helper Function Call機能の実装だ。Helper Function Callは機能は、TCGおよびホストマシンのアセンブリ命令では実装しきれない高度な機能を、C言語で記述されホストマシンの機械語コンパイルされた機能にオフロードする機能のことを言う。QEMUでは、CSRレジスタの読み書きに対してこの機能が使用されている。

自分の作っている自作エミュレータでも、この機能をすでにCSR命令の実装で使用している。今回はこの機能をより一般化していこうと思う。

まず、TCGのオペコードとしてHelper Function Callを意味するコードHELPER_CALLを用意する。下記の4つの新たなTCGOpcodeは、それぞれ引数を持たないHelper関数から、引数を3つまで持つ子のできるHelper関数を意味する。

#[derive(Debug, Copy, Clone, PartialEq)]
#[allow(non_camel_case_types)]
pub enum TCGOpcode {
    HELPER_CALL_ARG0,
    HELPER_CALL_ARG1,
    HELPER_CALL_ARG2,
    HELPER_CALL_ARG3,
    ...

このHelper関数を使用する例として、今回はRISC-VのMRET命令の実装を行う。MRET命令はCSRレジスタMEPCから値を読み出し、それをPCに設定する。つまり例外・割込みルーチンからの復帰を意味する命令だ。この命令で必要なのはCSRレジスタMEPCの読み出しだが、この機能は重たいのでHelper関数に投げてしまおうと思う。つまり、MRETをデコードした場合には以下のようなTCGを作成する。

    pub fn translate_mret(_inst: &InstrInfo) -> Vec<TCGOp> {
        let mret_op = TCGOp::new_helper_call_arg0(CALL_HELPER_IDX::CALL_MRET_IDX as usize);
        vec![mret_op]
    }

HELPER_CALL_ARG0をTCGOpcodeとして持つTCGを生成する。Helper関数のインデックスはCALL_MRET_IDX(=6)を設定する。これはHelper関数リストの6番目にこの関数が用意されているという意味だ。この6という数値にあまり意味はなく、とりあえず私が実装しているHelper関数リストの6番目に入っているのでそのような値を設定しているだけである。

impl EmuEnv {
    pub fn new() -> EmuEnv {
        EmuEnv {
            head: [0xdeadbeef; 1],
            m_regs: [0; 32],
            m_pc: [0x0; 1],
            m_csr: RiscvCsr::new(),

            helper_func: [
                Self::helper_func_csrrw,
                Self::helper_func_csrrs,
                Self::helper_func_csrrc,
                Self::helper_func_csrrwi,
                Self::helper_func_csrrsi,
                Self::helper_func_csrrci,
                Self::helper_func_mret,            // CALL_HELPER_IDX::CALL_MRET_IDXがここに相当する。
                Self::helper_func_ecall,
                Self::dummy_helper,
                Self::dummy_helper,
                Self::dummy_helper,
                Self::dummy_helper,
                Self::dummy_helper,
                Self::dummy_helper,
...

CALL_HELPER_ARG0からCALL_HELPER_ARG3までのTCGをホスト機械語に変換するためのルーチンを以下のように作成した。

impl TCG for TCGX86 {
    fn tcg_gen(emu: &EmuEnv, pc_address: u64, tcg: &TCGOp, mc: &mut Vec<u8>) -> usize {
        match tcg.op {
            Some(op) => {
                return match op {
...
                    TCGOpcode::HELPER_CALL_ARG0 => {
                        TCGX86::tcg_gen_helper_call(emu, 0, pc_address, tcg, mc)
                    }
                    TCGOpcode::HELPER_CALL_ARG1 => {
                        TCGX86::tcg_gen_helper_call(emu, 1, pc_address, tcg, mc)
                    }
                    TCGOpcode::HELPER_CALL_ARG2 => {
                        TCGX86::tcg_gen_helper_call(emu, 2, pc_address, tcg, mc)
                    }
                    TCGOpcode::HELPER_CALL_ARG3 => {
                        TCGX86::tcg_gen_helper_call(emu, 3, pc_address, tcg, mc)
                    }

TCGX86::tcg_gen_helper_call()は以下のように実装している。X86の引数用のレジスタに値を設定して、CALL命令を呼ぶだけだ。

    fn tcg_gen_helper_call(
        emu: &EmuEnv,
        arg_size: usize,
        pc_address: u64,
        tcg: &TCGOp,
        mc: &mut Vec<u8>,
    ) -> usize {
        let mut gen_size: usize = pc_address as usize;

        let self_ptr = emu.head.as_ptr() as *const u8;
        let self_diff = unsafe { self_ptr.offset(0) };
        gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RDI, self_diff as u64, mc);

        if arg_size >= 1 {
            let arg0 = tcg.arg0.unwrap();
            gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RSI, arg0.value as u64, mc);
        }
        if arg_size >= 2 {
            let arg1 = tcg.arg1.unwrap();
            gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RDX, arg1.value as u64, mc);
        }
        if arg_size >= 3 {
            let arg2 = tcg.arg2.unwrap();
            gen_size += Self::tcg_gen_imm_u64(X86TargetRM::RCX, arg2.value as u64, mc);
        }

        gen_size += Self::tcg_modrm_32bit_out(
            X86Opcode::CALL,
            X86ModRM::MOD_10_DISP_RBP,
            X86TargetRM::RDX,
            mc,
        );
        let csr_helper_idx = tcg.helper_idx;
        let helper_func_addr = emu.calc_helper_func_relat_address(csr_helper_idx as usize);
        gen_size += Self::tcg_out(helper_func_addr as u64, 4, mc);

        //  Jump Epilogue
        gen_size = Self::tcg_exit_tb(emu, gen_size, mc);
        return gen_size;
    }

引数用のレジスタ設定はSelf::tcg_gen_imm_u64()を使用して知っていしている。引数の並べ方は、

  • RDI / RSI / RDX / RCX

の順番となっている。その後、csr_helper_idx、つまりHelper関数リスト中の使用する関数インデックスへのアドレスを計算し、CALL命令でジャンプする。この時の流れをQEMU機械語生成機能を使って確認すると以下のようになった。

----------------
IN:
0x4001cb20a8:  48 bf 20 b5 89 01 40 00  movabsq  $0x400189b520, %rdi
0x4001cb20b0:  00 00
0x4001cb20b2:  ff 95 40 02 00 00        callq    *0x240(%rbp)   // helper_mret関数へのジャンプ

helper_mret()の実装は以下のようになっている。

    fn helper_func_mret(emu: &mut EmuEnv, dest: u32, imm: u32, csr_addr: u32) -> usize {
        println!(
            "helper_mret(emu, {:}, {:}, 0x{:03x}) is called!",
            dest, imm, csr_addr
        );
        emu.m_pc[0] = emu.m_csr.csrrc(CsrAddr::Mepc, 0 as i64) as u64;
        print!("PC is set to 0x{:08x}\n", emu.m_pc[0]);
        return 0;
    }

CSRレジスタからMEPCの値を読みだしてPCに設定するだけだ。ここまででMRETの実装は完了となる。