FPGA開発日記

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

Binary Translation型エミュレータを作る(浮動小数点演算の検討)

Binary Translation方式のエミュレータ開発、RISC-Vの基本命令については一通り実装が完了した。次はどちらの方向に進むかということだが、いくつか考えられるのは、

  • 浮動小数点命令のサポート
  • 仮想アドレスのサポート
  • Compressed 命令のサポート
  • AArch64ホストマシンのサポート

とりあえずまずは浮動小数点命令を試そう。QEMUを調査して、浮動小数点命令の実行にはsoftfloatライブラリの力を利用していることが分かった。これを使えば簡単に浮動小数点命令のサポートができそうだ(実際自作RISC-Vシミュレータの開発にはsoftfloatを使っていた)。

しかしまずは浮動小数点のロードストア命令から実装すべきだろう。ロードストア命令は単純なメモリアクセスなので、softfloatを使用する必要はない。まずはRISC-Vの浮動小数点ロードストア命令から実装して行く。

最初に、浮動小数レジスタを用意した。以下のm_fregsがそれに当たる。Double Sizeまでサポートするためにu64型のレジスタを32本用意した。

pub struct EmuEnv {
    pub head: [u64; 1], // pointer of this struct. Do not move.

    m_regs: [u64; 32],      // Integer Registers
    m_fregs: [u64; 32],      // Floating Point Registers
    m_pc: [u64; 1],
...

サポート用に、ホストの命令からm_fregsまでの距離を計算するための相対距離計算ルーチンを用意しておく。これは浮動小数レジスタにアクセスする際、ベースレジスタからのオフセットとして使用する。

    pub fn calc_fregs_relat_address(&self, gpr_addr: u64) -> isize {
        let guestcode_ptr = self.m_fregs.as_ptr() as *const u8;
        let self_ptr = self.head.as_ptr() as *const u8;
        let mut diff = unsafe { guestcode_ptr.offset_from(self_ptr) };
        diff += gpr_addr as isize * mem::size_of::<u64>() as isize;
        diff
    }

では、RISC-Vの命令を定義する。今回定義するのはFLD, FLW, FSD, FSWの4命令だ。

    pub fn translate(id: RiscvInstId, inst: &InstrInfo) -> Vec<TCGOp> {
        return match id {
            RiscvInstId::ADDI => TranslateRiscv::translate_addi(inst),
            RiscvInstId::ADD => TranslateRiscv::translate_add(inst),
...
            RiscvInstId::FLD => TranslateRiscv::translate_fld(inst),
            RiscvInstId::FLW => TranslateRiscv::translate_flw(inst),
            RiscvInstId::FSD => TranslateRiscv::translate_fsd(inst),
            RiscvInstId::FSW => TranslateRiscv::translate_fsw(inst),

それぞれのtranslate_xxx関数では、3オペランドの演算実行用TCGを定義する。これは本当はもっと細かなTCGに分解した方が良い気がするが、現在はざっくりとしたTCGになってしまっている。

    pub fn translate_fld(inst: &InstrInfo) -> Vec<TCGOp> {
        Self::translate_float_rri(TCGOpcode::LOAD_FLOAT_64BIT, inst)
    }
    pub fn translate_flw(inst: &InstrInfo) -> Vec<TCGOp> {
        Self::translate_float_rri(TCGOpcode::LOAD_FLOAT_32BIT, inst)
    }
    pub fn translate_fsd(inst: &InstrInfo) -> Vec<TCGOp> {
        Self::translate_float_rri(TCGOpcode::STORE_FLOAT_64BIT, inst)
    }
    pub fn translate_fsw(inst: &InstrInfo) -> Vec<TCGOp> {
        Self::translate_float_rri(TCGOpcode::STORE_FLOAT_32BIT, inst)
    }

なぜ浮動小数点命令用に特殊なtranslate_float_rri()という関数を用意したかというと、通常のtranslate_rri()はゼロレジスタの書き込みを禁止してしまうので。translate_float_rri()はゼロレジスタへの書き込みを許可している。

    fn translate_float_rri(op: TCGOpcode, 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 = TCGOp::new_3op(op, *rd, *rs1, *imm);
        return vec![tcg_inst]
    }

次はこのTCGx86命令に変換する処理だ。これは実は整数ロードストア命令とほとんど変わらない。書き込み先・読み込み先が浮動小数レジスタに置き換わるだけである。

    /* Memory Access : Load */
    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;

...
        // Store Loaded value into destination register.
        if target_reg == RegisterType::IntRegister {
            gen_size += Self::tcg_gen_store_gpr_64bit(emu, X86TargetRM::RAX, arg0.value, mc);
        } else if target_reg == RegisterType::FloatRegister && mem_size == MemOpType::LOAD_64BIT {
            gen_size += Self::tcg_gen_store_fregs_64bit(emu, X86TargetRM::RAX, arg0.value, mc);
        } else if target_reg == RegisterType::FloatRegister && mem_size == MemOpType::LOAD_32BIT {
            gen_size += Self::tcg_gen_store_fregs_32bit(emu, X86TargetRM::RAX, arg0.value, mc);
        } else {
            panic!("Unknown condition for Register Write")
        }
        return gen_size;

もし浮動小数レジスタへをターゲットとしているならば、tcg_gen_store_fregs_64bit()もしくはtcg_gen_store_fregs_32bit()を呼び出す。これは先ほど定義した浮動小数レジスタへのベースポインタからの相対距離を利用して値を書き込む関数だ。

    fn tcg_gen_store_fregs_64bit(
        emu: &EmuEnv,
        source: X86TargetRM,
        dest: u64,
        mc: &mut Vec<u8>,
    ) -> usize {
        let mut gen_size = 0;
        gen_size +=
            Self::tcg_modrm_64bit_out(X86Opcode::MOV_EV_GV, X86ModRM::MOD_10_DISP_RBP, source, mc);
        gen_size += Self::tcg_out(emu.calc_fregs_relat_address(dest) as u64, 4, mc);
        return gen_size;
    }

ストア命令も同様だ。読み込み元のレジスタ浮動小数レジスタに置き換えるだけである。

    /* Memory Access : Store */
    fn tcg_gen_store(
        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;
...
        // Load value from rs1(addr)
        if target_reg == RegisterType::IntRegister {
            gen_size += Self::tcg_gen_load_gpr_64bit(emu, X86TargetRM::RAX, arg0.value, mc);
        } else if target_reg == RegisterType::FloatRegister && mem_size == MemOpType::LOAD_64BIT {
            gen_size += Self::tcg_gen_load_fregs_64bit(emu, X86TargetRM::RAX, arg0.value, mc);
        } else if target_reg == RegisterType::FloatRegister && mem_size == MemOpType::LOAD_32BIT {
            gen_size += Self::tcg_gen_load_fregs_32bit(emu, X86TargetRM::RAX, arg0.value, mc);
        } else {
            panic!("Unknown Register read condition.")
        }