FPGA開発日記

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

QEMUに入門してみる(19. 分岐命令の実装方法)

QEMU実装の続き。命令実装を進めていく中で、次に実装すべきは分岐命令だ。QEMUで分岐命令をどのように実装するかについて検討を行う。

まずは分岐命令の定義だ。分岐命令はデコードテーブルの中で以下のようにして定義する。

  • qemu/target/myriscvx/insn32.decode
# Branch Instructions
beq      ....... .....    ..... 000 ..... 1100011 @b
bne      ....... .....    ..... 001 ..... 1100011 @b
blt      ....... .....    ..... 100 ..... 1100011 @b
bge      ....... .....    ..... 101 ..... 1100011 @b
bltu     ....... .....    ..... 110 ..... 1100011 @b
bgeu     ....... .....    ..... 111 ..... 1100011 @b

次にTranslatorの定義を行う。Translatorで実施すべきことは、

  • 2つのレジスタを読み込む。
  • 比較を行う。比較が成立したら次の場所までジャンプする命令を実行する。
  • 比較が成立しなかった場合は、PCを進めるだけで何もしない。

となる。これを単純に実装すると以下のようになる。

  • qemu/target/myriscvx/insn_trans/trans_rvi.inc.c
static bool trans_beq(DisasContext *ctx, arg_beq *a)
{
  TCGLabel *label = gen_new_label();

  TCGv rs1_val = tcg_temp_new();
  TCGv rs2_val = tcg_temp_new();

  gen_get_gpr(rs1_val, a->rs1);
  gen_get_gpr(rs2_val, a->rs2);

  TCGv res = tcg_temp_new();
  tcg_gen_brcond_tl(TCG_COND_EQ, rs1_val, rs2_val, label);
  gen_goto_tb(ctx, 0, ctx->base.pc_next + 4);   // False Condition: Go through
  gen_set_label(label);  // True Condition --> Jump
  gen_goto_tb(ctx, 0, ctx->base.pc_next + a->imm);

  tcg_temp_free(rs1_val);
  tcg_temp_free(rs2_val);
  tcg_temp_free(res);

  return true;
}
  1. 2つのレジスタ値を取り出す。これはgen_get_gpr()により実現される。
  2. 比較はtcg_gen_brcond_tl()により実行する。今回はBEQ命令なのでTCG_COND_EQオペレーションが適用される。もしも比較が成立した場合、ラベルlabelに飛ぶような命令が生成される。
  3. labelに飛ばない場合は直後のgen_goto_tb()が実行される。これはこのTB自体を終了させる意味合いを持っており、次のPC+4に命令実行を移す。
  4. labelを配置し、以下に分岐成立時の処理を記述する。ここではgen_goto_tbによりジャンプ先にジャンプする記述が実装されている。

というわけで実装内でラベルを配置してまるでさながらアセンブリ命令のように処理を配置していくわけだ。これでBEQ命令を実行できる。

で、これをBEQ命令以外のBNE, BLT, BGE, BLTU, BGEUも同じように実装するわけだが、面倒なのでdefineマクロを作って統一化した。

#define TCG_BRANCH(op)                                                         \
  TCGLabel *label = gen_new_label();                                           \
                                                                               \
  TCGv rs1_val = tcg_temp_new();                                               \
  TCGv rs2_val = tcg_temp_new();                                               \
                                                                               \
  gen_get_gpr(rs1_val, a->rs1);                                                \
  gen_get_gpr(rs2_val, a->rs2);                                                \
                                                                               \
  TCGv res = tcg_temp_new();                                                   \
  tcg_gen_brcond_tl(op, rs1_val, rs2_val, label);                              \
  gen_goto_tb(ctx, 1, ctx->pc_succ_insn);   /* False Condition: Go through */ \
  gen_set_label(label);  /* True Condition --> Jump */                          \
  gen_goto_tb(ctx, 0, ctx->base.pc_next + a->imm);                             \
                                                                               \
  tcg_temp_free(rs1_val);                                                      \
  tcg_temp_free(rs2_val);                                                      \
  tcg_temp_free(res);                                                          \
                                                                               \
  return true;

すると各分岐命令の実装は以下のようになる。

static bool trans_bne(DisasContext *ctx, arg_bne *a)
{
  TCG_BRANCH(TCG_COND_NE);
}


static bool trans_blt(DisasContext *ctx, arg_blt *a)
{
  TCG_BRANCH(TCG_COND_LT);
}


static bool trans_bge(DisasContext *ctx, arg_bge *a)
{
  TCG_BRANCH(TCG_COND_GE);
}


static bool trans_bltu(DisasContext *ctx, arg_bltu *a)
{
  TCG_BRANCH(TCG_COND_LTU);
}


static bool trans_bgeu(DisasContext *ctx, arg_bgeu *a)
{
  TCG_BRANCH(TCG_COND_GEU);
}

これでとりあえず完了だ。ここまでの実装を来ないQEMUをリコンパイルして実行すると、以下のようなトレースが出力された。

$ qemu-system-myriscvx64 --machine virt --d in_asm \
    --nographic --trace enable=myriscvx_trap --kernel rv64ui-p-simple
Priv: 3; Virt: 0
0x0000000000001000:  00000297          auipc           t0,0            # 0x1000
0x0000000000001004:  02028593          addi            a1,t0,32
0x0000000000001008:  f1402573          csrrs           a0,mhartid,zero

----------------
IN:
Priv: 3; Virt: 0
0x000000000000100c:  0182b283          ld              t0,24(t0)
0x0000000000001010:  00028067          jr              t0

----------------
IN:
Priv: 3; Virt: 0
0x0000000080000000:  04c0006f          j               76              # 0x8000004c

----------------
IN:
Priv: 3; Virt: 0
0x000000008000004c:  f1402573          csrrs           a0,mhartid,zero

----------------
IN:
Priv: 3; Virt: 0
0x0000000080000050:  00051063          bnez            a0,0            # 0x80000050
0x0000000080000054:  00000297          auipc           t0,0            # 0x80000054
0x0000000080000058:  01028293          addi            t0,t0,16
0x000000008000005c:  30529073          csrrw           zero,mtvec,t0

----------------
IN:
Priv: 3; Virt: 0
0x0000000080000054:  00000297          auipc           t0,0            # 0x80000054
0x0000000080000058:  01028293          addi            t0,t0,16
0x000000008000005c:  30529073          csrrw           zero,mtvec,t0

30487@1595649721.398664:myriscvx_trap hart:0, async:0, cause:2, epc:0x8000005c, tval:0x0, desc=illegal_instruction
----------------

分岐命令が実行されることを確認できた。しかし今度はcsrrwで再び命令例外だ。これはmtvecレジスタを実装していないことを意味する。先は長いなあ。。。