FPGA開発日記

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

QEMUを作る(RISC-VからTCGへのDynamic Translation)

QEMUの構造について勉強したので、似たようなものをRustで作ってみたくなった。Rustの勉強も兼ねている。ターゲットについてはゲストコードをRISC-V、ホストコードをx86としてとりあえず何か動くものを作ってみたい。

RISC-Vの機械語は、例えば以下のようなものを考えてみる。

0x13, 0x05, 0xa0, 0x00, // addi a0,zero,10
0x67, 0x80, 0x00, 0x00, // ret => JALR x0, x1, 0

a0レジスタに10を代入して、ret命令を実行するだけの命令だ。これをx86に落とし込むためには、どのような構造を作ればいいのか考えてみる。

  • 即値10を用意する。
  • レジスタA10が定義されているアドレスに対して10をmoveする。
  • 関数から戻る。x1に格納されている値にジャンプする。

まずはこのRISC-Vの機械語をデコードする。デコーダはすでに作ってあるのでこれをそのまま流用しよう。

        for inst in &self.m_inst_vec {
            let riscv_id = match decode_inst(*inst) {
                Some(id) => id,
                _ => panic!("Decode Failed"),
            };
        ...

正常にデコードされた場合は、TCG(Tiny Code Generator)に中間コードに変換する。

            let tcg_inst = match riscv_id {
                RiscvInstId::ADDI => Self::tcg_gen_addi(inst),
                RiscvInstId::SUB => Self::tcg_gen_sub(inst),
                RiscvInstId::JALR => Self::tcg_gen_jalr(inst),
                _ => panic!("Not supported these instructions."),
            };

例えばADDIにデコードされた場合にはtcg_gen_addi()を呼び出すが、中身はTCGの構造体を生成するコードになっている。

    fn tcg_gen_addi(inst: &u32) -> Box<TCGOp> {
        let rs1_addr: usize = get_rs1_addr!(*inst) as usize;
        let imm_const: u64 = (*inst as u64) >> 20 & 0xfff;
        let rd_addr: usize = get_rd_addr!(*inst) as usize;

        let rs1 = Box::new(TCGv::new_reg(rs1_addr as u64));
        let imm = Box::new(TCGv::new_imm(imm_const));
        let rd = Box::new(TCGv::new_reg(rd_addr as u64));

        let tcg_inst = Box::new(TCGOp::new(TCGOpcode::ADD, *rd, *rs1, *imm));

        tcg_inst
    }

これを蓄えていく仕組みになっている。TCGOp構造体の構成は以下のようになっており、

#[derive(Debug, Copy, Clone)]
struct TCGOp {
    op: TCGOpcode,
    arg0: TCGv,
    arg1: TCGv,
    arg2: TCGv,
}

TCGvは以下のような構成になっており、即値かレジスタのどちらかを取る。レジスタの場合は、CPUに定義されているレジスタへのポインタを示そうと思ったのだが、どうもlifetimeの関係上実装がとても難しくなったのであきらめ、レジスタアドレスのみを格納しておいた。

#[derive(Debug, Copy, Clone, PartialEq)]
enum TCGvType {
    Register = 0,
    Immediate = 1,
}
#[derive(Debug, Copy, Clone)]
struct TCGv {
    t: TCGvType,
    value: u64,
}

arg0には書き込み先レジスタのポインタ、arg1には読み込みレジスタのポインタ、arg2には即値が入る想定で実装している。

これで上記のRISC-V機械語をデコードすると以下のようになった。

inst = 00a00513
inst = 00008067
tcg_inst = TCGOp { op: ADD, arg0: TCGv { t: Register, value: 10 }, arg1: TCGv { t: Register, value: 0 }, arg2: TCGv { t: Immediate, value: 10 } }
tcg_inst = TCGOp { op: JMP, arg0: TCGv { t: Register, value: 0 }, arg1: TCGv { t: Register, value: 1 }, arg2: TCGv { t: Immediate, value: 0 } }

TCGOp : ADDの場合、書き込みレジスタx10つまりa10、読み込みレジスタはx0, 即値は10となっている。JMPの場合、書き込みレジスタはx0, 読み込みレジスタはx1、第3引数はなにも用意されていない。

上手くデコードできていることが分かる。これを今度はx86機械語に変換していく。