FPGA開発日記

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

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;
...