FPGA開発日記

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

QEMUのTCG(Tiny Code Generator)を読み解く(4. QEMUがカーネルを呼ぶ際に実行するPrologueコードを読み解く)

TCGの続き。続いて、TCGを使ってどのようにRISC-Vのコードをx86上で実行しているのかをチェックする。このためには、まずは関数のプロローグを調べていく必要があるだろう。

デバッグ情報を出力しながらQEMUを実行する。

$ qemu-system-riscv64 --machine virt --d in_asm,op,op_opt,op_ind,out_asm --nographic --trace enable=myriscvx_trap \
    --kernel ./simple_asm 2>&1 | tee qemu.riscv64.log
PROLOGUE: [size=45]
0x7f88b4000000:  55                       pushq    %rbp
0x7f88b4000001:  53                       pushq    %rbx
0x7f88b4000002:  41 54                    pushq    %r12
0x7f88b4000004:  41 55                    pushq    %r13
0x7f88b4000006:  41 56                    pushq    %r14
0x7f88b4000008:  41 57                    pushq    %r15
0x7f88b400000a:  48 8b ef                 movq     %rdi, %rbp
0x7f88b400000d:  48 81 c4 78 fb ff ff     addq     $-0x488, %rsp
0x7f88b4000014:  ff e6                    jmpq     *%rsi
0x7f88b4000016:  33 c0                    xorl     %eax, %eax
0x7f88b4000018:  48 81 c4 88 04 00 00     addq     $0x488, %rsp
0x7f88b400001f:  c5 f8 77                 vzeroupper
0x7f88b4000022:  41 5f                    popq     %r15
0x7f88b4000024:  41 5e                    popq     %r14
0x7f88b4000026:  41 5d                    popq     %r13
0x7f88b4000028:  41 5c                    popq     %r12
0x7f88b400002a:  5b                       popq     %rbx
0x7f88b400002b:  5d                       popq     %rbp
0x7f88b400002c:  c3                       retq

こんな感じのプロローグコードが実行されるのだが、これはどのように生成されているのかというと、qemu/tcg/i386/tcg-target.inc.cを見ればわかる。

