FPGA開発日記

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

QEMUによるメモリアクセスを実現する仕組み

QEMUによる分岐命令の実現方法が分かったので、次はロードストア命令について実現方法を見ていきたいと思う。例えばRISC-VにおけるLD命令とSD命令を実行する場合、どのようにx86命令に変換されるのだろうか?

   LD  x10, 0(data)
    SD  x10, 8(data)

以下のようなテストベンチを作って動作を確認してみる。

     .section    .text

 _start:
     la      x10, data_region
     j       1f
 1:
     ld      x11, 0(x10)
     j       1f
 1:
     addi    x11, x11, 0x100

     j       1f
 1:
     sd      x11, 8(x10)


     .section    .data
 data_region:
     .word   0xdeadbeef

ロードストア命令のの直後にジャンプ文を入れているのは、ジャンプすることで必ずそこで分岐が発生するのでブロックが小さくなり、生成されるx86コードを小さくして確認しやすくする。

以下のようにしてテストコードをコンパイルし実行する。

$ make load_test.asm.riscv
riscv64-unknown-elf-gcc -march=rv64g -O3 -o load_test.riscv load_test.S -DPREALLOCATE=1 -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -static -nostdlib -nostartfiles -lm -lgcc -Ttest.ld
/home/msyksphinz/riscv64/lib/gcc/riscv64-unknown-elf/7.2.0/../../../../riscv64-unknown-elf/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000080000000
riscv64-unknown-elf-objdump -d -r load_test.riscv > load_test.riscv.dmp

QEMUで実行して動作を確認する。

qemu-system-riscv64 --machine virt --d in_asm,op,op_opt,op_ind,out_asm \
  --nographic --trace enable=myriscvx_trap \
  --kernel load_test.riscv 2>&1 | tee load_test.riscv.log

対象となるのは以下の命令だ。

IN:
Priv: 3; Virt: 0
0x000000008000000c:  00053583          ld              a1,0(a0)
0x0000000080000010:  0040006f          j               4               # 0x80000014

以下のようなTCGに変換されることが分かる。qemu_ld_i64というのがポイントらしい。

 mov_i64 tmp2,x10/a0
 qemu_ld_i64 tmp3,tmp2,leq,3
 mov_i64 x11/a1,tmp3

これはgen_ldst_i64によって生成されていることが分かる。

  • qemu/tcg/tcg-op.c
