Binary Translation型エミュレータにおいてメモリアクセス命令をどのように実現するかについて纏めておこう。RISC-Vのバイナリからx86の命令に変換して実行する方法をまとめておく。この方式は基本的にQEMUで実現されている方式に基づいているが、初期の実装のためのかなり簡素化している。
0000000000000000 <_start>: 0: 00000537 lui a0,0x0 4: 08450513 addi a0,a0,132 # 84 <data_region> 8: 00053583 ld a1,0(a0) c: 00452603 lw a2,4(a0) 10: 00651683 lh a3,6(a0) 14: 00750703 lb a4,7(a0) 18: 00052803 lw a6,0(a0) ... 3c: 00053823 sd zero,16(a0) 40: 00053c23 sd zero,24(a0) 44: 02053023 sd zero,32(a0) 48: 02053423 sd zero,40(a0) 4c: 00b53823 sd a1,16(a0) 50: 00c52c23 sw a2,24(a0) ... 0000000000000084 <data_region>: 84: deadbeef jal t4,fffffffffffdb66e <__global_pointer$+0xfffffffffffdac52> 88: 01234567 0x1234567 8c: 00000013 nop
ここでBinary Translationエミュレータはいくつかの要素を持っている。
いくつかの変換プロセスを取っているが、分割して要点を解説していく。
ロード・ストア命令は以下のようなTCGに変換された。
tcg_inst = TCGOp { op: Some(LD), arg0: Some(TCGv { t: Register, value: 11 }), arg1: Some(TCGv { t: Register, value: 10 }), arg2: Some(TCGv { t: Immediate, value: 0 }), label: None } tcg_inst = TCGOp { op: Some(SD), arg0: Some(TCGv { t: Register, value: 10 }), arg1: Some(TCGv { t: Register, value: 0 }), arg2: Some(TCGv { t: Immediate, value: 16 }), label: None }
さて、このメモリアクセスのTCGを変換するためにはいくつかのプロセスが必要だ。
- ロード対象のメモリはRISC-Vのゲストコード中に存在している。この
guestcode
のアドレス情報が必要となる。 - 汎用レジスタの値を読み込み、
guestcode
のアドレスと加算してアクセスするアドレスを決定する。 - メモリアクセスを発行する。
- ロードした結果を汎用レジスタに格納する。
これらの操作をx86の命令で実現することを考える。
guestcode
のアドレスはポインタを取得して計算する。以下のcalc_guestcode_address()
はemu
構造体のguestcode
のポインタアドレスを取得している。
// Load Guest Memory Head into EAX gen_size += Self::tcg_out(0x48, 1, mc); gen_size += Self::tcg_out( X86Opcode::MOV_EAX_IV as u32 + X86TargetRM::RCX as u32, 1, mc, ); let guestcode_addr = emu.calc_guestcode_address(); gen_size += Self::tcg_out((guestcode_addr & 0xffff_ffff) as u32, 4, mc); gen_size += Self::tcg_out(((guestcode_addr >> 32) & 0xffff_ffff) as u32, 4, mc);
pub fn calc_guestcode_address(&self) -> usize { let guestcode_ptr = self.m_guestcode.as_ptr(); return guestcode_ptr as usize; }
命令の生成にはMOVABS
命令を使用して64ビット長のポインタアドレスを取得している。ロード命令の結果は%rcx
に格納している。
- 次に、汎用レジスタの値を読み込み、
guestcode
のアドレスと加算してアドレスを生成する。
// Load value from rs1 gen_size += Self::tcg_modrm_64bit_out( X86Opcode::MOV_GV_EV, X86ModRM::MOD_10_DISP_RBP, X86TargetRM::RAX, mc, ); gen_size += Self::tcg_out(emu.calc_gpr_relat_address(arg1.value) as u32, 4, mc); // Execute Load // GPR value + Memory Head Address Self::tcg_modrm_64bit_out( X86Opcode::ADD_GV_EV, X86ModRM::MOD_11_DISP_RCX, X86TargetRM::RAX, mc, );
calc_gpr_relat_address
は、emu
構造体の先頭(これは%rbp
レジスタに格納されている)から汎用レジスタの先頭までの相対アドレスを取得している。
pub fn calc_gpr_relat_address(&self, gpr_addr: u64) -> isize { let guestcode_ptr = self.m_regs.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 }
- メモリアクセスを発行する。これは
MOV
命令によって実現する。
gen_size += match mem_size { MemOpType::LOAD_64BIT => { let mut gen_size = 0; gen_size += Self::tcg_modrm_64bit_out( X86Opcode::MOV_GV_EV, X86ModRM::MOD_10_DISP_RAX, X86TargetRM::RAX, mc, ); gen_size += Self::tcg_out(arg2.value as u32, 4, mc); gen_size } ...
そして、その結果を汎用レジスタに格納する。
// Store Loaded value into destination register. gen_size += Self::tcg_modrm_64bit_out( X86Opcode::MOV_EV_GV, X86ModRM::MOD_10_DISP_RBP, X86TargetRM::RAX, mc, ); gen_size += Self::tcg_out(emu.calc_gpr_relat_address(arg0.value) as u32, 4, mc);
この結果、ロード命令は以下のようなx86命令に変換される。
0x4001c9901e: 48 b9 20 06 09 00 40 00 movabsq $0x4000090620, %rcx 0x4001c99026: 00 00 0x4001c99028: 48 8b 85 58 00 00 00 movq 0x58(%rbp), %rax 0x4001c9902f: 48 03 c1 addq %rcx, %rax 0x4001c99032: 48 8b 80 00 00 00 00 movq (%rax), %rax 0x4001c99039: 48 89 85 60 00 00 00 movq %rax, 0x60(%rbp)
0x4000090620
がguestcode
のポインタに相当する。これが%rcx
に格納される。
%rbp
はEmuEnv
の先頭アドレスを示しており、0x58
がEmuEnv
からGPR[10]
までの相対距離を示している。これが%rax
に格納されている。
この2つを加算してアドレスを生成し、movq (%rax), %rax
によって%rax
にロード結果を格納する。その結果を汎用レジスタに格納する。