0x10 相关代码及调用栈
0x11 QEMU 软件虚拟化核心思想
早在 QEMU 0.10.0 时代,TCG(Tiny Code Generator) 就已成为 QEMU 的翻译引擎。TCG 起源于 C 编译器后端,后来被简化为 QEMU 的动态代码生成器。实际上,TCG 和一个真实的编译器后端一样,负责分析、优化已经生成 Host 代码。
QEMU 软件虚拟化采用的思路是二进制指令翻译技术,这里 Target 表示我们要运行目标架构代码,而 Host 表示我们拥有的真实 CPU 架构,通常是 x86。QEMU 通过 TCG,提取 Target(实际上就是我们在 QEMU 所说的 Guest 虚拟机),将其翻译成 TCG 中间代码,最后再将中间代码翻译成 Host(搭载 QEMU 的真实物理机平台)架构指令。
动态翻译的基本思想就是把每一条 Target 指令切分成为若干条微操作,每条微操作由一段简单的 C 代码来实现,运行时通过一个动态代码生成器(TCG)把这些微操作组合成一个函数,最后执行这个函数,就相当于执行了一条 Target 指令。
0x12 整体代码流程
整体流程
target instruction -> micro-op -> tcg-> host instruction
函数调用栈
linux-user/main.c -> main ->target_xxx/cpu.h -> cpu_init -> xxx_cpu_realizefn -> …
代码结构如下
相应的函数主要位于根目录下的cpu-exec.c
cpus.c
以及各个架构独立的 target-xxx/cpu.c
cpu-exec.c
代码中的cpu_exec()
函数作为 TCG 执行主函数,主要负责中断异常处理、找到代码翻译块、执行
for(;;) {
process interruptrequest;
tb_find_fast();
tcg_qemu_tb_exec(tc_ptr);
}
TCG 在翻译过程中,会将翻译好的代码缓存起来。因此在翻译 TB
的过程中,首先会判断 pc
对应的 TB
是否存在缓存中。如果存在,则直接取出。如果不存在,则调用 tb_find_slow()
函数,进行翻译工作。
static inline TranslationBlock *tb_find_fast(CPUArchState *env)
{
CPUState *cpu = ENV_GET_CPU(env);
TranslationBlock *tb;
target_ulong cs_base, pc;
int flags;
/* we record a subset of the CPU state. It will
always be the same before a given translated block
is executed. */
cpu_get_tb_cpu_state(env, &pc, &cs_base, &flags);
tb = cpu->tb_jmp_cache[tb_jmp_cache_hash_func(pc)];
if (unlikely(!tb || tb->pc != pc || tb->cs_base != cs_base ||
tb->flags != flags)) {
tb = tb_find_slow(env, pc, cs_base, flags);
}
return tb;
}
整体翻译流程如下
这张图结合后面实际的源码,会有更好的效果。
0x20 指令翻译核心步骤
TCG 一些核心翻译函数
上述流程图是从 CPU 初始化到生成 TCG 中间代码的过程,部分代码与 Target 强相关,从如下 References 可以看出
0x21 节可以忽略,只是所做的额外分析,不影响 TCG 代码流程分析。
0x21 可以忽略的 pc
pc
实际上就是 env->pc
,env
是在哪里赋值的呢?经过检查,函数 tcg_exec_all()
的 1385 行对其进行了赋值
static void tcg_exec_all(void)
{
int r;
/* Account partial waits to QEMU_CLOCK_VIRTUAL. */
qemu_clock_warp(QEMU_CLOCK_VIRTUAL);
if (next_cpu == NULL) {
next_cpu = first_cpu;
}
for (; next_cpu != NULL && !exit_request; next_cpu = CPU_NEXT(next_cpu)) {
CPUState *cpu = next_cpu;
CPUArchState *env = cpu->env_ptr; // 赋值
qemu_clock_enable(QEMU_CLOCK_VIRTUAL,
(cpu->singlestep_enabled & SSTEP_NOTIMER) == 0);
if (cpu_can_run(cpu)) {
r = tcg_cpu_exec(env);
if (r == EXCP_DEBUG) {
cpu_handle_guest_debug(cpu);
break;
}
} else if (cpu->stop || cpu->stopped) {
break;
}
}
exit_request = 0;
}
这里可以看到,每个 CPU 是用 tail queue 数据结构进行管理的。暂时没有发现这个 pc
到底是怎么得到的。
0x22 追踪 tb_find_slow 翻译
如果没有可供翻译的代码,就使用 tb_gen_code()
完成代码的翻译工作
not_found:
/* if no translated code available, then translate it now */
tb = tb_gen_code(cpu, pc, cs_base, flags, 0);
found:
/* Move the last found TB to the head of the list */
if (likely(*ptb1)) {
*ptb1 = tb->phys_hash_next;
tb->phys_hash_next = tcg_ctx.tb_ctx.tb_phys_hash[h];
tcg_ctx.tb_ctx.tb_phys_hash[h] = tb;
}
/* we add the TB in the virtual pc hash table */
cpu->tb_jmp_cache[tb_jmp_cache_hash_func(pc)] = tb;
return tb;
tb_gen_code()
定义在文件 translate-all.c
- 使用
tb_alloc()
分配一个新的翻译块 tb,用于记录将要翻译的 pc 等信息。如果翻译块太多或者生成的代码太多,则清空翻译缓存 TBs。 - 使用
cpu_gen_code()
翻译 tb,并将翻译好的代码存在一个缓冲区中(gen_code_buf
)
TranslationBlock *tb_gen_code(CPUState *cpu,
target_ulong pc, target_ulong cs_base,
int flags, int cflags)
{
CPUArchState *env = cpu->env_ptr;
TranslationBlock *tb; // 分配 TB 对象
tb_page_addr_t phys_pc, phys_page2;
target_ulong virt_page2;
int code_gen_size;
phys_pc = get_page_addr_code(env, pc);
tb = tb_alloc(pc); // 分配一个新的 TB,如果 TBs 太多,或者太多的生成代码,则清空
if (!tb) {
/* flush must be done */
tb_flush(env);
/* cannot fail at this point */
tb = tb_alloc(pc);
/* Don't forget to invalidate previous TB info. */
tcg_ctx.tb_ctx.tb_invalidated_flag = 1;
}
tb->tc_ptr = tcg_ctx.code_gen_ptr;
tb->cs_base = cs_base;
tb->flags = flags;
tb->cflags = cflags;
cpu_gen_code(env, tb, &code_gen_size);
tcg_ctx.code_gen_ptr = (void *)(((uintptr_t)tcg_ctx.code_gen_ptr +
code_gen_size + CODE_GEN_ALIGN - 1) & ~(CODE_GEN_ALIGN - 1));
/* check next page if needed */
virt_page2 = (pc + tb->size - 1) & TARGET_PAGE_MASK;
phys_page2 = -1;
if ((pc & TARGET_PAGE_MASK) != virt_page2) {
phys_page2 = get_page_addr_code(env, virt_page2);
}
tb_link_page(tb, phys_pc, phys_page2);
return tb;
}
继续分析 cpu_gen_code()
,如果首条指令是无效的,则函数返回非零值。虚拟 CPU 能够基于此触发一个异常。gen_code_size_ptr
包括了生成代码(host code)的长度。
0x23 指令翻译核心函数 cpu_gen_code
变量 s 作为 TCG 上下文
TCGContext
的定义(部分代码)
struct TCGContext {
uint8_t *pool_cur, *pool_end;
TCGPool *pool_first, *pool_current;
TCGLabel *labels;
int nb_labels;
TCGTemp *temps; /* globals first, temps after */
int nb_globals;
int nb_temps;
/* index of free temps, -1 if none */
int first_free_temp[TCG_TYPE_COUNT * 2];
/* goto_tb support */
uint8_t *code_buf;
unsigned long *tb_next;
uint16_t *tb_next_offset;
uint16_t *tb_jmp_offset; /* != NULL if USE_DIRECT_JUMP */
/* liveness analysis */
uint16_t *op_dead_args; /* for each operation, each bit tells if the
corresponding argument is dead */
/* tells in which temporary a given register is. It does not take
into account fixed registers */
int reg_to_temp[TCG_TARGET_NB_REGS];
TCGRegSet reserved_regs;
tcg_target_long current_frame_offset;
tcg_target_long frame_start;
tcg_target_long frame_end;
int frame_reg;
uint8_t *code_ptr;
TCGTemp static_temps[TCG_MAX_TEMPS];
TCGHelperInfo *helpers;
int nb_helpers;
int allocated_helpers;
int helpers_sorted;
};
在对一个翻译块进行二进制翻译时,TCG 需要维护一些信息完成动态翻译,即 TCGContext,TCG 上下文包含三类信息:内存池、标号和变量1。
- TCGPool:内存池,用于二进制转换期间的内存管理。为了提高性能,TCG 在初始化期间分配了较大的存储空间(32K)。TCG 需要内存时,直接从中获取,当内存池剩余空间不满足申请需求,再申请新内存。内存池之间通过链表维护。
- TCG 变量:临时、局部和全局变量。在 TCG 上下文中,所有变量存放在一个静态分配的数组中。不再使用的变量,使用链表链接。先从被释放链表中分配,后从静态数组中分配。
具体翻译代码如下
int cpu_gen_code(CPUArchState *env, TranslationBlock *tb, int *gen_code_size_ptr)
{
TCGContext *s = &tcg_ctx;
tcg_insn_unit *gen_code_buf;
int gen_code_size;
#ifdef CONFIG_PROFILER
int64_t ti;
#endif
#ifdef CONFIG_PROFILER
s->tb_count1++; /* includes aborted translations because of
exceptions */
ti = profile_getclock();
#endif
tcg_func_start(s);
gen_intermediate_code(env, tb);
trace_translate_block(tb, tb->pc, tb->tc_ptr);
/* generate machine code */
gen_code_buf = tb->tc_ptr;
tb->tb_next_offset[0] = 0xffff;
tb->tb_next_offset[1] = 0xffff;
s->tb_next_offset = tb->tb_next_offset;
#ifdef USE_DIRECT_JUMP
s->tb_jmp_offset = tb->tb_jmp_offset;
s->tb_next = NULL;
#else
s->tb_jmp_offset = NULL;
s->tb_next = tb->tb_next;
#endif
#ifdef CONFIG_PROFILER
s->tb_count++;
s->interm_time += profile_getclock() - ti;
s->code_time -= profile_getclock();
#endif
gen_code_size = tcg_gen_code(s, gen_code_buf);
*gen_code_size_ptr = gen_code_size;
#ifdef CONFIG_PROFILER
s->code_time += profile_getclock();
s->code_in_len += tb->size;
s->code_out_len += gen_code_size;
#endif
return 0;
}
目标指令翻译成 TCG 中间码
gen_intermediate_code()
函数定义在不同架构的 target-xxx/translate.c
文件中。该函数将 target 指令翻译成中间码。操作码和操作数分开存储。操作码存放在 gen_opc_buf
变量,操作数存放在 gen_opparam_buf
变量。翻译过程就是不断向上述两个缓冲区填充操作码和操作数。
TARGET 与平台有很强相关性,因此,上述函数在不同架构的文件代码中都会有单独定义,并且代码差距比较大。我们以摩托罗拉的 m68k 为例,看看 m68k 架构是怎么翻译成 TCG 中间代码的。
void gen_intermediate_code(CPUM68KState *env, TranslationBlock *tb)
{
gen_intermediate_code_internal(m68k_env_get_cpu(env), tb, false);
}
Guest code 翻译步骤都在函数 gen_intermediate_code_internal()
,将基本块(basic block, tb) 翻译成 TCG 中间代码(intermediate code)。
gen_intermediate_code_internal(M68kCPU *cpu, TranslationBlock *tb,
bool search_pc)
{
CPUState *cs = CPU(cpu); // CPU状态
CPUM68KState *env = &cpu->env; // 模拟器CPU状态
DisasContext dc1, *dc = &dc1; // 反汇编代码上下文
uint16_t *gen_opc_end; //
CPUBreakpoint *bp; //
int j, lj;
target_ulong pc_start;
int pc_offset;
int num_insns;
int max_insns;
/* ... */
/* generate intermediate code */
pc_start = tb->pc; // tb翻译块的开始
dc->tb = tb;
gen_opc_end = tcg_ctx.gen_opc_buf + OPC_MAX_SIZE;
// 将基本快上下文拷贝到反汇编代码上下文
dc->env = env;
dc->is_jmp = DISAS_NEXT; // 接下来要反汇编的指令类型
dc->pc = pc_start; // 将要翻译的指令
dc->cc_op = CC_OP_DYNAMIC;
dc->singlestep_enabled = cs->singlestep_enabled;
dc->fpcr = env->fpcr;
dc->user = (env->sr & SR_S) == 0;
dc->is_mem = 0;
dc->done_mac = 0;
lj = -1;
num_insns = 0; // 已经翻译的指令数
max_insns = tb->cflags & CF_COUNT_MASK;
if (max_insns == 0)
max_insns = CF_COUNT_MASK;
gen_tb_start();
}
gen_tb_start()
函数定义在头文件 include/exec/gen-icount.h
中,与架构无关,Unicorn 在移植的 QEMU 的过程中,已经进行了修改
static inline void gen_tb_start(void)
{
TCGv_i32 count;
TCGv_i32 flag;
exitreq_label = gen_new_label();
flag = tcg_temp_new_i32();
tcg_gen_ld_i32(flag, cpu_env,
offsetof(CPUState, tcg_exit_req) - ENV_OFFSET);
tcg_gen_brcondi_i32(TCG_COND_NE, flag, 0, exitreq_label);
tcg_temp_free_i32(flag);
if (!use_icount)
return;
icount_label = gen_new_label();
count = tcg_temp_local_new_i32();
tcg_gen_ld_i32(count, cpu_env,
-ENV_OFFSET + offsetof(CPUState, icount_decr.u32));
/* This is a horrid hack to allow fixing up the value later. */
icount_arg = tcg_ctx.gen_opparam_ptr + 1;
tcg_gen_subi_i32(count, count, 0xdeadbeef);
tcg_gen_brcondi_i32(TCG_COND_LT, count, 0, icount_label);
tcg_gen_st16_i32(count, cpu_env,
-ENV_OFFSET + offsetof(CPUState, icount_decr.u16.low));
tcg_temp_free_i32(count);
}
开始翻译
gen_tb_start();
do {
pc_offset = dc->pc - pc_start;
gen_throws_exception = NULL; // 翻译未出现异常
// 遍历每个断点
if (unlikely(!QTAILQ_EMPTY(&cs->breakpoints))) {
QTAILQ_FOREACH(bp, &cs->breakpoints, entry) {
if (bp->pc == dc->pc) {
gen_exception(dc, dc->pc, EXCP_DEBUG);
dc->is_jmp = DISAS_JUMP;
break;
}
}
if (dc->is_jmp)
break;
}
if (search_pc) {
j = tcg_ctx.gen_opc_ptr - tcg_ctx.gen_opc_buf;
if (lj < j) {
lj++;
while (lj < j)
tcg_ctx.gen_opc_instr_start[lj++] = 0;
}
tcg_ctx.gen_opc_pc[lj] = dc->pc;
tcg_ctx.gen_opc_instr_start[lj] = 1;
tcg_ctx.gen_opc_icount[lj] = num_insns;
}
if (num_insns + 1 == max_insns && (tb->cflags & CF_LAST_IO))
gen_io_start();
dc->insn_pc = dc->pc;
disas_m68k_insn(env, dc); // m68k instruction -> TCG IR
num_insns++;
} while (!dc->is_jmp && tcg_ctx.gen_opc_ptr < gen_opc_end &&
!cs->singlestep_enabled &&
!singlestep &&
(pc_offset) < (TARGET_PAGE_SIZE - 32) &&
num_insns < max_insns);
if (tb->cflags & CF_LAST_IO)
gen_io_end();
if (unlikely(cs->singlestep_enabled)) {
/* Make sure the pc is updated, and raise a debug exception. */
if (!dc->is_jmp) {
gen_flush_cc_op(dc);
tcg_gen_movi_i32(QREG_PC, dc->pc);
}
gen_helper_raise_exception(cpu_env, tcg_const_i32(EXCP_DEBUG));
} else {
switch(dc->is_jmp) {
case DISAS_NEXT:
gen_flush_cc_op(dc);
gen_jmp_tb(dc, 0, dc->pc);
break;
default:
case DISAS_JUMP:
case DISAS_UPDATE:
gen_flush_cc_op(dc);
/* indicate that the hash table must be used to find the next TB */
tcg_gen_exit_tb(0);
break;
case DISAS_TB_JUMP:
/* nothing more to generate */
break;
}
}
gen_tb_end(tb, num_insns);
调用 disas_m68k_insn()
将 m68k 指令翻译成 TCG IR(TCG 中间代码)。 TCG 的所有微操作,都在文件./tcg/tcg-op.h
中
TCG 中间码翻译成宿主机指令
tcg_gen_code()
函数定义在 tcg.c
文件中。该函数将中间代码翻译成 host code。
int tcg_gen_code(TCGContext *s, tcg_insn_unit *gen_code_buf)
{
#ifdef CONFIG_PROFILER
{
int n;
n = (s->gen_opc_ptr - s->gen_opc_buf);
s->op_count += n;
if (n > s->op_count_max)
s->op_count_max = n;
s->temp_count += s->nb_temps;
if (s->nb_temps > s->temp_count_max)
s->temp_count_max = s->nb_temps;
}
#endif
tcg_gen_code_common(s, gen_code_buf, -1);
/* flush instruction cache */
flush_icache_range((uintptr_t)s->code_buf, (uintptr_t)s->code_ptr);
return tcg_current_code_size(s);
}
将 TCG 中间码翻译成 host 机器码的函数是 tcg_gen_code_common
。主要过程如下
- 从缓存中找到 TCG 操作码和相应参数;
- 为输入参数分配 host 平台的寄存器;
- 为输出参数分配 host 平台的寄存器;
- 输出翻译好的二进制指令到翻译缓存中。
从代码层面分析,tcg_reg_alloc_op
主要是分析该指令的输入、输出约束,根据这些约束分配寄存器等,然后调用tcg_out_op
将该中间码翻译成host机器码。
0x30 总结
0x31 CPU 初始化
main_loop(…){/vl.c} :
函数 main_loop
初始化 qemu_main_loop_start()
然后进入无限循环 cpu_exec_all()
, 这个是 QEMU 的一个主要循环,在里面会不断的判断一些条件,如虚拟机的关机断电之类的。2
qemu_main_loop_start(…){/cpus.c} :
函数设置系统变量 qemu_system_ready = 1
并且重启所有的线程并且等待一个条件变量。
cpu_exec_all(…){/cpus.c} :
它是 cpu 循环,QEMU 能够启动 256 个 cpu 核,但是这些核将会分时运行,然后执行 qemu_cpu_exec()
。
struct CPUState{/target-xyz/cpu.h} :
它是 cpu 状态结构体,关于 cpu 的各种状态,不同架构下面还有不同。
cpu_exec(…){/cpu-exec.c}:
这个函数是主要的执行循环,这里第一次翻译之前说 TB,TB 被初始化为 TranslationBlock *tb)
,然后不停的执行异常处理。其中嵌套了两个无限循环 find tb_find_fast()
和 tcg_qemu_tb_exec()
.
cantb_find_fast()
为客户机初始化查询下一个 TB,并且生成主机代码。
tcg_qemu_tb_exec()
执行生成的主机代码
0x32 TB 初始化
struct TranslationBlock {/exec-all.h}:
结构体 TranslationBlock 包含下面的成员:PC, CS_BASE, Flags (表明TB), tc_ptr (指向这个TB翻译代码的指针), tb_next_offset[2], tb_jmp_offset[2] (接下去的Tb), *jmp_next[2], *jmp_first (之前的TB).
tb_find_fast(…){/cpu-exec.c} :
函数通过调用获得程序指针计数器,然后传到一个哈希函数从 tb_jmp_cache[]
(一个哈希表)得到TB的所以,所以使用 tb_jmp_cache
可以找到下一个TB。如果没有找到下一个 TB,则使用tb_find_slow
。
tb_find_slow(…){/cpu-exec.c}:
这个是在快速查找失败以后试图去访问物理内存,寻找 TB。
0x33 指令翻译
tb_gen_code(…){/exec.c}:
开始分配一个新的 TB,TB 的 PC 是刚刚从 CPUstate 里面通过 using get_page_addr_code()
找到的
phys_pc = get_page_addr_code(env, pc);
tb = tb_alloc(pc);
ph 当调用 cpu_gen_code()
以后,接着会调用 tb_link_page()
,它将增加一个新的 TB,并且指向它的物理页表。
cpu_gen_code(…){translate-all.c}:
函数初始化真正的代码生成,在这个函数里面有下面的函数调用:
gen_intermediate_code(){/target-arch/translate.c}->gen_intermediate_code_internal(){/target-arch/translate.c }->disas_insn(){/target-arch/translate.c}
disas_insn(){/target-arch/translate.c}:
函数 disas_insn()
真正的实现将客户机代码翻译成 TCG 代码,它通过一长串的switch case
,将不同的指令做不同的翻译,最后调用 tcg_gen_code
。
tcg_gen_code(…){/tcg/tcg.c}:
这个函数将 TCG 的代码转化成主机代码,这个就不细细说明了,和前面类似。
#define tcg_qemu_tb_exec(…){/tcg/tcg.g}:
通过上面的步骤,当 TB 生成以后就通过这个函数进行执行
next_tb = tcg_qemu_tb_exec(tc_ptr) :
extern uint8_t code_gen_prologue[];
#define tcg_qemu_tb_exec(tb_ptr) ((long REGPARM(*)(void *)) code_gen_prologue)(tb_ptr)
术语说明
-
TB: TranslationBlock, 翻译块
-
TCG: Tiny Code Generator, 微型代码生成器
-
IR: Intermediate Representation
-
Backend Ops: 前端翻译器
-
Frontend Ops: 后端生成器