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レジスタを実装していないことを意味する。先は長いなあ。。。

QEMUに入門してみる(18. ディスアセンブル機能の実装)

前回のQEMUの実行ログを見ると、命令のニーモニックが出力されていなかった。どうしたら命令のディスアセンブルが表示されるんだろう?ということでいろいろ調査した。

IN:
Priv: 3; Virt: 0
0x0000000000001000:
OBJD-T: 9702000093850202732540f1

このOBJD-Tというのがキーワードな気がする。これをQEMUのリポジトリで検索してみると、

  • qemu/disas.c
static int print_insn_od_target(bfd_vma pc, disassemble_info *info)
{
    return print_insn_objdump(pc, info, "OBJD-T");
}

print_insn_od_target()が呼ばれていることが分かった。さらにトレースしていくと、やはり以下のコールバック関数が登録されていないことが分かった。

  • qemu/target/myriscvx/cpu.c
static void myriscvx_cpu_class_init(ObjectClass *c, void *data)
{
  MYRISCVXCPUClass *mcc = MYRISCVX_CPU_CLASS(c);
  CPUClass *cc = CPU_CLASS(c);
  DeviceClass *dc = DEVICE_CLASS(c);

  device_class_set_parent_realize(dc, myriscvx_cpu_realize,
                                  &mcc->parent_realize);
...
#if defined(TARGET_MYRISCVX32)
  cc->gdb_core_xml_file = "myriscvx-32bit-cpu.xml";
#elif defined(TARGET_MYRISCVX64)
  cc->gdb_core_xml_file = "myriscvx-64bit-cpu.xml";
#endif
  cc->gdb_stop_before_watchpoint = true;
  // cc->disas_set_info = myriscvx_cpu_disas_set_info; このコードをコメントアウトしていた。
#ifndef CONFIG_USER_ONLY

このmyriscvx_cpu_disas_set_info()は以下のように実装している。

static void myriscvx_cpu_disas_set_info(CPUState *s, disassemble_info *info)
{
  info->print_insn = print_insn_myriscvx64;
}

print_insn_myriscvx64()は、どうやら手動で実装するらしい。まじか!

  • qemu/disas/myriscvx.c
/* types */

typedef uint64_t rv_inst;
typedef uint16_t rv_opcode;

...
static int
print_insn_myriscvx(bfd_vma memaddr, struct disassemble_info *info, rv_isa isa)
{
  char buf[128] = { 0 };
  bfd_byte packet[2];
  rv_inst inst = 0;
  size_t len = 2;
...
    
  disasm_inst(buf, sizeof(buf), isa, memaddr, inst);
  (*info->fprintf_func)(info->stream, "%s", buf);

  return len;
}
/* disassemble instruction */

static void
disasm_inst(char *buf, size_t buflen, rv_isa isa, uint64_t pc, rv_inst inst)
{
  rv_decode dec = { 0 };
  dec.pc = pc;
  dec.inst = inst;
  decode_inst_opcode(&dec, isa);
  decode_inst_operands(&dec);
  decode_inst_decompress(&dec, isa);
  decode_inst_lift_pseudo(&dec);
  format_inst(buf, buflen, 16, &dec);
}
f:id:msyksphinz:20200723013338p:plain:w300
disasm_inst()の実行フロー

decode_inst_opcode()は機械語ビット列に応じてdec->opを更新する。フォーマットは以下のように設定している。

...
#define rv_fmt_none                   "O\t"
#define rv_fmt_rs1                    "O\t1"
#define rv_fmt_offset                 "O\to"
#define rv_fmt_pred_succ              "O\tp,s"
#define rv_fmt_rs1_rs2                "O\t1,2"
#define rv_fmt_rd_imm                 "O\t0,i"
...

最終的にbufに命令フォーマットが生成される。これを出力するわけだ。

そして、MYRISCVXのdisas.cに登録する。

  • qemu/include/disas/dis-asm.h
int print_insn_myriscvx32       (bfd_vma, disassemble_info*);
int print_insn_myriscvx64       (bfd_vma, disassemble_info*);
  • qemu/disas/Makefile.objs
...
common-obj-$(CONFIG_RISCV_DIS) += riscv.o
common-obj-$(CONFIG_MYRISCVX_DIS) += myriscvx.o
common-obj-$(CONFIG_S390_DIS) += s390.o
...

これでリコンパイルして再びテストを実行した。

$ qemu-system-myriscvx64 --machine virt --d in_asm \
    --nographic --trace enable=myriscvx_trap \
    --kernel rv64ui-p-simple 2>&1 | tee qemu.myriscvx64.log
(qemu) ----------------
IN:
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

上手く行ったようだ。ディスアセンブル結果が表示された。

QEMUに入門してみる(17. CSR操作命令が実行される手順の解析)

とりあえずQEMUのブートコードだけ動かしたいので、次はCSR命令を実装しなければならない。RISC-VのCSR命令がQEMUでどのように実行されているかを解析しよう。

まず、CSR命令をinsn32.decodeに追加する。これでCSR命令のデコーダが自動的に追加され、trans_xxx関数を追加する必要性が発生する。

  • qemu/target/myriscvx/insn32.decode
@csr     ............   .....  ... ..... .......               %csr     %rs1 %rd

csrrw    ............     ..... 001 ..... 1110011 @csr
csrrs    ............     ..... 010 ..... 1110011 @csr
csrrc    ............     ..... 011 ..... 1110011 @csr
csrrwi   ............     ..... 101 ..... 1110011 @csr
csrrsi   ............     ..... 110 ..... 1110011 @csr
csrrci   ............     ..... 111 ..... 1110011 @csr
  • qemu/target/myriscvx/insn_trans/trans_rvi.inc.c
static bool trans_csrrw(DisasContext *ctx, arg_csrrw *a)
{
  TCGv source1, csr_store, dest, rs1_pass;
  MYRISCVX_OP_CSR_PRE;
  gen_helper_csrrw(dest, cpu_env, source1, csr_store);
  MYRISCVX_OP_CSR_POST;
  return true;
}

で、このtrans_csrrw()の中身だが、TCGvというのはTiny Code Generatorの変数であるとして、MYRISCVX_OP_CSR_PREMYRISCVX_OP_CSR_POSTはマクロである。すぐ上にその定義があるが、正直良く意味が分からない。

#define MYRISCVX_OP_CSR_PRE do {                   \
    source1 = tcg_temp_new();                   \
    csr_store = tcg_temp_new();                 \
    dest = tcg_temp_new();                      \
    rs1_pass = tcg_temp_new();                  \
    gen_get_gpr(source1, a->rs1);               \
    tcg_gen_movi_tl(cpu_pc, ctx->base.pc_next); \
    tcg_gen_movi_tl(rs1_pass, a->rs1);          \
    tcg_gen_movi_tl(csr_store, a->csr);         \
    gen_io_start();                             \
  } while (0)

