FPGA開発日記

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

Binary Translation型エミュレータを作る(システムレジスタアクセスを実現するためのホストコードジャンプ)

QEMURISC-VのCSR命令をエミュレートする仕組み

QEMUが複雑な命令をエミュレートする場合、TCG(Tiny Code Generator)によるx86命令への直接的な変換を行うだけでなく、ホストコードのサポート関数を直接呼び出してその実行を肩代わりする場合がある。例えば、RISC-VのCSRRW命令のシステムレジスタ操作命令はシステムレジスタの種類に応じて命令の動作が切り替わるためTCGでの実現が非常に難しい。実際にはQEMUCSR操作命令はhelper関数と呼ばれる関数の呼び出しに変換され、CSRRW命令であればhelper_csrrw()関数の呼び出しによってC言語により複雑な処理が記述されている。

  • qemu/target/riscv/insn_trans/trans_rvi.inc.c
static bool trans_csrrw(DisasContext *ctx, arg_csrrw *a)
{
    TCGv source1, csr_store, dest, rs1_pass;
    RISCV_OP_CSR_PRE;
    gen_helper_csrrw(dest, cpu_env, source1, csr_store);
    RISCV_OP_CSR_POST;
    return true;
}
  • qemu/target/riscv/op_helper.c
target_ulong helper_csrrw(CPURISCVState *env, target_ulong src,
        target_ulong csr)
{
    target_ulong val = 0;
    if (riscv_csrrw(env, csr, &val, src, -1) < 0) {
        riscv_raise_exception(env, RISCV_EXCP_ILLEGAL_INST, GETPC());
    }
    return val;
}
  • qemu/target/riscv/op_helper.c
/*
 * riscv_csrrw - read and/or update control and status register
 *
 * csrr   <->  riscv_csrrw(env, csrno, ret_value, 0, 0);
 * csrrw  <->  riscv_csrrw(env, csrno, ret_value, value, -1);
 * csrrs  <->  riscv_csrrw(env, csrno, ret_value, -1, value);
 * csrrc  <->  riscv_csrrw(env, csrno, ret_value, 0, value);
 */

int riscv_csrrw(CPURISCVState *env, int csrno, target_ulong *ret_value,
                target_ulong new_value, target_ulong write_mask)
...
f:id:msyksphinz:20200911232951p:plain

RISC-VのCSR命令エミュレートする仕組みをRustで実装する

同じようなエミュレート機能をRustで実装している自作エミュレータで実現したい。つまり、CSRRW/CSRRS/CSRRC/CSRRWI/CSRRSI/CSRRCI命令を変換する場合に、該当するヘルプ関数に呼び出す仕組みが必要となる。

これを実現するためのRustでの実現方法を考えた。helper_csrrw()関数は以下のように関数ポインタとして定義する。

fn(emu: &EmuEnv, dest: u32, source: u32, csr_addr: u32) -> usize

そして、EmuEnvにヘルプ関数を登録するためのメンバ変数を定義しておく。

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

    m_regs: [u64; 32],
    m_pc: [u64; 1],

    m_csr: [u64; 1024], // CSR implementation

    helper_csrrw: [fn(emu: &EmuEnv, dest: u32, source: u32, csr_addr: u32) -> usize; 1],
...

helper_csrrw()関数は以下のように定義した。とりあえずは簡単にprintln!するだけの関数となっている。

    fn dummy_helper_csrrw(emu: &EmuEnv, dest: u32, source: u32, csr_addr: u32) -> usize {
        println!(
            "helper_csrrw(emu, {:}, {:}, 0x{:03x}) is called!",
            dest, source, csr_addr
        );
        return 0;
    }

この関数を、エミュレートしているコード側から呼び出したい。これについて考えよう。

エミュレート中のx86コードからRustの関数を用意する

上記ののdummy_helper_csrrw()は4つの引数が用意されている。x86の場合、4つの引数を渡すためには以下のレジスタに順番に当該実引数値を格納していく。

https://th0x4c.github.io/blog/2013/04/10/gdb-calling-convention/

  • 第1引数:RDI
  • 第2引数:RSI
  • 第3引数:RDX
  • 第4引数:RCX
  • 第5引数:R8
  • 第6引数:R9

