0%

qemu tcg取指令逻辑分析

基本逻辑

qemu的基本的执行模型是翻译guest代码到tb和在host上执行tb两个动作交替进行,所以vCPU
取指令这个动作可能发生在很多地方。它可以发生在翻译guest代码这个步骤,这时guest
指令还没有翻译到tb里;它也可以发生在qemu搜tb哈希表这个步骤,qemu可能把相关的guest
代码已经翻译到tb里了,所以也可以从这里“取指令”; qemu可以把tb chain起来,从一个tb
直接跳到一个tb,我们也可以把这个tb跳转理解成一种取指令的方式。

翻译guest代码要通过guest VA得到guest PA,进而的到host VA,然后就可以得到host VA
上的guest指令,其中guest VA到guest PA可能TLB hit,也可能要经过page table walk,
还有可能触发异常,借助软件把页补齐,guest PA和hostVA通常就是一个固定的偏移。
指令翻译是一条一条进行的,但是终归在一个page内,所以在开始翻译一段指令之前可以
先计算得到对应的host VA,这样后面的翻译,只要还是在相同page内,host VA就不变。

qemu搜tb哈希表时,要先做地址翻译得到guest VA对应的guest PA,然后用guest PA以及
其它参数作为key去tb哈希表里搜索。

chain tb之间来回跳转,但是qemu规定chain tb只能在一个page的范围内,这样一旦guest
代码在不同的page里,qemu取指令就不能直接跳过去,它必须要么做guest代码翻译要么做
tb哈希表搜索,而这两者都做guest VA到guest PA的地址翻译。

那么我们能否去掉chain tb只能在一页内这个限制?相关的逻辑可以参考这里

代码分析

如下是qemu翻译执行时取指令的代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
tb_gen_code
/*
* phys_pc是guest PA,pc是guest VA,host_pc是host VA。在tb翻译开始时,先
* 得到各种地址。
*/
+-> phys_pc = get_page_addr_code_hostp(env, pc, &host_pc);
/*
* 先看TLB是否命中,TLB不命中会触发page table walk,tlb_fill里的page
* table walk失败后会直接触发异常。
*
* TLB相关的分析可以参考[这里](todo)。
*/
+-> probe_access_internal

+-> gen_intermediate_code(cpu, tb, max_insns, pc, host_pc);
+-> translator_loop(cs, tb, max_insns, pc, host_pc, &riscv_tr_ops, &ctx.base);

+-> db->host_addr[0] = host_pc;
/* 反复执行单条指令的翻译 */
+-> riscv_tr_translate_insn(DisasContextBase *dcbase, CPUState *cpu);
/* 得到当前指令的编码 */
+-> opcode16 = translator_lduw(env, &ctx->base, ctx->base.pc_next);
/*
* 这里会直接用上面db->host_addr[0]里的值的到host VA,但是也可能
* guest的指令会跨过页边界,这个时候就有可能要再走一下get_page_addr_code_hostp
* 的流程,得到下一页的host VA。
*/
+-> void *p = translator_access(env, db, pc, sizeof(ret));
/* 如果可以得到host VA,那么直接memcpy就可以得到指令编码 */
+-> lduw_p(p);
/* 不明白什么逻辑会走到这里? */
+-> cpu_lduw_code(env, pc);
+-> load_helper

如下tb搜索时的逻辑:

1
2
3
4
tb_lookup
+-> tb_htable_lookup(cpu, pc, cs_base, flags, cflags);
+->phys_pc = get_page_addr_code(desc.env, pc);
+-> get_page_addr_code_hostp(env, addr, NULL);