0%

qemu tcg跳转的处理

以beq为例:

1
2
3
4
5
6
7
8
9
/* qemu/target/riscv/insn_trans/trans_rvi.c.inc */
trans_beq
-> gen_branch
-> gen_new_lable
-> tcg_gen_brcond_tl
-> gen_goto_tb
-> gen_set_label
-> gen_goto_tb
-> ctx->base.is_jmp = DISAS_NORETURN;

如上的逻辑是生成中间码的,生成的代码还要执行,我们不用关注host上执行的细节,只要
中间码这边的逻辑通就好。那就比较好理解上面的代码,结合tcg/README里的介绍。

gen_new_lable是创建了一个lable,brcond_t是brcond_i32/i64 t0, t1, cond, label,根据
t0/t1的计算决定是否要跳到lable处执行,gen_set_lable是set_label $label,相当于在
当前位置设置lable。所以上面的代码生成的代码伪代码表示大概是:

1
2
3
4
5
6
7
8
9
10
lable l;

if (t0 cond t1) {
goto l;
}

goto_tb(pc顺移);

l:
goto_tb(计算新pc);

其中gen_goto_tb的逻辑是:

1
2
3
4
5
6
7
8
9
gen_goto_tb
if (translator_use_goto_tb(&ctx->base, dest)) {
tcg_gen_goto_tb(n);
tcg_gen_movi_tl(cpu_pc, dest);
tcg_gen_exit_tb(ctx->base.tb, n);
} else {
tcg_gen_movi_tl(cpu_pc, dest);
tcg_gen_lookup_and_goto_ptr();
}

可见这里有两种实现方式:如果goto_tb在tcg后端有实现就用goto_tb来跳转,否则就用
goto_ptr来实现。goto_ptr的方式相对简单,先设置跳转的PC,然后会调用到
lookup_tb_ptr(在accel/tcg/cpu-exec.c),找对应的tb执行,如果没有找见就退出当前tb。

goto_tb的方式比较绕一点,我们那riscv的后端实现具体看下。tcg_gen_goto_tb对应的
中间码是INDEX_op_goto_tb,riscv的后端实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* tcg/riscv/tcg-target.c.inc */
tcg_out_op
-> tcg_out_ld(s, TCG_TYPE_PTR, TCG_REG_TMP0, TCG_REG_ZERO,
(uintptr_t)(s->tb_jmp_target_addr + a0));
-> tcg_out_opc_imm(s, OPC_JALR, TCG_REG_ZERO, TCG_REG_TMP0, 0);
-> set_jmp_reset_offset(s, a0);
/*
* 注意a0就是tcg_gen_goto_tb的入参,就是n。注意,这个是理解goto_tb的关键,
* tcg_out_opc_imm在当前的tb里产生一条指令,这个地方把这个指令在tb里的位置
* 写在了tb_jmp_target_addr[n]这个地方。这个动作为后面链接下一个tb留出了
* 一个指令的位置。
*/
-> s->tb_jmp_reset_offset[which] = tcg_current_code_size(s);

riscv exit_tb的后端实现:

1
2
3
4
5
6
7
tcg_out_op
if (a0 == 0) {
tcg_out_call_int(s, tcg_code_gen_epilogue, true);
} else {
tcg_out_movi(s, TCG_TYPE_PTR, TCG_REG_A0, a0);
tcg_out_call_int(s, tb_ret_addr, true);
}

注意这里的a0和n相关,n这个变量从这里被传入后端执行,然后从exit_tb里带出来,给到
下个tb。我们回到主循环。

1
2
3
4
5
6
cpu_exec
-> cpu_loop_exec_tb(cpu, tb, &last_tb, &tb_exit);
-> tb_add_jump(last_tb, tb_exit, tb);
-> tb_set_jmp_target(tb, n, (uintptr_t)tb_next->tc.ptr);
-> uintptr_t offset = tb->jmp_target_arg[n];
-> tb_target_set_jmp_target(tc_ptr, jmp_rx, jmp_rw, addr);

如上,在下一次tb翻译执行循环里会把新tb里指令的地址直接覆盖上次tb里保留的位置。
所以,使用go_tb,第一次执行的时候会退出tb,执行下一个tb,用新tb指令地址覆盖之前tb
里的跳转预留位置,当再次执行前一个tb时,会直接跳转到新tb,就不会退出当前tb。