FPGA開発日記

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

QEMUのTCG(Tiny Code Generator)を読み解く(7. QEMUの分岐命令の取り扱い)

QEMUの調査続き。次は分岐命令をどのように処理しているのか見ていこう。

例えばBEQ命令がどのようにx86に処理されるのかを見てみよう。

  • qemu/target/riscv/insn_trans/trans_rvi.inc.c
static bool trans_beq(DisasContext *ctx, arg_beq *a)
{
    return gen_branch(ctx, a, TCG_COND_EQ);
}

gen_branch()の実装はqemu/target/riscv/insn_trans/trans_rvi.inc.cに格納されている。これはすべての分岐命令に使用されるテンプレートのようだ。

  • qemu/target/riscv/insn_trans/trans_rvi.inc.c
static bool gen_branch(DisasContext *ctx, arg_b *a, TCGCond cond)
{
    TCGLabel *l = gen_new_label();
    TCGv source1, source2;
    source1 = tcg_temp_new();
    source2 = tcg_temp_new();
    gen_get_gpr(source1, a->rs1);
    gen_get_gpr(source2, a->rs2);

    tcg_gen_brcond_tl(cond, source1, source2, l);
    gen_goto_tb(ctx, 1, ctx->pc_succ_insn);
    gen_set_label(l); /* branch taken */

    if (!has_ext(ctx, RVC) && ((ctx->base.pc_next + a->imm) & 0x3)) {
        /* misaligned */
        gen_exception_inst_addr_mis(ctx);
    } else {
        gen_goto_tb(ctx, 0, ctx->base.pc_next + a->imm);
    }
    ctx->base.is_jmp = DISAS_NORETURN;

    tcg_temp_free(source1);
    tcg_temp_free(source2);

    return true;
}