#define MYRISCVX_OP_CSR_POST do {                  \
    gen_set_gpr(a->rd, dest);                   \
    tcg_gen_movi_tl(cpu_pc, ctx->pc_succ_insn); \
    exit_tb(ctx);                               \
    ctx->base.is_jmp = DISAS_NORETURN;          \
    tcg_temp_free(source1);                     \
    tcg_temp_free(csr_store);                   \
    tcg_temp_free(dest);                        \
    tcg_temp_free(rs1_pass);                    \
  } while (0)

gen_helper_csrrw()だが、これは一応関数定義上どこにも存在しないので最初は少しびっくりする。実はマクロで定義されており、

  • qemu/target/myriscvx/helper.h
/* Special functions */
DEF_HELPER_3(csrrw, tl, env, tl, tl)
DEF_HELPER_4(csrrs, tl, env, tl, tl, tl)
DEF_HELPER_4(csrrc, tl, env, tl, tl, tl)

このDEF_HELPERマクロがgen_helper_csrrw()を定義している。このマクロは最終的にtcg_gen_callN(HELPER(csrrw))を呼び出している。このHELPE()マクロは、

#define HELPER(name) glue(helper_, name)

なので、結局helper_csrrw()を呼び出していることになる。これはqemu/target/myriscvx/op_helper.cに定義した。

  • qemu/target/myriscvx/op_helper.c
target_ulong helper_csrrw(CPUMYRISCVXState *env, target_ulong src,
                          target_ulong csr)
{
  target_ulong val = 0;
  if (myriscvx_csrrw(env, csr, &val, src, -1) < 0) {
    myriscvx_raise_exception(env, MYRISCVX_EXCP_ILLEGAL_INST, GETPC());
  }
  return val;
}
f:id:msyksphinz:20200722012716p:plain
trans_csrrs()が動作する手順

C言語で記述してあるmyriscvx_csrrw()を呼び出す。これはqemu/target/myriscvx/csr.cに記述した。