void tcg_gen_qemu_ld_i64(TCGv_i64 val, TCGv addr, TCGArg idx, MemOp memop)
{
    MemOp orig_memop;
    uint16_t info;

...
    addr = plugin_prep_mem_callbacks(addr);
    gen_ldst_i64(INDEX_op_qemu_ld_i64, val, addr, memop, idx);
    plugin_gen_mem_callbacks(addr, info);
...

qemu_ld_i64がどのように変換されるのかを見てみる。

  • qemu/tcg/i386/tcg-target.inc.c
static inline void tcg_out_op(TCGContext *s, TCGOpcode opc,
                              const TCGArg *args, const int *const_args)
{
    TCGArg a0, a1, a2;
    int c, const_a2, vexop, rexw = 0;

...
    switch (opc) {
    case INDEX_op_exit_tb:
        /* Reuse the zeroing that exists for goto_ptr.  */
        if (a0 == 0) {
...
    case INDEX_op_qemu_ld_i64:
        tcg_out_qemu_ld(s, args, 1);
        break;
...
  • qemu/tcg/i386/tcg-target.inc.c
/* XXX: qemu_ld and qemu_st could be modified to clobber only EDX and
   EAX. It will be useful once fixed registers globals are less
   common. */
static void tcg_out_qemu_ld(TCGContext *s, const TCGArg *args, bool is64)
{
    TCGReg datalo, datahi, addrlo;
    TCGReg addrhi __attribute__((unused));
    TCGMemOpIdx oi;
    MemOp opc;
#if defined(CONFIG_SOFTMMU)
    int mem_index;
    tcg_insn_unit *label_ptr[2];
#endif
...
#if defined(CONFIG_SOFTMMU)
    mem_index = get_mmuidx(oi);

    tcg_out_tlb_load(s, addrlo, addrhi, mem_index, opc,
                     label_ptr, offsetof(CPUTLBEntry, addr_read));

    /* TLB Hit.  */
    tcg_out_qemu_ld_direct(s, datalo, datahi, TCG_REG_L1, -1, 0, 0, is64, opc);

    /* Record the current context of a load into ldst label */
    add_qemu_ldst_label(s, true, is64, oi, datalo, datahi, addrlo, addrhi,
                        s->code_ptr, label_ptr);
#else
...

どうもTLBの設定の後で、ロード命令が生成されるらしい。 この辺りのTCGの構成が良く分からなかった。うーん、何をしているんだ?

static inline void tcg_out_tlb_load(TCGContext *s, TCGReg addrlo, TCGReg addrhi,
                                    int mem_index, MemOp opc,
                                    tcg_insn_unit **label_ptr, int which)
{
...

そして、tcg_out_qemu_ld_directによりメモリアドレスのロードが行われるらしい。

static void tcg_out_qemu_ld_direct(TCGContext *s, TCGReg datalo, TCGReg datahi,
                                   TCGReg base, int index, intptr_t ofs,
                                   int seg, bool is64, MemOp memop)
...
    case MO_Q:
        if (TCG_TARGET_REG_BITS == 64) {
            tcg_out_modrm_sib_offset(s, movop + P_REXW + seg, datalo,
                                     base, index, 0, ofs);
            if (bswap) {
                tcg_out_bswap64(s, datalo);
            }

どうも良く分からないので、生成されるx86アセンブリコードを読み解いていくことにしてみる。

OUT: [size=152]
0x7f4b44000500:  8b 5d f0                 movl     -0x10(%rbp), %ebx
0x7f4b44000503:  85 db                    testl    %ebx, %ebx
0x7f4b44000505:  0f 8c 55 00 00 00        jl       0x7f4b44000560

// x10の値をロードする。
//     tcg_out_mov(s, tlbtype, r0, addrlo);
0x7f4b4400050b:  48 8b 5d 50              movq     0x50(%rbp), %rbx

// rdi = reg[x10]
// rdi = reg[x10] >> 7
//     tcg_out_shifti(s, SHIFT_SHR + tlbrexw, r0,
//                   TARGET_PAGE_BITS - CPU_TLB_ENTRY_BITS);
0x7f4b4400050f:  48 8b fb                 movq     %rbx, %rdi
0x7f4b44000512:  48 c1 ef 07              shrq     $7, %rdi

//     tcg_out_modrm_offset(s, OPC_AND_GvEv + trexw, r0, TCG_AREG0,
//                         TLB_MASK_TABLE_OFS(mem_index) +
//                         offsetof(CPUTLBDescFast, mask));
0x7f4b44000516:  48 23 7d e0              andq     -0x20(%rbp), %rdi

//    tcg_out_modrm_offset(s, OPC_ADD_GvEv + hrexw, r0, TCG_AREG0,
//                         TLB_MASK_TABLE_OFS(mem_index) +
//                         offsetof(CPUTLBDescFast, table));
0x7f4b4400051a:  48 03 7d e8              addq     -0x18(%rbp), %rdi

// tcg_out_modrm_offset(s, OPC_LEA + trexw, r1, addrlo, s_mask - a_mask);
0x7f4b4400051e:  48 8d 73 07              leaq     7(%rbx), %rsi

// tgen_arithi(s, ARITH_AND + trexw, r1, tlb_mask, 0);
0x7f4b44000522:  48 81 e6 00 f0 ff ff     andq     $0xfffff000, %rsi

    /* cmp 0(r0), r1 */
// tcg_out_modrm_offset(s, OPC_CMP_GvEv + trexw, r1, r0, which);
0x7f4b44000529:  48 3b 37                 cmpq     (%rdi), %rsi

    /* Prepare for both the fast path add of the tlb addend, and the slow
       path function argument setup.  */
//    tcg_out_mov(s, ttype, r1, addrlo);
0x7f4b4400052c:  48 8b f3                 movq     %rbx, %rsi

    /* jne slow_path */
    // tcg_out_opc(s, OPC_JCC_long + JCC_JNE, 0, 0, 0);
0x7f4b4400052f:  0f 85 37 00 00 00        jne      0x7f4b4400056c

//    tcg_out_modrm_offset(s, OPC_ADD_GvEv + hrexw, r1, r0,
//                         offsetof(CPUTLBEntry, addend));
0x7f4b44000535:  48 03 77 18              addq     0x18(%rdi), %rsi
// 実際のメモリロードは以下。
0x7f4b44000539:  48 8b 1e                 movq     (%rsi), %rbx
// ロード結果をx11に格納する。
0x7f4b4400053c:  48 89 5d 58              movq     %rbx, 0x58(%rbp)
0x7f4b44000540:  66 66 90                 nop
// J命令にジャンプ。
0x7f4b44000543:  e9 00 00 00 00           jmp      0x7f4b44000548
0x7f4b44000548:  bb 14 00 00 80           movl     $0x80000014, %ebx
0x7f4b4400054d:  48 89 9d 00 02 00 00     movq     %rbx, 0x200(%rbp)
0x7f4b44000554:  48 8d 05 e5 fe ff ff     leaq     -0x11b(%rip), %rax
0x7f4b4400055b:  e9 b8 fa ff ff           jmp      0x7f4b44000018
0x7f4b44000560:  48 8d 05 dc fe ff ff     leaq     -0x124(%rip), %rax
0x7f4b44000567:  e9 ac fa ff ff           jmp      0x7f4b44000018
// slow pathの場合はここに飛ぶ。
0x7f4b4400056c:  48 8b fd                 movq     %rbp, %rdi
0x7f4b4400056f:  ba 33 00 00 00           movl     $0x33, %edx
0x7f4b44000574:  48 8d 0d c1 ff ff ff     leaq     -0x3f(%rip), %rcx
0x7f4b4400057b:  ff 15 0f 00 00 00        callq    *0xf(%rip)
0x7f4b44000581:  48 8b d8                 movq     %rax, %rbx
0x7f4b44000584:  e9 b3 ff ff ff           jmp      0x7f4b4400053c
0x7f7580000589:  90                       nop
0x7f758000058a:  90                       nop
0x7f758000058b:  90                       nop
0x7f758000058c:  90                       nop
0x7f758000058d:  90                       nop
0x7f758000058e:  90                       nop
0x7f758000058f:  90                       nop
0x7f7580000590:  .quad  0x000000000048e5ae

slow_pathはTLBに当該アドレスがヒットしない場合に呼び出されるのだが、この時の仕組みはどのようになっているのかというと、

  1. TLBにヒットしない場合、jne 0x7f4b4400056cが呼び出されることによりslow_pathを用意するルーチンに飛ぶ。
  2. 第1引数: movq %rbp, %rdi : rbpつまりenvが設定される。
  3. 第2引数: rsi : TLBエントリの場所が設定される。
  4. 第3引数: rdx : 0x33、これは引数としてはsizeだが、何のサイズなのか良く分からない。
  5. 第4引数: rcx : retaddrこれはTLB処理を行った後にどこに戻ってくるのかを記憶するためのものらしい。現在の場所から-0x3fなので0x7f4b4400053cとなり、ちょうどロード命令の場所だ。
  6. callq *0xf(%rip)により現在の場所よりも0xf離れた場所に格納されたアドレス、つまり0x000000000048e5aeにジャンプする。

この0x000000000048e5aeは、helper_le_ldq_mmuであり、helper_le_ldq_mmu()が呼び出されることが分かる。

000000000048e5ae <helper_le_ldq_mmu>:
  48e5ae:       55                      push   %rbp
  48e5af:       48 89 e5                mov    %rsp,%rbp
  48e5b2:       48 83 ec 20             sub    $0x20,%rsp
  48e5b6:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
  48e5ba:       48 89 75 f0             mov    %rsi,-0x10(%rbp)
  48e5be:       89 55 ec                mov    %edx,-0x14(%rbp)
  48e5c1:       48 89 4d e0             mov    %rcx,-0x20(%rbp)
  48e5c5:       48 8b 4d e0             mov    -0x20(%rbp),%rcx
  48e5c9:       8b 55 ec                mov    -0x14(%rbp),%edx
  48e5cc:       48 8b 75 f0             mov    -0x10(%rbp),%rsi
  48e5d0:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  48e5d4:       48 83 ec 08             sub    $0x8,%rsp
  48e5d8:       68 ae e5 48 00          pushq  $0x48e5ae
  48e5dd:       41 b9 00 00 00 00       mov    $0x0,%r9d
  48e5e3:       41 b8 03 00 00 00       mov    $0x3,%r8d
  48e5e9:       48 89 c7                mov    %rax,%rdi
  48e5ec:       e8 c0 f7 ff ff          callq  48ddb1 <load_helper>
  48e5f1:       48 83 c4 10             add    $0x10,%rsp
  48e5f5:       c9                      leaveq
  48e5f6:       c3                      retq
  • qemu/accel/tcg/cputlb.c
uint64_t helper_be_ldq_mmu(CPUArchState *env, target_ulong addr,
                           TCGMemOpIdx oi, uintptr_t retaddr)
{
    return load_helper(env, addr, oi, retaddr, MO_BEQ, false,
                       helper_be_ldq_mmu);
}

ここはもうホストマシンのコードであり、これがslow_path()と呼ばれるゆえんである。ここは普通にC言語で記述されたコードが実行され、TLB Fillが行われる。

最終的に実行されるのはriscv_cpu_tlb_fill()だ。これで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;
...

--d mmuオプションを追加すると、TLBがFillされるログが表示される。

riscv_cpu_tlb_fill ad 80000000 rw 2 mmu_idx 3
riscv_cpu_tlb_fill address=80000000 ret 0 physical 0000000080000000 prot 7