FPGA開発日記

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

Binary Translation型のエミュレータを作る(条件分岐命令の実装)

Rustで実装しているBinary Translation型エミュレータ、分岐命令を実装するための方針を考える。分岐命令は基本的に以下のような方針で組み立てることにする。

  1. 比較する値をレジスタにロードしてくる。
  2. cmp命令により比較を実行する。
  3. 分岐命令の種類に応じてジャンプ命令を実行する。例えば、BEQの場合はx86のje命令を実行し、成立の場合は所望のラベルまでジャンプする。
  4. 所望のラベルに飛ぶと、それは分岐が成立したということを意味する。実際にRISC-Vの命令上でジャンプすべきアドレスを生成し、これをPCが格納されているメモリ領域に格納する。その後エピローグまでジャンプする。
  5. 分岐が成立しない場合、現在のPCから次の命令を指し示すアドレスを生成し、それをPCが格納されているメモリ領域に格納する。その後エピローグまでジャンプする。

例えば、以下のようなRISC-Vの命令をx86に変換することを考えよう。

 li       x10, 1
    li       x11, 1
    beq      x10, x11, label
    // 分岐が成立しなかった場合の処理
label:
    // 分岐が成立した場合の処理

li疑似命令の処理はADDI命令の実装で完了しているものとして、BEQの処理方法について考える。まずQEMUは分岐命令に到達するとそこで実行単位が一旦停止し、ホストマシンの制御コードに戻ってくると考えて良い。

li        x10, 1
li       x11, 1
beq      x10, x11, label
// ここでJITコンパイルが一旦停止する。

labelの解決しておかなくていいの?と思われるかもしれないが、すでにリンク済みのバイナリであればラベルのアドレスは現在のPCアドレスと相対ジャンプ距離が分かっているので気にする必要はない。むしろ以降に出てくるx86上で処理するためのラベルと混同しがちなので注意。

上記のBEQ命令は、TCGに変換する際に以下のように解釈される。

  1. 分岐先を処理するためのラベルオブジェクトを生成する。
  2. 比較する値をx86レジスタにロードする命令を生成する。
  3. cmp命令を生成しフラグに比較結果を格納する。
  4. Branch when Conditionの命令を生成する。この場合は互いのオペランドが等しい場合にジャンプするので、JE命令を生成する。ジャンプ先としてとりあえずラベルオブジェクトを指定しておく。この時にまだラベルオブジェクトがどのアドレスに配置されるのかは未決定である。
  5. 分岐が成立しなかった場合に、ホストに戻るためのx86命令を生成する。具体的には、仮想マシンのプログラムカウンタに「現在の仮想マシンのプログラムカウンタ+4を格納する命令」を生成し、ホストコードに制御を移す。これはJMP命令でエピローグにジャンプすることで実現できる。
  6. 1.で生成したラベルオブジェクトに対して、TCGの順序的に、ここにラベルが配置されるように教える。
  7. 分岐が成立した場合に、ホストに戻るためのx86命令を生成する。具体的には、仮想マシンのプログラムカウンタに「現在の仮想マシンのプログラムカウンタ⁺ジャンプ距離を格納する命令」を生成し、ホストコードに制御を移す。これはJMP命令でエピローグにジャンプすることで実現できる。

これを具体的にRustで実装したコードが以下になる。

    fn translate_branch(op: TCGOpcode, inst: &InstrInfo) -> Vec<TCGOp> {
        let rs1_addr: usize = get_rs1_addr!(inst.inst) as usize;
        let rs2_addr: usize = get_rs2_addr!(inst.inst) as usize;
        let target: u64 = get_sb_field!(inst.inst) + inst.addr;

        let rs1 = Box::new(TCGv::new_reg(rs1_addr as u64));
        let rs2 = Box::new(TCGv::new_reg(rs2_addr as u64));
        let addr = Box::new(TCGv::new_imm(target));

        let label = Rc::new(RefCell::new(TCGLabel::new()));

        let tcg_inst = TCGOp::new_4op(op, *rs1, *rs2, *addr, Rc::clone(&label));
        let tcg_true_tb = TCGOp::new_goto_tb(TCGv::new_imm(inst.addr + 4));
        let tcg_set_label = TCGOp::new_label(Rc::clone(&label));
        let tcg_false_tb = TCGOp::new_goto_tb(TCGv::new_imm(target));

        vec![tcg_inst, tcg_true_tb, tcg_set_label, tcg_false_tb]
    }

Rc::cloneなどを使っているが、どうしてもlabelを2か所から参照するためにRustの参照カウンタ方式の実装が必要だったのでこうなった。いや、、、これを理解するためにかなり時間がかかってしまった。

従って、ラベルの宣言は以下のようになっており、Rustの制約である通常は1か所のみのアクセスに限定させるオブジェクトを、2か所から参照できるようにしている。

        let label = Rc::new(RefCell::new(TCGLabel::new()));

生成されるTCGは以下のようになっている。

// 条件分岐のためのTCG
tcg_inst = TCGOp { 
  op: Some(EQ), 
  arg0: Some(TCGv { t: Register, value: 10 }), 
  arg1: Some(TCGv { t: Register, value: 11 }), 
  arg2: Some(TCGv { t: Immediate, value: 32 }), 
  label: Some(RefCell { value: TCGLabel { offset: 0, code_ptr_vec: [] } }) 
}

