FPGA開発日記

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

Binary Translation型エミュレータを作る(メモリアクセス命令をどのように実現するか)

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エミュレータはいくつかの要素を持っている。

  • RISC-Vの汎用レジスタをエミュレートするための配列: GPR
  • RISC-Vの実行コードを格納しているguestcode

いくつかの変換プロセスを取っているが、分割して要点を解説していく。

  • RISC-Vの実行コードguestcodeをデコードして命令の識別を行い、これをTCGに変換する。これにはRiscvTranslate::translate()により変換する。

ロード・ストア命令は以下のような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を変換するためにはいくつかのプロセスが必要だ。

  1. ロード対象のメモリはRISC-Vのゲストコード中に存在している。このguestcodeのアドレス情報が必要となる。
  2. 汎用レジスタの値を読み込み、guestcodeのアドレスと加算してアクセスするアドレスを決定する。
  3. メモリアクセスを発行する。
  4. ロードした結果を汎用レジスタに格納する。

これらの操作をx86の命令で実現することを考える。

  1. 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に格納している。

  1. 次に、汎用レジスタの値を読み込み、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
    }
  1. メモリアクセスを発行する。これは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);
f:id:msyksphinz:20200905120058p:plain

この結果、ロード命令は以下のような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)

0x4000090620guestcodeのポインタに相当する。これが%rcxに格納される。

%rbpEmuEnvの先頭アドレスを示しており、0x58EmuEnvからGPR[10]までの相対距離を示している。これが%raxに格納されている。

この2つを加算してアドレスを生成し、movq (%rax), %raxによって%raxにロード結果を格納する。その結果を汎用レジスタに格納する。