/*
 * myriscvx_csrrw - read and/or update control and status register
 *
 * csrr   <->  myriscvx_csrrw(env, csrno, ret_value, 0, 0);
 * csrrw  <->  myriscvx_csrrw(env, csrno, ret_value, value, -1);
 * csrrs  <->  myriscvx_csrrw(env, csrno, ret_value, -1, value);
 * csrrc  <->  myriscvx_csrrw(env, csrno, ret_value, 0, value);
 */

int myriscvx_csrrw(CPUMYRISCVXState *env, int csrno, target_ulong *ret_value,
                   target_ulong new_value, target_ulong write_mask)
{
  int ret;
  target_ulong old_value;
...
    

で、ここで命令実行に失敗していたのでGDBを当てて観察していたのだが、CSR拡張のフラグチェックの部分をまじめに実装していなかったので命令実行に失敗していたらしい。とりあえず省略。

  /* ensure the CSR extension is enabled. */
  // if (!cpu->cfg.ext_icsr) {
  //   return -1;
  // }

QEMUにおいてRISC-VのCSRがどのように実装してあるかという話だが、CSRのオペレーションを各CSRで定義し、これをリスト化してある。

/* CSR function table */
static myriscvx_csr_operations csr_ops[];

/* CSR function table constants */
enum {
    CSR_TABLE_SIZE = 0x1000
};

CSRのオペレーションはとりあえず最小限必要なmhartidだけ用意した。

/* Control and Status Register function table */
static myriscvx_csr_operations csr_ops[CSR_TABLE_SIZE] = {
  [CSR_MHARTID] =             { any,  read_mhartid                        },
};

read_mhartid()という関数を定義する。これがMHARTIDレジスタを読み出す動作になる。

static int read_mhartid(CPUMYRISCVXState *env, int csrno, target_ulong *val)
{
    *val = env->mhartid;
    return 0;
}

これで一応CSR命令は通過することが確認できた。でもまだ命令トレースが出てこないなあ。どのように出力されているのか確認したい。

QEMU 5.0.0 monitor - type 'help' for more information
(qemu) ----------------
IN:
Priv: 3; Virt: 0
0x0000000000001000:
OBJD-T: 9702000093850202732540f1

----------------
IN:
Priv: 3; Virt: 0
0x000000000000100c:
OBJD-T: 83b2820167800200

----------------
IN:
Priv: 3; Virt: 0
0x0000000080000000:
OBJD-T: 6f00c004

----------------
...

QEMUに入門してみる(16. 命令デコーダの追加)

前回の続き。所望の場所から命令をフェッチできるようになったので、次は命令デコーダを追加していきたい。QEMUにおける命令デコーダはDSLで記述されており、MYRISCVXの場合はqemu/target/myriscvx/insn32.decodeに置いている。これはRISC-Vにおけるデコーダの配置場所と全く同じだ。

  • qemu/target/myriscvx/insn32.decode
# Formats

lw       ............     ..... 010 ..... 0000011 @i
sw       .......  .....   ..... 010 ..... 0100011 @s

とりあえずリセットベクタの最初の数命令が動いてほしいので、AUIPC, LUI, ADDIなどを追加した。CSR命令はとりあえず省略だ。

# *** RV32I Base Instruction Set ***
lui      ....................       ..... 0110111 @u
auipc    ....................       ..... 0010111 @u
jal      ....................       ..... 1101111 @j
jalr     ............     ..... 000 ..... 1100111 @i

lw       ............     ..... 010 ..... 0000011 @i
sw       .......  .....   ..... 010 ..... 0100011 @s

addi     ............     ..... 000 ..... 0010011 @i

と、こんな感じで命令デコーダを追加すると自動的に命令テンプレートが生成されるので、これを追加していかなければならない。命令のテンプレートはTCGを利用して命令の動作を記述していく。これは普通のシミュレータと非常に似ている。

static bool trans_lui(DisasContext *ctx, arg_lui *a)
{
  if (a->rd != 0) {
    tcg_gen_movi_tl(cpu_gpr[a->rd], a->imm);
  }
  return true;
}

static bool trans_auipc(DisasContext *ctx, arg_auipc *a)
{
  if (a->rd != 0) {
    tcg_gen_movi_tl(cpu_gpr[a->rd], a->imm + ctx->base.pc_next);
  }
  return true;
}

例えばADDI命令の場合は以下のようなTCGを組み合わせることになる。

static bool trans_addi(DisasContext *ctx, arg_addi *a)
{
  TCGv source1;
  source1 = tcg_temp_new();

  gen_get_gpr(source1, a->rs1);

  tcg_gen_addi_tl(source1, source1, a->imm);

  gen_set_gpr(a->rd, source1);
  tcg_temp_free(source1);
  return true;
}

trans_addiではレジスタを取得、加算のためのTCGを追加して、レジスタに結果を格納する。これでADDI命令が実行可能となる。

これで再度QEMUをビルドして動作を確認する。この際、CSR命令が嫌なのでリセットベクタを以下のように急遽変更した。

  • qemu/hw/myriscvx/virt.c
static void myriscvx_virt_board_init(MachineState *machine)
{
  const struct MemmapEntry *memmap = virt_memmap;
  MYRISCVXVirtState *s = MYRISCVX_VIRT_MACHINE(machine);
  MemoryRegion *system_memory = get_system_memory();
...
//  /* reset vector */
//  uint32_t reset_vec[8] = {
//    0x00000297,                  /* 1:  auipc  t0, %pcrel_hi(dtb) */
//    0x02028593,                  /*     addi   a1, t0, %pcrel_lo(1b) */
//    0xf1402573,                  /*     csrr   a0, mhartid  */
//#if defined(TARGET_MYRISCVX32)
//    0x0182a283,                  /*     lw     t0, 24(t0) */
//#elif defined(TARGET_MYRISCVX64)
//    0x0182b283,                  /*     ld     t0, 24(t0) */
//#endif
//    0x00028067,                  /*     jr     t0 */
//    0x00000000,
//    start_addr,                  /* start: .dword */
//    0x00000000,
//    /* dtb: */
//  };

  uint32_t reset_vec[8] = {
    0x00000297,                  /* 1:  auipc  t0, %pcrel_hi(dtb) */
    0x02028593,                  /*     addi   a1, t0, %pcrel_lo(1b) */
    0x00128593,
    0x00228593,
    0x00328593,
    0x00428593,
    0x00528593,
    start_addr,                  /* start: .dword */
  };
$ ${QEMU_BUILD}/myriscvx64-softmmu/qemu-system-myriscvx64 --machine virt \
    --d in_asm --nographic \
    --d op \                    # TCGオペレーションのログを表示する
    --trace myriscvx_trap \     # myriscvx_trapを追加する
    --kernel ${RISCV}/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-p-simple
QEMU 5.0.0 monitor - type 'help' for more information
(qemu) OP:
 ld_i32 tmp0,env,$0xfffffffffffffff0
 movi_i32 tmp1,$0x0
 brcond_i32 tmp0,tmp1,lt,$L0

 ---- 0000000000001000
 movi_i64 x5/t0,$0x1000

 ---- 0000000000001004
 mov_i64 tmp2,x5/t0
 movi_i64 tmp3,$0x20
 add_i64 tmp2,tmp2,tmp3
 mov_i64 x11/a1,tmp2

 ---- 0000000000001008
 mov_i64 tmp2,x5/t0
 movi_i64 tmp3,$0x1
 add_i64 tmp2,tmp2,tmp3
 mov_i64 x11/a1,tmp2

 ---- 000000000000100c
 mov_i64 tmp2,x5/t0
 movi_i64 tmp3,$0x2
 add_i64 tmp2,tmp2,tmp3
 mov_i64 x11/a1,tmp2

 ---- 0000000000001010
 mov_i64 tmp2,x5/t0

うまく実行できていることが確認できた。では、引き続きTCGの追加を進めていこう。

ちなみにtrans_addi()のコンパイル結果は以下のような感じだ。結構長く感じるのはデバッグビルドだからか。リリースビルドなら、おそらくインライン展開されてどこに配置されているのかもわからなくなるだろう。

f:id:msyksphinz:20200718125338p:plain

QEMUに入門してみる(15. QEMUにおけるリセットベクタの追加方法)

前回QEMUのトレース情報を取得しながらシミュレーションをスタートできるようになったが、よく見てみると最初の命令でいきなりIllegal Instructionで例外に飛んでいる。当たり前だ、そこに命令は配置されておらず、しかもデコーダもろくに実装していないので例外に飛んでいるのは至極順当どいえよう。

次にやらなければならないのは2つ:

  • リセットベクタを正しく設定し、最初のフェッチを成功させる。
  • 命令デコーダを追加し、命令を正しく実行できるようにする。

まずはリセットベクタの位置を修正しなければ始まらない。この実装方法について調査する。

まず、QEMUにおけるCPUが立ち上がる(リセット解除ではなくインスタンス化される)のはおそらくxxx_cpu_realize()という関数が担当している。一方で、リセットが行われて命令実行が開始されるのはxxx_cpu_reset()という関数の役割である。すると、CPUのリセット時にリセットベクタを設定するのが良さそうだ。

static void myriscvx_cpu_reset(DeviceState *dev)
{
  CPUState *cs = CPU(dev);
  MYRISCVXCPU *cpu = MYRISCVX_CPU(cs);
  MYRISCVXCPUClass *mcc = MYRISCVX_CPU_GET_CLASS(cpu);
  CPUMYRISCVXState *env = &cpu->env;

#ifndef CONFIG_USER_ONLY
  env->priv = PRV_M;
  env->mstatus &= ~(MSTATUS_MIE | MSTATUS_MPRV);
  env->mcause = 0;
  env->pc = env->resetvec;        // リセットベクタの設定
#endif
  cs->exception_index = EXCP_NONE;
  env->load_res = -1;

  mcc->parent_reset(dev);
  cs->exception_index = EXCP_NONE;
}

ここでenv->resetvecmyriscvx_cpu_realize()実行時に設定される。設定される値は、マクロDEFAULT_RSTVECで設定されている。

static void myriscvx_cpu_realize(DeviceState *dev, Error **errp)
{
  CPUState *cs = CPU(dev);
  MYRISCVXCPU *cpu = MYRISCVX_CPU(dev);
  CPUMYRISCVXState *env = &cpu->env;
  MYRISCVXCPUClass *mcc = MYRISCVX_CPU_GET_CLASS(dev);
...
  // set_priv_version(env, priv_version);
  set_resetvec(env, DEFAULT_RSTVEC);
...
  • qemu/target/myriscvx/cpu_bits.h
/* Default Reset Vector adress */
#define DEFAULT_RSTVEC      0x1000

この状態でQEMUをビルドしてトレースログを取ってみる。無事に0x1000からフェッチが開始された。ただしまだIllegal Instructionで例外に飛ぶ。デコーダを追加していないので当然である。

QEMU 5.0.0 monitor - type 'help' for more information
(qemu) ----------------
IN:
Priv: 3; Virt: 0
0x0000000000001000:
OBJD-T: 97020000

29197@1595039322.887998:myriscvx_trap hart:0, async:0, cause:2, epc:0x1000, tval:0x0, desc=illegal_instruction
----------------
IN:

ちなみに、0x1000にはhw/myriscvx/virt.cで設定したリセットベクタ用のブートROMが格納されている、はずだ。

  • qemu/hw/myriscvx/virt.c
static void myriscvx_virt_board_init(MachineState *machine)
{
  const struct MemmapEntry *memmap = virt_memmap;
  MYRISCVXVirtState *s = MYRISCVX_VIRT_MACHINE(machine);
  MemoryRegion *system_memory = get_system_memory();
...
  /* reset vector */
  uint32_t reset_vec[8] = {
    0x00000297,                  /* 1:  auipc  t0, %pcrel_hi(dtb) */
    0x02028593,                  /*     addi   a1, t0, %pcrel_lo(1b) */
    0xf1402573,                  /*     csrr   a0, mhartid  */
#if defined(TARGET_MYRISCVX32)
    0x0182a283,                  /*     lw     t0, 24(t0) */
#elif defined(TARGET_MYRISCVX64)
    0x0182b283,                  /*     ld     t0, 24(t0) */
#endif
    0x00028067,                  /*     jr     t0 */
    0x00000000,
    start_addr,                  /* start: .dword */
    0x00000000,
    /* dtb: */
  };

  /* copy in the reset vector in little_endian byte order */
  for (i = 0; i < sizeof(reset_vec) >> 2; i++) {
    reset_vec[i] = cpu_to_le32(reset_vec[i]);
  }
  rom_add_blob_fixed_as("mrom.reset", reset_vec, sizeof(reset_vec),
                        memmap[VIRT_MROM].base, &address_space_memory);

一応、最初の0x0000_0297がトレースログで見えているので、動いてはいるのだろう。ということはこのトレースログはビッグエンディアンで表示しているのか?

Priv: 3; Virt: 0
0x0000000000001000:
OBJD-T: 97020000        // <-- ココ

QEMUに入門してみる(14. トレース関数の自動生成方法)

QEMUにはトレース出力用の関数を自動的に生成するフレームワークが存在している。これは命令トレースを出力するのとは異なり、各種イベントを取得するためのフレームワークを生成するものだ。

まず、取りたいイベントを定義しなければならない。target/myriscvx/trace-eventsというファイルを作成してそこに記述する。以下はRISC-Vの実装から取ってきた。

  • qemu/target/myriscvx/trace-events
# target/myriscvx/cpu_helper.c
myriscvx_trap(uint64_t hartid, bool async, uint64_t cause, uint64_t epc, uint64_t tval, const char *desc) "hart:%"PRId64", async:%d, cause:%"PRId64", epc:0x%"PRIx64", tval:0x%"PRIx64", desc=%s"

myriscvx_trap()という関数を自動生成する。ここでは引数を指定しており、その引数を表示するためのprintf()のフォーマットを順番に与えている。この情報に基づいて関数が自動的に生成される。

このqemu/target/myriscvx/trace-eventsを利用するために、qemu/Makefile.objsに当該ディレクトリを追加する必要がある。これによりビルド対象に当該ディレクトリのtrace-eventsファイルが含まれるようになる。

  • qemu/Makefile.objs
...
trace-events-subdirs += target/mips
trace-events-subdirs += target/ppc
trace-events-subdirs += target/riscv
trace-events-subdirs += target/myriscvx      # これを追加する。
trace-events-subdirs += target/s390x
trace-events-subdirs += target/sparc
...

これでQEMUのビルドを行うと、ビルドディレクトリにトレースファイル用のヘッダと実装が自動的に生成されていることが分かる。

 ls -lt build-myriscvx/target/myriscvx
total 32
-rw-rw-rw- 1 msyksphinz msyksphinz 20592 Jul 17 23:22 trace.o
-rw-rw-rw- 1 msyksphinz msyksphinz  8188 Jul 17 23:22 trace.d
-rw-rw-rw- 1 msyksphinz msyksphinz   644 Jul 17 23:22 trace.c
-rw-rw-rw- 1 msyksphinz msyksphinz  1373 Jul 17 23:22 trace.h
-rw-rw-rw- 1 msyksphinz msyksphinz   644 Jul 17 23:22 trace.c-timestamp
-rw-rw-rw- 1 msyksphinz msyksphinz  1373 Jul 17 23:22 trace.h-timestamp

trace.hを開いてみよう。先ほどの定義に基づいて関数が作られていることが分かる。

  • build-myriscvx/target/myriscvx/trace.h
static inline void _nocheck__trace_myriscvx_trap(uint64_t hartid, bool async, uint64_t cause, uint64_t epc, uint64_t tval, const char * desc)
{
    if (trace_event_get_state(TRACE_MYRISCVX_TRAP) && qemu_loglevel_mask(LOG_TRACE)) {
        struct timeval _now;
        gettimeofday(&_now, NULL);
        qemu_log("%d@%zu.%06zu:myriscvx_trap " "hart:%"PRId64", async:%d, cause:%"PRId64", epc:0x%"PRIx64", tval:0x%"PRIx64", desc=%s" "\n",
                 qemu_get_thread_id(),
                 (size_t)_now.tv_sec, (size_t)_now.tv_usec
                 , hartid, async, cause, epc, tval, desc);
    }
}

static inline void trace_myriscvx_trap(uint64_t hartid, bool async, uint64_t cause, uint64_t epc, uint64_t tval, const char * desc)
{
    if (true) {
        _nocheck__trace_myriscvx_trap(hartid, async, cause, epc, tval, desc);
    }
}

trace.cにはトレースイベントの登録用関数が含まれており、trace_init()が呼び出されていることからこれは自動的に実行されるようだ。

  • build-myriscvx/target/myriscvx/trace.c
TraceEvent _TRACE_MYRISCVX_TRAP_EVENT = {
    .id = 0,
    .vcpu_id = TRACE_VCPU_EVENT_NONE,
    .name = "myriscvx_trap",
    .sstate = TRACE_MYRISCVX_TRAP_ENABLED,
    .dstate = &_TRACE_MYRISCVX_TRAP_DSTATE 
};
TraceEvent *target_myriscvx_trace_events[] = {
    &_TRACE_MYRISCVX_TRAP_EVENT,
  NULL,
};

static void trace_target_myriscvx_register_events(void)
{
    trace_event_register_group(target_myriscvx_trace_events);
}
trace_init(trace_target_myriscvx_register_events)

そして、この自動生成されたtrace_myriscvx_trap()をしかるべき場所で呼び出す。今回の実装では割り込みが発生したときのハンドラで呼び出せばよい。

void myriscvx_cpu_do_interrupt(CPUState *cs)
{
#if !defined(CONFIG_USER_ONLY)

  MYRISCVXCPU *cpu = MYRISCVX_CPU(cs);
  CPUMYRISCVXState *env = &cpu->env;
...

  // ここにトレース生成の関数を記述する。
  trace_myriscvx_trap(env->mhartid, async, cause, env->pc, tval, cause < 23 ?
                      (async ? myriscvx_intr_names : myriscvx_excp_names)[cause] : "(unknown)");

  if (env->priv <= PRV_S &&
      cause < TARGET_LONG_BITS && ((deleg >> cause) & 1)) {
...

では、QEMUにオプションを付加してトレースを呼び出してみる。

$ ${QEMU_BUILD}/myriscvx64-softmmu/qemu-system-myriscvx64 --machine virt \
    --d in_asm --nographic \
    --trace myriscvx_trap \     # myriscvx_trapを追加する
    --kernel ${RISCV}/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-p-simple

このmyriscvx_trapはどこから来たのかというとtrace.cで定義されているイベント名で分かる。

TraceEvent _TRACE_MYRISCVX_TRAP_EVENT = {
    .id = 0,
    .vcpu_id = TRACE_VCPU_EVENT_NONE,
    .name = "myriscvx_trap",
    .sstate = TRACE_MYRISCVX_TRAP_ENABLED,
    .dstate = &_TRACE_MYRISCVX_TRAP_DSTATE 
};
IN:
Priv: 3; Virt: 0
0x0000000000000000:
OBJD-T: 00000000

1752@1595002791.401878:myriscvx_trap hart:0, async:0, cause:2, epc:0x0, tval:0x0, desc=illegal_instruction
----------------

トレースファイルが出力されていることが分かる。Illegal Instructionが大量に生成された。ほとんど命令を定義していないからか...

ちなみに、先ほどの--trace myriscvx_trap--trace "*"に変えると大量のトレースを出力することができる。

f:id:msyksphinz:20200718012832p:plain

QEMUに入門してみる(13. 割り込み・例外ハンドラの実装)

QEMUのデバッグ続き。次に落ちたのは以下の部分。GDBで確認する。

$ gdb ${QEMU_BUILD}/myriscvx64-softmmu/qemu-system-myriscvx64
Starting program: ${QEMU_BUILD}/myriscvx64-softmmu/qemu-system-myriscvx64 --machine virt --d in_asm --nographic --kernel ${RISCV}/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-p-simple

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff8f40700 (LWP 27923)]
[New Thread 0x7fffb3ff0700 (LWP 27924)]
QEMU 5.0.0 monitor - type 'help' for more information
(qemu) ----------------
IN:
Priv: 0; Virt: 0
0x0000000000000000:
OBJD-T: 00000000


Thread 3 "qemu-system-myr" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fffb3ff0700 (LWP 27924)]
0x0000000000000000 in ?? ()
(gdb) bt
#0  0x0000000000000000 in  ()
#1  0x00000000004a4727 in cpu_handle_exception (cpu=0xc78fc0, ret=0x7fffb3fef58c) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cpu-exec.c:504
#2  0x00000000004a4d7d in cpu_exec (cpu=0xc78fc0) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cpu-exec.c:712
#3  0x0000000000468061 in tcg_cpu_exec (cpu=0xc78fc0) at /home/msyksphinz/work/riscv/qemu/cpus.c:1405
#4  0x000000000046835a in qemu_tcg_rr_cpu_thread_fn (arg=0xc78fc0) at /home/msyksphinz/work/riscv/qemu/cpus.c:1507
#5  0x0000000000708280 in qemu_thread_start (args=0xc8e350) at /home/msyksphinz/work/riscv/qemu/util/qemu-thread-posix.c:519
#6  0x00007ffffe367164 in start_thread (arg=<optimized out>) at pthread_create.c:486
#7  0x00007ffffe28adef in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

このcpu_handle_exception()の中で、do_interrupt()が呼び出されている。これは実装していなかったので実装しよう。

static inline bool cpu_handle_exception(CPUState *cpu, int *ret)
{
    if (cpu->exception_index < 0) {
#ifndef CONFIG_USER_ONLY
        if (replay_has_exception()
...

        if (replay_exception()) {
            CPUClass *cc = CPU_GET_CLASS(cpu);
            qemu_mutex_lock_iothread();
            cc->do_interrupt(cpu);
            qemu_mutex_unlock_iothread();
            cpu->exception_index = -1;
        } else if (!replay_has_interrupt()) {
            /* give a chance to iothread in replay mode */
            *ret = EXCP_INTERRUPT;
            return true;
        }

do_interrupt()はその名の通り割り込みに対するハンドリングを記述する必要があるらしい。RISC-Vの場合は、

  • qemu/target/riscv/cpu_helper.c
/*
 * Handle Traps
 *
 * Adapted from Spike's processor_t::take_trap.
 *
 */
void riscv_cpu_do_interrupt(CPUState *cs)
{
#if !defined(CONFIG_USER_ONLY)

    RISCVCPU *cpu = RISCV_CPU(cs);
    CPURISCVState *env = &cpu->env;
    bool force_hs_execp = riscv_cpu_force_hs_excep_enabled(env);
    target_ulong s;

    /* cs->exception is 32-bits wide unlike mcause which is XLEN-bits wide
     * so we mask off the MSB and separate into trap type and cause.
     */
    bool async = !!(cs->exception_index & RISCV_EXCP_INT_FLAG);
    target_ulong cause = cs->exception_index & RISCV_EXCP_INT_MASK;
    target_ulong deleg = async ? env->mideleg : env->medeleg;
    target_ulong tval = 0;
    target_ulong htval = 0;
...

これはSpikeにおける割り込みハンドリングに関する処理と同一だ。各割込み要因に応じてtvalの値、mcause, mstatus, mbadaddrなどの値を設定するようになっている。

        s = set_field(s, MSTATUS_MPP, env->priv);
        s = set_field(s, MSTATUS_MIE, 0);
        env->mstatus = s;
        env->mcause = cause | ~(((target_ulong)-1) >> async);
        env->mepc = env->pc;
        env->mbadaddr = tval;
        env->mtval2 = mtval2;
        env->pc = (env->mtvec >> 2 << 2) +
            ((async && (env->mtvec & 3) == 1) ? cause * 4 : 0);
        riscv_cpu_set_mode(env, PRV_M);
    }

最終的にプログラムカウンタはmtvecに格納されている値に設定される。これも想定通りだ。

これをMYRISCVX用に移植する。移植と言っても結構コピーしたが...MYRISCVXではハイパーバイザーは組み込まないのでとりあえずMachineモード・Supervisorモード・Userモードのハンドリングの未実装する。トラップのトレースについては、どうも自動生成?されているためとりあえず省略した。

...
  target_ulong tval = 0;

  if (!async) {
    /* set tval to badaddr for traps with address information */
    switch (cause) {
    case MYRISCVX_EXCP_INST_ADDR_MIS:
    case MYRISCVX_EXCP_INST_ACCESS_FAULT:
    case MYRISCVX_EXCP_LOAD_ADDR_MIS:
    case MYRISCVX_EXCP_STORE_AMO_ADDR_MIS:
    case MYRISCVX_EXCP_LOAD_ACCESS_FAULT:
    case MYRISCVX_EXCP_STORE_AMO_ACCESS_FAULT:
    case MYRISCVX_EXCP_INST_PAGE_FAULT:
    case MYRISCVX_EXCP_LOAD_PAGE_FAULT:
    case MYRISCVX_EXCP_STORE_PAGE_FAULT:
      tval = env->badaddr;
      break;
    default:
      break;
    }
    /* ecall is dispatched as one cause so translate based on mode */
    if (cause == MYRISCVX_EXCP_U_ECALL) {
      assert(env->priv <= 3);

      if (env->priv == PRV_M) {
        cause = MYRISCVX_EXCP_M_ECALL;
      } else if (env->priv == PRV_S) {
        cause = MYRISCVX_EXCP_S_ECALL;
      } else if (env->priv == PRV_U) {
        cause = MYRISCVX_EXCP_U_ECALL;
      }
    }
  }

  // trace_myriscvx_trap(env->mhartid, async, cause, env->pc, tval, cause < 23 ?
  //                     (async ? myriscvx_intr_names : myriscvx_excp_names)[cause] : "(unknown)");

  if (env->priv <= PRV_S &&
      cause < TARGET_LONG_BITS && ((deleg >> cause) & 1)) {

    s = env->mstatus;
    s = set_field(s, MSTATUS_SPIE, get_field(s, MSTATUS_SIE));
    s = set_field(s, MSTATUS_SPP, env->priv);
    s = set_field(s, MSTATUS_SIE, 0);
    env->mstatus = s;
    env->scause = cause | ((target_ulong)async << (TARGET_LONG_BITS - 1));
    env->sepc = env->pc;
    env->sbadaddr = tval;
    env->pc = (env->stvec >> 2 << 2) +
      ((async && (env->stvec & 3) == 1) ? cause * 4 : 0);
    myriscvx_cpu_set_mode(env, PRV_S);
  } else {
    s = env->mstatus;
    s = set_field(s, MSTATUS_MPIE, get_field(s, MSTATUS_MIE));
    s = set_field(s, MSTATUS_MPP, env->priv);
    s = set_field(s, MSTATUS_MIE, 0);
    env->mstatus = s;
...