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に詳細な表があった。
先頭のオペコードが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つの引数が設定される。これがenv
とtb_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.c
のcpu_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()
の関数に戻ってきた。これにより先ほどの変換されたコードが実行されるという訳だ。