このコードが実際のBEQ命令挙動を示している。簡単に説明すると、

  • source1に新しいオペランドを確保する。gen_get_gpr(source1, a->rs1)によりsource1rs1の情報を格納する。
  • source2に新しいオペランドを確保する。gen_get_gpr(source2, a->rs2)によりsource2rs2の情報を格納する。

  • lはラベルを示している。これは分岐命令によりジャンプ先を制御するために使用する。

  • tcg_gen_brcond_tl(cond, source1, source2, l)が分岐そのものを示している。source1source2を比較し、condで示す条件が成立すればlが配置されている場所まで飛ぶというものだと思われる(あくまで、lはホストコード上のジャンプ先で、goto:みたいなものだととらえれば良い。
    • gen_set_label(l)によりlの場所が指定される。もし分岐が成立すればlまでジャンプするため、その1行上のgen_goto_tb(ctx, 1, ctx->pc_succ_insn)は実行が省略されることになる。
  • もし分岐命令が成立しなければ、gen_goto_tb(ctx, 1, ctx->pc_succ_insn);が実行され次のPCに移動される。
  • もし分岐命令が成立すれば、gen_goto_tb(ctx, 0, ctx->base.pc_next + a->imm);によりPCが設定されジャンプする。

このgen_goto_tb()だが、仕組みを解説するより生成されたコードを見る方が分かりやすい。

  • simple_start.S
_start:
    beq     x10, x11, label
    li      x12, 0x4000
label:
    li      x13, 0x4001

    ret

これはどういうTCGになるかというと、

----------------
IN:
Priv: 3; Virt: 0
0x0000000080000000:  06400513          addi            a0,zero,100
0x0000000080000004:  06f00593          addi            a1,zero,111
0x0000000080000008:  00b50463          beq             a0,a1,8         # 0x80000010
OUT: [size=104]
0x7f13680003c0:  8b 5d f0                 movl     -0x10(%rbp), %ebx
0x7f13680003c3:  85 db                    testl    %ebx, %ebx
0x7f13680003c5:  0f 8c 51 00 00 00        jl       0x7f136800041c
0x7f13680003cb:  48 8b 5d 50              movq     0x50(%rbp), %rbx
0x7f13680003cf:  4c 8b 65 58              movq     0x58(%rbp), %r12
0x7f13680003d3:  49 3b dc                 cmpq     %r12, %rbx
0x7f13680003d6:  0f 84 20 00 00 00        je       0x7f13680003fc
0x7f13680003dc:  66 66 90                 nop
0x7f13680003df:  e9 00 00 00 00           jmp      0x7f13680003e4
0x7f13680003e4:  bb 04 00 00 80           movl     $0x80000004, %ebx
0x7f13680003e9:  48 89 9d 00 02 00 00     movq     %rbx, 0x200(%rbp)
0x7f13680003f0:  48 8d 05 0a ff ff ff     leaq     -0xf6(%rip), %rax
0x7f13680003f7:  e9 1c fc ff ff           jmp      0x7f1368000018
0x7f13680003fc:  66 66 90                 nop
0x7f13680003ff:  e9 00 00 00 00           jmp      0x7f1368000404
0x7f1368000404:  bb 08 00 00 80           movl     $0x80000008, %ebx
0x7f1368000409:  48 89 9d 00 02 00 00     movq     %rbx, 0x200(%rbp)
0x7f1368000410:  48 8d 05 e9 fe ff ff     leaq     -0x117(%rip), %rax
0x7f1368000417:  e9 fc fb ff ff           jmp      0x7f1368000018
0x7f136800041c:  48 8d 05 e0 fe ff ff     leaq     -0x120(%rip), %rax
0x7f1368000423:  e9 f0 fb ff ff           jmp      0x7f1368000018

例外処理などが加わっているため比較的長いコードになっているが、比較処理を行命令はcmpq %r12, %rbxje 0x7f13680003fcだと思う。比較が成立した場合には0x7f13680003fcにジャンプするが、ジャンプ先の命令は

0x7f13680003fc:  66 66 90                 nop
0x7f13680003ff:  e9 00 00 00 00           jmp      0x7f1368000404
0x7f1368000404:  bb 08 00 00 80           movl     $0x80000008, %ebx
0x7f1368000409:  48 89 9d 00 02 00 00     movq     %rbx, 0x200(%rbp)
0x7f1368000410:  48 8d 05 e9 fe ff ff     leaq     -0x117(%rip), %rax
0x7f1368000417:  e9 fc fb ff ff           jmp      0x7f1368000018

ジャンプ先アドレスとして%ebxに0x80000008を設定して、さらに戻り値を設定するために%rax%rip-0x117を設定して0x7f1368000018にジャンプしている。

比較が成立しない場合には、以下のコードシーケンスを取る。

0x7f13680003dc:  66 66 90                 nop
0x7f13680003df:  e9 00 00 00 00           jmp      0x7f13680003e4
0x7f13680003e4:  bb 04 00 00 80           movl     $0x80000004, %ebx
0x7f13680003e9:  48 89 9d 00 02 00 00     movq     %rbx, 0x200(%rbp)
0x7f13680003f0:  48 8d 05 0a ff ff ff     leaq     -0xf6(%rip), %rax
0x7f13680003f7:  e9 1c fc ff ff           jmp      0x7f1368000018

こちらの場合は%ebxに0x80000004を指定して、さらに戻り値を設定するために%rax%rip-0xf6を設定する。最後に0x7f1368000018にジャンプしているのは、分岐が成立しているケースと一緒である。

この0x7f1368000018はいったいどこなのかというと、実はエピローグの先頭である。つまり分岐が成立しようがしまいが、エピローグが実行されQEMUに処理が返されるということになる。QEMUの実行はこのようにして分岐命令の度に途切れるため、この分岐命令までの一連の処理をブロックと呼称し、このブロックをつなげながら実行することをBlock Chainingと呼んでいる。

QEMUに処理が返ってくると、設定されたPCのアドレスに基づいて次のブロックのJITコンパイルが始まり、処理が継続されるという仕組みだ。