FPGA開発日記

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

QEMUはどのようにMMUをエミュレートしているのか

QEMUがどのようにしてMMUによるアドレス変換を実現しているのかを観察する。

まず以下のオプションでテストパタンを実行しログの様子を観察する。

./build-riscv/riscv64-softmmu/qemu-system-riscv64 \
    -nographic -d in_asm,mmu \
    -kernel /home/msyksphinz/riscv64/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-v-add 2>&1 | \
    tee rv64ui-v-add.riscv.log

mmuオプションを追加してMMUの動作の様子を観察する。

riscv_cpu_tlb_fill ad 1000 rw 2 mmu_idx 3
riscv_cpu_tlb_fill address=1000 ret 0 physical 0000000000001000 prot 7
----------------
IN: 
Priv: 3; Virt: 0
0x0000000000001000:  00000297          auipc           t0,0            # 0x1000
0x0000000000001004:  02028593          addi            a1,t0,32
0x0000000000001008:  f1402573          csrrs           a0,mhartid,zero

riscv_cpu_tlb_fill()という関数が呼ばれている。

bool riscv_cpu_tlb_fill(CPUState *cs, vaddr address, int size,
                        MMUAccessType access_type, int mmu_idx,
                        bool probe, uintptr_t retaddr)
{
    RISCVCPU *cpu = RISCV_CPU(cs);
    CPURISCVState *env = &cpu->env;
...

内部の実装を見る前に、どのようにしてこの関数が呼ばれたかを観察する。

riscv_cpu_tlb_fill()は、CPU環境のCPUClasstlb_fillに関数ポインタとして登録されている。

  • qemu/target/riscv/cpu_helper.c
static void riscv_cpu_class_init(ObjectClass *c, void *data)
{
    RISCVCPUClass *mcc = RISCV_CPU_CLASS(c);
    CPUClass *cc = CPU_CLASS(c);
    DeviceClass *dc = DEVICE_CLASS(c);
...
#ifdef CONFIG_TCG
    cc->tcg_initialize = riscv_translate_init;
    cc->tlb_fill = riscv_cpu_tlb_fill;
#endif

これがどこから呼ばれているのか分からないのでGDBを使ってスタックフレームを観察する。

#0  0x00000000083ab21d in riscv_cpu_tlb_fill (cs=0x8e049b0, address=4096, size=0, access_type=MMU_INST_FETCH, mmu_idx=3, probe=false, retaddr=0)
    at /home/msyksphinz/work/riscv/qemu/target/riscv/cpu_helper.c:703
#1  0x00000000082f4fc0 in tlb_fill (cpu=0x8e049b0, addr=4096, size=0, access_type=MMU_INST_FETCH, mmu_idx=3, retaddr=0)
    at /home/msyksphinz/work/riscv/qemu/accel/tcg/cputlb.c:1017
#2  0x00000000082f5620 in get_page_addr_code_hostp (env=0x8e0d3d0, addr=4096, hostp=0x0) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cputlb.c:1172
#3  0x00000000082f574d in get_page_addr_code (env=0x8e0d3d0, addr=4096) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cputlb.c:1204
#4  0x000000000830dac3 in tb_htable_lookup (cpu=0x8e049b0, pc=4096, cs_base=0, flags=3, cf_mask=4278714368) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cpu-exec.c:337
#5  0x000000000830cfed in tb_lookup__cpu_state (cpu=0x8e049b0, pc=0x7ffff7e8f558, cs_base=0x7ffff7e8f550, flags=0x7ffff7e8f54c, cf_mask=4278714368)
    at /home/msyksphinz/work/riscv/qemu/include/exec/tb-lookup.h:43
#6  0x000000000830dd84 in tb_find (cpu=0x8e049b0, last_tb=0x0, tb_exit=0, cf_mask=524288) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cpu-exec.c:404
#7  0x000000000830e684 in cpu_exec (cpu=0x8e049b0) at /home/msyksphinz/work/riscv/qemu/accel/tcg/cpu-exec.c:731
#8  0x00000000082d0e9b in tcg_cpu_exec (cpu=0x8e049b0) at /home/msyksphinz/work/riscv/qemu/cpus.c:1405
#9  0x00000000082d16f1 in qemu_tcg_cpu_thread_fn (arg=0x8e049b0) at /home/msyksphinz/work/riscv/qemu/cpus.c:1713
#10 0x000000000875fa62 in qemu_thread_start (args=0x8e1a280) at /home/msyksphinz/work/riscv/qemu/util/qemu-thread-posix.c:519
#11 0x00007ffffdb77164 in start_thread (arg=<optimized out>) at pthread_create.c:486
#12 0x00007ffffda9adef in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

なるほど、上から順に追っていこう。

  • qemu/accel/tcg/cpu-exec.c
/* main execution loop */

int cpu_exec(CPUState *cpu)
{
    CPUClass *cc = CPU_GET_CLASS(cpu);
...
            tb = tb_find(cpu, last_tb, tb_exit, cflags);
            cpu_loop_exec_tb(cpu, tb, &last_tb, &tb_exit);
  • qemu/accel/tcg/cpu-exec.c
static inline TranslationBlock *tb_find(CPUState *cpu,
                                        TranslationBlock *last_tb,
                                        int tb_exit, uint32_t cf_mask)
{
    TranslationBlock *tb;
    target_ulong cs_base, pc;
    uint32_t flags;

    tb = tb_lookup__cpu_state(cpu, &pc, &cs_base, &flags, cf_mask);
    ...
  • qemu/include/exec/tb-lookup.h
/* Might cause an exception, so have a longjmp destination ready */
static inline TranslationBlock *
tb_lookup__cpu_state(CPUState *cpu, target_ulong *pc, target_ulong *cs_base,
                     uint32_t *flags, uint32_t cf_mask)
{
...
    tb = tb_htable_lookup(cpu, *pc, *cs_base, *flags, cf_mask);
...
  • qemu/accel/tcg/cpu-exec.c
TranslationBlock *tb_htable_lookup(CPUState *cpu, target_ulong pc,
                                   target_ulong cs_base, uint32_t flags,
                                   uint32_t cf_mask)
{
...
    phys_pc = get_page_addr_code(desc.env, pc);
  • qemu/accel/tcg/cputlb.c
tb_page_addr_t get_page_addr_code(CPUArchState *env, target_ulong addr)
{
    return get_page_addr_code_hostp(env, addr, NULL);
}
  • qemu/accel/tcg/cputlb.c
tb_page_addr_t get_page_addr_code_hostp(CPUArchState *env, target_ulong addr,
                                        void **hostp)
{
    uintptr_t mmu_idx = cpu_mmu_index(env, true);
    uintptr_t index = tlb_index(env, mmu_idx, addr);
    CPUTLBEntry *entry = tlb_entry(env, mmu_idx, addr);
    void *p;

    if (unlikely(!tlb_hit(entry->addr_code, addr))) {
        if (!VICTIM_TLB_HIT(addr_code, addr)) {
            tlb_fill(env_cpu(env), addr, 0, MMU_INST_FETCH, mmu_idx, 0);
            index = tlb_index(env, mmu_idx, addr);
            entry = tlb_entry(env, mmu_idx, addr);
...

なるほど、フェッチの部分からもTLBをサーチして失敗すればPhysicalAddressを計算するルーチンに移行する訳か。

riscv_cpu_tlb_fill()に立ち戻ってもう一度処理の流れを追いかけてみる。

bool riscv_cpu_tlb_fill(CPUState *cs, vaddr address, int size,
                        MMUAccessType access_type, int mmu_idx,
                        bool probe, uintptr_t retaddr)
{
    RISCVCPU *cpu = RISCV_CPU(cs);
    CPURISCVState *env = &cpu->env;
...

まず、マシンモードでロードストアアクセスの場合はMSTATUSから特権モードを取ってくる。

    if (mode == PRV_M && access_type != MMU_INST_FETCH) {
        if (get_field(env->mstatus, MSTATUS_MPRV)) {
            mode = get_field(env->mstatus, MSTATUS_MPP);
        }
    }

マシンモードでない場合は仮想アドレスから物理アドレスへの変換を行う。Hypervisorでない場合場合は通常これは2ステップで行われる。

    if (riscv_cpu_virt_enabled(env) || m_mode_two_stage || hs_mode_two_stage) {
        /* Two stage lookup */
        ret = get_physical_address(env, &pa, &prot, address, access_type,
                                   mmu_idx, true, true);

            /* Second stage lookup */
            im_address = pa;

            ret = get_physical_address(env, &pa, &prot, im_address,
                                       access_type, mmu_idx, false, true);

...

そうでない場合は1ステップでマシンモードへの変換を行う。

    } else {
        /* Single stage lookup */
        ret = get_physical_address(env, &pa, &prot, address, access_type,
                                   mmu_idx, true, false);

        qemu_log_mask(CPU_LOG_MMU,
                      "%s address=%" VADDR_PRIx " ret %d physical "
                      TARGET_FMT_plx " prot %d\n",
                      __func__, address, ret, pa, prot);
    }

ちなみにget_physical_address()はゴリゴリにC言語で書いてあった。やはりこれはTCGではないらしい。

/* get_physical_address - get the physical address for this virtual address
 *
 * Do a page table walk to obtain the physical address corresponding to a
 * virtual address. Returns 0 if the translation was successful
 *
 * Adapted from Spike's mmu_t::translate and mmu_t::walk
 *
 * @env: CPURISCVState
 * @physical: This will be set to the calculated physical address
 * @prot: The returned protection attributes
 * @addr: The virtual address to be translated
 * @access_type: The type of MMU access
 * @mmu_idx: Indicates current privilege level
 * @first_stage: Are we in first stage translation?
 *               Second stage is used for hypervisor guest translation
 * @two_stage: Are we going to perform two stage translation
 */
static int get_physical_address(CPURISCVState *env, hwaddr *physical,
                                int *prot, target_ulong addr,
                                int access_type, int mmu_idx,
                                bool first_stage, bool two_stage)
{
...