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->pcenv 是在哪里赋值的呢?经过检查,函数 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: 后端生成器


  1. TCG动态二进制翻译技术研究.pdf ↩︎

  2. KVM虚拟机代码揭秘——QEMU代码结构分析 ↩︎

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