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)
によりsource1
にrs1
の情報を格納する。source2
に新しいオペランドを確保する。gen_get_gpr(source2, a->rs2)
によりsource2
にrs2
の情報を格納する。l
はラベルを示している。これは分岐命令によりジャンプ先を制御するために使用する。tcg_gen_brcond_tl(cond, source1, source2, l)
が分岐そのものを示している。source1
とsource2
を比較し、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, %rbx
とje 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コンパイルが始まり、処理が継続されるという仕組みだ。