従ってこのように引数を設定していく。

  • 第1引数:RDIレジスタを設定。引数に設定されるEmuEnvのポインタはemu.head.as_ptr() as *const u8により取得している。
        // Argument 0 : Env
        let self_ptr = emu.head.as_ptr() as *const u8;
        let self_diff = unsafe { self_ptr.offset(0) };
        gen_size += Self::tcg_out(0x48, 1, mc);
        gen_size += Self::tcg_out(
            X86Opcode::MOV_EAX_IV as u64 + X86TargetRM::RDI as u64,
            1,
            mc,
        );
        gen_size += Self::tcg_out(self_diff as u64, 8, mc);
        // Argument 1 : rd u32
        gen_size += Self::tcg_out(
            X86Opcode::MOV_EAX_IV as u64 + X86TargetRM::RSI as u64,
            1,
            mc,
        );
        gen_size += Self::tcg_out(rd.value as u64, 4, mc);
        // Argument 2 : rs1 u32
        gen_size += Self::tcg_out(
            X86Opcode::MOV_EAX_IV as u64 + X86TargetRM::RDX as u64,
            1,
            mc,
        );
        gen_size += Self::tcg_out(rs1.value as u64, 4, mc);
  • 第4引数:RCXレジスタを設定。csr_addressのアドレスはcsr_addr.valueにより取得している。
        // Argument 3 : csr_addr u32
        gen_size += Self::tcg_out(
            X86Opcode::MOV_EAX_IV as u64 + X86TargetRM::RCX as u64,
            1,
            mc,
        );
        gen_size += Self::tcg_out(csr_addr.value as u64, 4, mc);

最終的にhelper関数のアドレスの取得と関数呼び出しは以下のように実装している。x86のCALL命令を使用しており、RBPをベースアドレスとして、オフセットとしてヘルパー関数までの距離を設定している。

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

calc_helper_func_relat_address()は以下のように実装している。EmuEnvからヘルパー関数への距離を以下のように計算する。

    pub fn calc_helper_func_relat_address(&self) -> isize {
        let csr_helper_func_ptr = self.helper_csrrw.as_ptr() as *const u8;
        let self_ptr = self.head.as_ptr() as *const u8;
        let diff = unsafe { csr_helper_func_ptr.offset_from(self_ptr) };
        diff
    }

実行結果

以下のようなRISC-Vアセンブリ命令をコンパイルしてエミュレートすることを考える。

_start:
    csrr    a0,mhartid
1:
    bnez    a0,1b
    auipc   t0,0x0
    addi    t0,t0,16
    csrw    mtvec,t0       // <-- ここの部分のエミュレート結果を引き抜く。
    csrwi   satp,0
    auipc   t0,0x0
    addi    t0,t0,28
    csrw    mtvec,t0
...

最終的に変換されるx86コードは以下のようになった。これは自作エミュレータQEMU上で実行することで変換されたコードをログにより取得している。

0x4001ca1000:  48 8b 85 08 01 00 00     movq     0x108(%rbp), %rax
0x4001ca1007:  05 00 00 00 00           addl     $0, %eax
0x4001ca100c:  48 89 85 30 00 00 00     movq     %rax, 0x30(%rbp)
0x4001ca1013:  48 8b 85 30 00 00 00     movq     0x30(%rbp), %rax
0x4001ca101a:  05 10 00 00 00           addl     $0x10, %eax
0x4001ca101f:  48 89 85 30 00 00 00     movq     %rax, 0x30(%rbp)
0x4001ca1026:  48 bf 18 21 89 01 40 00  movabsq  $0x4001892118, %rdi    // 第1引数の設定。
0x4001ca102e:  00 00
0x4001ca1030:  be 00 00 00 00           movl     $0, %esi               // 第2引数の設定
0x4001ca1035:  ba 05 00 00 00           movl     $5, %edx               // 第3引数の設定
0x4001ca103a:  b9 05 03 00 00           movl     $0x305, %ecx           // 第4引数の設定
0x4001ca103f:  ff 95 10 21 00 00        callq    *0x2110(%rbp)          // dummy_helper_csrrw()の呼び出し

以下が表示され、dummy_helper_csrrw()が呼び出されていることが確認できた。

helper_csrrw(emu, 0, 5, 0x305) is called!