QEMUがRISC-VのCSR命令をエミュレートする仕組み
QEMUが複雑な命令をエミュレートする場合、TCG(Tiny Code Generator)によるx86命令への直接的な変換を行うだけでなく、ホストコードのサポート関数を直接呼び出してその実行を肩代わりする場合がある。例えば、RISC-VのCSRRW命令のシステムレジスタ操作命令はシステムレジスタの種類に応じて命令の動作が切り替わるためTCGでの実現が非常に難しい。実際にはQEMUでCSR操作命令は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) ...
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!