FPGA開発日記

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

Binary Translation型エミュレータを作る(レジスタ+レジスタ /レジスタ+即値 演算の実装)

Binary Translation型エミュレータを作っている。ゲストマシンはRISC-Vで、ホストマシンはx86である。ADDI命令は簡単なものであれば実装できるようになった(ソースレジスタがx0の場合に限る)。どんどん実装を進めていこう。次は普通のADDI命令と、ADD命令を実装することを考える。

という構成になっている。これらの命令を受け取ると、TCGは簡単に書き下すと以下のように構成される。

  • ADDI rd,rs1,imm
TCG(opcode::ADD, TCGvType::Register rd, TCGvType::Register rs1, TCGvType::Immediate imm)
  • ADD rd,rs1,rs2
TCG(opcode::ADD, TCGvType::Register rd, TCGvType::Register rs1, TCGvType::Register rs2)

共通フォーマットに変換されれば、次はこれをx86命令に変換する。基本的なポリシは以下の通りだ。

となる。

まずはADDI命令からだ。これはtcg_gen_rri()で実現する。

    fn tcg_gen_rri(op: X86Opcode, tcg: &tcg::TCGOp, mc: &mut Vec<u8>) {
        assert_eq!(tcg.arg0.t, TCGvType::Register);
        assert_eq!(tcg.arg1.t, TCGvType::Register);
        assert_eq!(tcg.arg2.t, TCGvType::Immediate);

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

        // add    imm16,%eax
        Self::tcg_out(op as u32, 1, mc);
        Self::tcg_out(tcg.arg2.value as u32, 4, mc);

        // mov    %eax,reg_offset(%rbp)
        Self::tcg_modrm_out(X86Opcode::MOV_EV_GV, X86ModRM::MOD_10, mc);
        Self::tcg_out(conv_gpr_offset!(tcg.arg0.value), 4, mc);
    }

MOV_GV_EVはEVで指定されるアドレスから値をロードしてGVで示される汎用レジスタに格納する命令だ。Mod/RMでこれらを指定する。

次に演算命令を実行する。これはopで指定している。加算ならばADD_RAX_IVを指定する。オペランドとしてTCGの第2引数の即値を指定する。これにより、%eaxと第2引数の加算が実行され、結果を%eaxに格納する。

MOV_EV_GVは汎用レジスタに格納された値をEVで指定されるアドレスにストアする。Mod/RMでこれらを指定する。

次はADD命令だ。これはtcg_gen_rrr()で実現する。

    fn tcg_gen_rrr(op: X86Opcode, tcg: &tcg::TCGOp, mc: &mut Vec<u8>) {
        assert_eq!(tcg.arg0.t, TCGvType::Register);
        assert_eq!(tcg.arg1.t, TCGvType::Register);
        assert_eq!(tcg.arg1.t, TCGvType::Register);

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

        // add    reg_offset(%rbp),%eax
        Self::tcg_modrm_out(op, X86ModRM::MOD_10, mc);
        Self::tcg_out(conv_gpr_offset!(tcg.arg2.value), 4, mc);

        // mov    %eax,reg_offset(%rbp)
        Self::tcg_modrm_out(X86Opcode::MOV_EV_GV, X86ModRM::MOD_10, mc);
        Self::tcg_out(conv_gpr_offset!(tcg.arg0.value), 4, mc);
    }

MOV_GV_EVはEVで指定されるアドレスから値をロードしてGVで示される汎用レジスタに格納する命令だ。Mod/RMでこれらを指定する。

次に演算命令を実行する。これはopで指定している。加算ならばADD_GV_EVを指定する。オペランドとしてTCGの第2引数を指定する。これにより、%eaxと第2引数の加算が実行され、結果を%eaxに格納する。

MOV_EV_GVは汎用レジスタに格納された値をEVで指定されるアドレスにストアする。Mod/RMでこれらを指定する。

この方針でOKだ。テストパタンを実行してみよう。

  • simple_start2.S
_start:
    addi    x1,  zero, 1
    addi    x2,  x1,  2
    addi    x3,  x2,  3
...
    addi    x17, x16, 17
    addi    x18, x17, 18
    addi    x19, x18, 19
    addi    x20, x19, 20
    add     x21, x20, x19
    add     x22, x21, x20
...
    add     x30, x29, x28
    add     x31, x30, x29

    sub     x31, x30, x29

    ret
$ cargo run /home/msyksphinz/work/riscv/qemu/qemu_test/simple_start2
x00 = 0000000000000000  x01 = 0000000000000001  x02 = 0000000000000003  x03 = 0000000000000006
x04 = 000000000000000a  x05 = 000000000000000f  x06 = 0000000000000015  x07 = 000000000000001c
x08 = 0000000000000024  x09 = 000000000000002d  x10 = 0000000000000037  x11 = 0000000000000042
x12 = 000000000000004e  x13 = 000000000000005b  x14 = 0000000000000069  x15 = 0000000000000078
x16 = 0000000000000088  x17 = 0000000000000099  x18 = 00000000000000ab  x19 = 00000000000000be
x20 = 00000000000000d2  x21 = 0000000000000190  x22 = 0000000000000262  x23 = 00000000000003f2
x24 = 0000000000000654  x25 = 0000000000000a46  x26 = 000000000000109a  x27 = 0000000000001ae0
x28 = 0000000000002b7a  x29 = 000000000000465a  x30 = 00000000000071d4  x31 = 0000000000002b7a

命令実行後のレジスタ状態も問題なさそうだ。

f:id:msyksphinz:20200822232315p:plain