FPGA開発日記

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

Binary Translation型エミュレータを作る(TCGの途中で例外を取得する方法の検討)

TCGは分岐命令に到達するまで一連のブロックとして変換されるため、1ブロックで複数の命令が実行されます。もしこのブロック実行中に例外が発生した場合どのようにすればよいでしょうか?例えば以下のような命令列をTCGに変換することを考えます。

 add  x10, x10, 1
    ld   x10, 0(x10)
    sret

LD命令で例外が発生すると、MEPCに現在のプログラムカウンタを保存して例外ルーチンにジャンプする必要があります。しかしこのブロックを変換した時点でTCGにプログラムカウンタの情報は保存されていません。例外が発生した場合に、MEPCにどのようにプログラムカウンタを通知すればよいのか問題になります。

これを解決するために、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 op = TCGOp::new_helper_call_arg4(CALL_HELPER_IDX::CALL_LOAD64_IDX as usize, *rd, *rs1, *imm, *tcg_inst_addr);
        vec![op]

        // Self::translate_rri(TCGOpcode::LOAD_64BIT, inst)
    }

ヘルパー関数の呼び出しですが、引数を増やして*tcg_inst_addrを第4引数に追加しました。tcg_inst_addrは現在のプログラムカウンタinst.addrを保存しており、例外発生時にこの値(TCG変換時に明らかになっているゲストマシンのプログラムカウンタ)が渡されます。

これによりロード命令であれば、ページテーブルウォーク時にページテーブルエラーが発生した際、エラーの発生したアドレスを引数としてもらっているためMEPCに正しい値を設定可能になります。

    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);

        println!("load64 : converted address: {:016x}", addr);

        #[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; 
                println!("load64 : converted address: {:016x} --> {:016x}", addr, guest_phy_addr);
                emu.m_regs[rd as usize] = emu.read_mem_8byte(guest_phy_addr) as u64;
            }
            Err(error) => {
                print!("Read Error: {:?}\n", error);
                emu.generate_exception(guest_pc, ExceptCode::LoadPageFault, addr as i64);
            }
        };

        return 0;
    }
    pub fn generate_exception(&mut self, guest_pc: u64, code: ExceptCode, tval: i64) {
        println!(
            "<Info: Generate Exception Code={}, TVAL={:016x} PC={:016x}>",
            code as u32, tval, guest_pc
        );

        let epc = guest_pc;
...
        if (medeleg & (1 << (code as u32))) != 0 {
            // Delegation
            self.m_csr.csrrw(CsrAddr::Sepc, epc as i64);
            self.m_csr.csrrw(CsrAddr::Scause, code as i64);
            self.m_csr.csrrw(CsrAddr::Stval, tval as i64);

            tvec = self.m_csr.csrrs(CsrAddr::Stvec, 0 as i64);
            next_priv = PrivMode::Supervisor;
        } else {
            self.m_csr.csrrw(CsrAddr::Mepc, epc as i64);
            self.m_csr.csrrw(CsrAddr::Mcause, code as i64);
            self.m_csr.csrrw(CsrAddr::Mtval, tval as i64);

            tvec = self.m_csr.csrrs(CsrAddr::Mtvec, 0 as i64);
            print!("tvec = {:016x}\n", tvec);
        }

この実装でテストプログラムを走らせてみた。

$ cargo run -- --step --dump-gpr --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-v-lw
========= BLOCK START =========
11999: Guest PC Address = ffffffffffe02258
<Convert_Virtual_Address. virtual_addr=ffffffffffe02258 : vm_mode = 8, priv_mode = 1>
<Info: VAddr = 0xffffffffffe02258 PTEAddr = 0x0000000080004ff8 : PPTE = 0x20001801>
<Info: VAddr = 0xffffffffffe02258 PTEAddr = 0x0000000080006ff8 : PPTE = 0x200000cf>
  converted physical address = 80002258
  0000006f : j       pc + 0x0
tb_address  = 0x7fe8ba390000
00007FE8BA390000 48B85822E0FFFFFFFFFF movabs    $0xFFFF_FFFF_FFE0_2258,%rax
00007FE8BA39000A 48898508020000       mov       %rax,0x208(%rbp)
00007FE8BA390000 E9F9FF0800           jmp       0x0000_7FE8_BA41_FFFE
00007FE8BA390000 E9F4FF0800           jmp       0x0000_7FE8_BA41_FFF9
x00 = 0000000000000000  x01 = ffffffffffe028ac  x02 = ffffffffffe09660  x03 = 0000000000000003
x04 = 0000000000000000  x05 = 0000000000000008  x06 = 0000000000000000  x07 = ffffffffff00ff00
x08 = 000000000003f000  x09 = 00000000000003e0  x10 = 0000000000000007  x11 = ffffffffffe04000
x12 = ffffffffffe04000  x13 = 0000000000004000  x14 = 0000000000000000  x15 = ffffffffffe01250
x16 = 0000000000001000  x17 = 0000000000000000  x18 = 000000000003f000  x19 = 0000000000000007
x20 = ffffffffffe087e0  x21 = ffffffffffe00000  x22 = 0000000000040000  x23 = ffffffffffe04000
x24 = 0000000020010c5f  x25 = 0000000000000000  x26 = ffffffffffe083f0  x27 = ffffffffffe03000
x28 = 0000000000000000  x29 = 0000000000000000  x30 = 0000000000000000  x31 = 0000000000000000

Result: MEM[0x1000] = 00000007

何とか完走するようになったが、まだテストパタンがFailする。例外の処理を正しく実装しないとまだ難しいなあ。