/* Generate global QEMU prologue and epilogue code */
static void tcg_target_qemu_prologue(TCGContext *s)
{
...

まず、必要なレジスタをスタックに退避する。tcg_target_callee_save_regsなレジスタを退避するというコードになっている。tcg_target_callee_save_regsは、

static const int tcg_target_callee_save_regs[] = {
#if TCG_TARGET_REG_BITS == 64
    TCG_REG_RBP,
    TCG_REG_RBX,
#if defined(_WIN64)
    TCG_REG_RDI,
    TCG_REG_RSI,
#endif
    TCG_REG_R12,
    TCG_REG_R13,
    TCG_REG_R14, /* Currently used for the global env. */
    TCG_REG_R15,
#else
    TCG_REG_EBP, /* Currently used for the global env. */
    TCG_REG_EBX,
    TCG_REG_ESI,
    TCG_REG_EDI,
#endif
};

であり、従って、RBP, RBX, R12, R13, R14, R15を退避する命令が生成されている。

  • qemu/tcg/i386/tcg-target.inc.c
    /* Reserve some stack space, also for TCG temps.  */
    stack_addend = FRAME_SIZE - PUSH_SIZE;
    tcg_set_frame(s, TCG_REG_CALL_STACK, TCG_STATIC_CALL_ARGS_SIZE,
                  CPU_TEMP_BUF_NLONGS * sizeof(long));

    /* Save all callee saved registers.  */
    for (i = 0; i < ARRAY_SIZE(tcg_target_callee_save_regs); i++) {
        tcg_out_push(s, tcg_target_callee_save_regs[i]);
    }

次にレジスタの移動を行う。AREG0というのはx86のs0レジスタなのだが、これをtcg_target_call_iarg_regs[0]、つまり整数の引数レジスタの1番目のレジスタを代入する。これはLinuxのシステムコールの第1引数でもあるらしい。ここで、

  • tcg_target_call_ireg_regs[0] = TCG_REG_RDI
  • AREG0 = TCG_REG_EBPでこれはTCG_REG_RBPに相当する。

もうひとつはスタックの更新。TCG_REG_ESPつまりTCG_REG_RSPのスタックを更新する。

なので、

  • qemu/tcg/i386/tcg-target.inc.c
    tcg_out_mov(s, TCG_TYPE_PTR, TCG_AREG0, tcg_target_call_iarg_regs[0]);
    tcg_out_addi(s, TCG_REG_ESP, -stack_addend);

最後にジャンプ命令の生成だ。これはjmpq命令という命令を生成しているのだが、これを読み解くカギが、Mod R/Mという修飾モードらしく、これは先頭の1バイトに特定の修飾バイトを追加することで命令を拡張することができるモードらしい。

    /* jmp *tb.  */
    tcg_out_modrm(s, OPC_GRP5, EXT5_JMPN_Ev, tcg_target_call_iarg_regs[1]);

SDMに詳細な表があった。

f:id:msyksphinz:20200809231230p:plain

先頭のオペコードが0xFFで、次のModR/Mのバイトが0x04となるのでJMPN Ev命令としてデコードされるらしい。

これにより2番目の引数レジスタtcg_target_call_iarg_regs[1]に格納されているアドレスにジャンプするらしい。ちなみにtcg_target_call_iarg_regs[1]はRSIレジスタなのでそれが指定される。そうしてJMPN命令が生成されるという訳だ。ちなみにこのRSIレジスタはLinuxシステムコールの第2引数として使用されるらしい。

という訳で、RDIレジスタとRSIレジスタに何かが設定されて関数が呼び出されるらしいのだが、これは何だろう?これは実際にTCGが実行されるコードを見ると分かる。

  • qemu/include/tcg/tcg.h
#ifdef HAVE_TCG_QEMU_TB_EXEC
uintptr_t tcg_qemu_tb_exec(CPUArchState *env, uint8_t *tb_ptr);
#else
# define tcg_qemu_tb_exec(env, tb_ptr) \
    ((uintptr_t (*)(void *, void *))tcg_ctx->code_gen_prologue)(env, tb_ptr)
#endif

ここではcode_gen_prologueに登録した関数に対して2つの引数が設定される。これがenvtb_ptrである。つまり、

  • RDIレジスタ:envへのポインタが設定される。
  • RSIレジスタ:tb_ptrつまり、変換されたホストコードへの先頭ポインタが渡される。つまり、最初のjmpq命令で、変換コードの最初のブロックにジャンプするということがここで判明する。

そしてジャンプにより特定のカーネルが実行されると、それを終了するためにスタックポインタの戻しとエピローグ処理が行われる。

    /*
     * Return path for goto_ptr. Set return value to 0, a-la exit_tb,
     * and fall through to the rest of the epilogue.
     */
    s->code_gen_epilogue = s->code_ptr;
    tcg_out_movi(s, TCG_TYPE_REG, TCG_REG_EAX, 0);

    /* TB epilogue */
    tb_ret_addr = s->code_ptr;

    tcg_out_addi(s, TCG_REG_CALL_STACK, stack_addend);

    if (have_avx2) {
        tcg_out_vex_opc(s, OPC_VZEROUPPER, 0, 0, 0, 0);
    }
    for (i = ARRAY_SIZE(tcg_target_callee_save_regs) - 1; i >= 0; i--) {
        tcg_out_pop(s, tcg_target_callee_save_regs[i]);
    }
    tcg_out_opc(s, OPC_RET, 0, 0, 0);

これらの処理を並べると、最終的に以下のような命令が生成されるという訳だ。

PROLOGUE: [size=45]
0x7f13f8000000:  55                       pushq    %rbp
0x7f13f8000001:  53                       pushq    %rbx
0x7f13f8000002:  41 54                    pushq    %r12
0x7f13f8000004:  41 55                    pushq    %r13
0x7f13f8000006:  41 56                    pushq    %r14
0x7f13f8000008:  41 57                    pushq    %r15
0x7f13f800000a:  48 8b ef                 movq     %rdi, %rbp
0x7f13f800000d:  48 81 c4 78 fb ff ff     addq     $-0x488, %rsp
0x7f13f8000014:  ff e6                    jmpq     *%rsi
0x7f13f8000016:  33 c0                    xorl     %eax, %eax
0x7f13f8000018:  48 81 c4 88 04 00 00     addq     $0x488, %rsp
0x7f13f800001f:  c5 f8 77                 vzeroupper
0x7f13f8000022:  41 5f                    popq     %r15
0x7f13f8000024:  41 5e                    popq     %r14
0x7f13f8000026:  41 5d                    popq     %r13
0x7f13f8000028:  41 5c                    popq     %r12
0x7f13f800002a:  5b                       popq     %rbx
0x7f13f800002b:  5d                       popq     %rbp
0x7f13f800002c:  c3                       retq

ではここで変換されたホストの機械語がどのようにして実行されるのかをトレースしていく。まずはcpu-exec.ccpu_tb_exec()から追いかけていこう。

  • qemu/accel/tcg/cpu-exec.c
/* Execute a TB, and fix up the CPU state afterwards if necessary */
static inline tcg_target_ulong cpu_tb_exec(CPUState *cpu, TranslationBlock *itb)
{
    CPUArchState *env = cpu->env_ptr;
    uintptr_t ret;
...

ここでは、変換されたホストの機械語に対してtcg_qemu_tb_exec(env, tb_ptr)が実行される。

  • qemu/include/tcg/tcg.h
#ifdef HAVE_TCG_QEMU_TB_EXEC
uintptr_t tcg_qemu_tb_exec(CPUArchState *env, uint8_t *tb_ptr);
#else
# define tcg_qemu_tb_exec(env, tb_ptr) \
    ((uintptr_t (*)(void *, void *))tcg_ctx->code_gen_prologue)(env, tb_ptr)
#endif

先ほどのcode_gen_prologue()の関数に戻ってきた。これにより先ほどの変換されたコードが実行されるという訳だ。