// 条件が成立しなかった場合の処理。PC + 4を(=12)をPCに格納したあとホストに戻る。
tcg_inst = TCGOp { 
  op: Some(MOV), 
  arg0: Some(TCGv { t: ProgramCounter, value: 0 }), 
  arg1: Some(TCGv { t: Immediate, value: 12 }), 
  arg2: None, label: None 
}

// ラベルをここに配置する。
tcg_inst = TCGOp { 
  op: None, 
  arg0: None, 
  arg1: None, 
  arg2: None, 
  label: Some(RefCell { value: TCGLabel { offset: 0, code_ptr_vec: [38] } }) 
}

// 条件が成立した場合の処理。PC + imm(=32)をPCに格納したあとホストに戻る。
tcg_inst = TCGOp { 
  op: Some(MOV), 
  arg0: Some(TCGv { t: ProgramCounter, value: 0 }), 
  arg1: Some(TCGv { t: Immediate, value: 32 }), 
  arg2: None, 
  label: None 
}

次にこれをx86命令に変換する。まず、EQ操作は単純にcmp命令とje命令に置き換える。

    fn tcg_gen_cmp_branch(
        diff_from_epilogue: isize,
        pc_address: u64,
        x86_op: X86Opcode,
        tcg: &mut TCGOp,
        mc: &mut Vec<u8>,
    ) -> usize {
        let arg0 = tcg.arg0.unwrap();
        let arg1 = tcg.arg1.unwrap();
        let arg2 = tcg.arg2.unwrap();
        let mut label = match &mut tcg.label {
            Some(l) => l,
            None => panic!("Label is not defined."),
        };

        let mut gen_size: usize = pc_address as usize;

        // mov    reg_offset(%rbp),%eax
        gen_size += Self::tcg_modrm_out(X86Opcode::MOV_GV_EV, X86ModRM::MOD_10, mc);
        gen_size += Self::tcg_out(conv_gpr_offset!(arg0.value), 4, mc);

        // cmp    reg_offset(%rbp),%eax
        gen_size += Self::tcg_modrm_out(X86Opcode::CMP_GV_EV, X86ModRM::MOD_10, mc);
        gen_size += Self::tcg_out(conv_gpr_offset!(arg1.value), 4, mc);

        gen_size = Self::tcg_gen_jcc(gen_size, x86_op, mc, label);

        return gen_size;
    }

最初のmovで、第1オペランドをEAXにロードする。次にCMP (GV_EV)にて、EAXに格納した値と、メモリオフセットで記述された第2オペランドの比較を行う。最後にjccで所望の条件でジャンプする命令を生成する。

    fn tcg_gen_jcc(
        gen_size: usize,
        x86_op: X86Opcode,
        mc: &mut Vec<u8>,
        label: &mut Rc<RefCell<tcg::TCGLabel>>,
    ) -> usize {
        let mut gen_size = gen_size;

        gen_size += Self::tcg_out(x86_op as u32, 2, mc);
        gen_size += Self::tcg_out(10 as u32, 4, mc);
        gen_size += Self::tcg_out_reloc(gen_size - 4, label);

        return gen_size;
    }

jcc命令はジャンプの条件と、その次にジャンプ先を示す相対アドレスを指定する。それがSelf::tcg_out(10 as u32, 4, mc)によって生成されるのだが、ここで4バイトの領域に対して10を格納している。10という数値はダミーで、実は特に意味はない。この値は最後のリロケーションで置き換えられるからである。

そしてtcg_out_relocによってラベルオブジェクトに対して、「生成されたx86のホストコードのこのアドレスの値を置き換えてください」とマークを付けておく。

    fn tcg_out_reloc(host_code_ptr: usize, label: &mut Rc<RefCell<TCGLabel>>) -> usize {
        let mut l = &mut *label.borrow_mut();
        l.code_ptr_vec.push(host_code_ptr);
        println!("Added offset. code_ptr = {:x}", host_code_ptr);
        return 0;
    }

最後にすべてのラベルに対してリロケーションを行う。ラベルにマークされているすべての場所に対して、ラベルからその場所に対して相対距離を計算し、その値でメモリの値を上書きする(リロケーション)。

        for tcg in &self.m_tcg_vec {
            match tcg.op {
                Some(_) => {}
                None => {
                    println!("label found 2");
                    match &tcg.label {
                        Some(l) => {
                            let mut l = &mut *l.borrow_mut();
                            println!("label found. offset = {:x}", l.offset);
                            for v_off in &l.code_ptr_vec {
                                let diff = l.offset as usize - v_off - 4;
                                println!("replacement target is {:x}, data = {:x}", v_off, diff);
                                let s = tb_map.data();
                                unsafe {
                                    *s.offset(*v_off as isize) = (diff & 0xff) as u8;
                                };
                            }
                        }
                        None => {}
                    }
                }
            }
        }

その結果、生成されるx86のバイナリは以下のようになり、赤で示した部分がリロケーションされている。

f:id:msyksphinz:20200831004726p:plain

その結果、分岐外成立するとPCはジャンプ先に設定され、そうでない場合は分岐命令+4の場所に設定されるようになった。

PC = 0000000000000020    // 分岐が成立した場合
PC = 000000000000000c   // 分岐が成立しなかった場合