qemu 基础逻辑#
qemu 虚拟机提供两种 CPU 实现的方式,一种是基于中间码的实现,一种是基于 KVM 的实现。
第一种方式一般被叫做叫 tcg(tiny code generator),这种方式的基本思路是用纯软件的方式把 target CPU 的指令先翻译成中间码,然后再把中间码翻译成 host CPU 的指令,通常把 target CPU 指令翻译成中间码的过程叫整个过程的前端,中间码翻译成 host CPU 的过程对应的叫做后端。给 qemu 增加一个新 CPU 的模型需要既增加前端也增加后端,如果要模拟整个系统,还要增加基础设备以及 user mode 的支持。如果目的是在一个成熟的平台上验证另一个新的 CPU,比如在 x86 机器上跑 riscv 的虚拟机,验证 riscv 的逻辑,只需要加上 riscv 指令到中间码这个前端支持就可以了,因为中间码到 x86 的后端已经存在;如果目的是在一台 riscv 的机器上模拟 x86 架构,就需要添加中间码到 riscv 的后端支持。
KVM(Kernel Virtual Machine)是 Linux 的一个内核驱动模块,它能够让 Linux 主机成为一个 Hypervisor(虚拟机监控器)。QEMU 虚拟机是一个纯软件的实现,可以在没有 KVM 模块的情况下独立运行,但是性能会较低一些。QEMU 有整套的虚拟机实现,包括处理器虚拟化、内存虚拟化以及 I/O 设备的虚拟化。QEMU 默认使用纯软件模拟来模拟 CPU 的执行。当启用 KVM 时,QEMU 将 CPU 的执行转交给 KVM 来处理。在硬件支持的情况下,KVM 将直接使用主机 CPU 的硬件虚拟化扩展来执行虚拟机中的指令,从而极大地提升性能。简单来说,KVM 和 QEMU 相辅相成,QEMU 通过 KVM 达到了硬件虚拟化的速度,而 KVM 则通过 QEMU 来模拟设备。 我们暂时只关注第一种方式。riscv 体系相关的前端的代码在:target/riscv/,后端的代码在:tcg/riscv/,基础外设和 machine 的代码在 hw/riscv/。
qemu tcg 前端解码逻辑#
把 target cpu 指令翻译成 host cpu 指令有两种方式,一种是使用 helper 函数,一种是使用 TCG 函数的方式。通常是使用 TCG 函数来操作数据、翻译指令,只有在某些 TCG 操作不方便或者无法模拟 CPU 操作时才会使用 helper 函数。
如果把逻辑拉高一层来看,所谓 target CPU 的运行,实际上是根据 target CPU 指令流去不断的改变 target CPU 数据结构里的数据状态。因为实际的代码要运行到 host CPU 上,所以 target 代码要被翻译成 host 代码,才可以执行,通过执行模拟改变 target CPU 的数据状态。qemu 为了解耦把 target CPU 代码先翻译成中间码,翻译成的中间码的语义也就是改变 target CPU 数据状态的一组==描述语句==,所以 target CPU 状态参数会被当做入参传入中间码描述语句。这组中间码是改变 CPU 状态的抽象的描述,有些 CPU 上的状态不好抽象成一般的描述就用 helper 函数的方式补充,所以 helper 函数也是改变 target CPU 状态的描述。
如果要用 tcg 的方式,就需要使用 tcg_gen_xxx 的函数组织逻辑描述 target CPU 指令对 target CPU 状态的改变。一些公共的代码会自动生成的,qemu 里使用 decode tree 的方式自动生成这一部分代码。
riscv 的指令描述在 target/riscv/insn16.decode、insn32.decode 里(包括指令编码、参数位置、入参结构体等),qemu 编译的时候会解析.decode 文件,使用脚本(scripts/decodetree.py)生成对应的定义和函数,生成的文件放在 qemu/build/libqemu-riscv64-softmmu.fa.p/decode-insn32.c.inc,decode-insn16.c.inc 里。这些文件生成的 trans_xxx 函数只是声明,具体功能需要自己实现,riscv 的这部分实现是放在了在 target/riscv/insn_trans/*里。生成的文件里有两个很大的解码函数 decode-insn32.c.inc 和 decode-insn16.c.inc,qemu 把 target CPU 指令翻译成中间码的时候就需要调用上面两个解码函数,通过查找解码树来调用对应的翻译函数。
现在用 riscv 架构下 user mode 的代码来看看上层具体调用关系。qemu 提供 system mode 和 user mode 的模拟方式,其中 system mode 会完整模拟整个系统,一个完整的 OS 可以运行在这个模拟的系统上,user mode 只是支持加载一个 target CPU 构架的用户态程序来跑,对于一般指令使用 tcg 的方式翻译执行,对于用户态程序里的系统调用,user mode 代码里模拟实现了==系统调用==的过程。linux user mode 的代码在 qemu/linux-user/*,具体的调用过程如下:
/* qemu/linux-user/main.c */
main
+-> cpu_loop
+-> cpu_exec
+-> tb_gen_code
| /* qemu/target/riscv/trannslate.c */
| +-> gen_intermediate_code
| | +-> translator_loop(&riscv_tr_ops, xxx)
| | /* riscv_tr_translate_insn */
| | +-> ops->translator_insn
| | +-> decode_ops
| | +-> decode_insn16
| | +-> decode_insn32
| +-> tcg_gen_code
| +-> tcg_out_xxx
+-> cpu_loop_exec_tbcgen_intermediate_code 是前端的解码函数,把 target CPU 的指令翻译成 tcg 中间码。tcg_gen_code 是后端,把中间码翻译成 host CPU 上的指令,其中 tcg_out_xxx 的一组函数做具体的翻译工作。 下面展开其中的各个细节:
- tcg 整个翻译流程构架分析
- decode tree 的语法
- tcg trans_xxx 函数的语法
tcg 翻译流程#
整个 tcg 前后端的翻译流程按指令块的粒度来搞,收集一个指令块翻译成中间码,然后把中间码翻译成 host CPU 指令,整个过程动态执行。为了加速翻译,qemu 把翻译成的 host CPU 指令块做了缓存,tcg 前端解码的时候,先在缓存里找,如果找见就直接执行。
decode tree 语法#
CPU 指令编码通常是按组划分的,因此可以用 decode 去描述这些固定的结构,然后 qemu 根据这些指令定义,使用一个脚本(scripts/decodetree.py)在编译的时候生成解码函数的框架。 decode tree 里定义了几个描述:field,argument,format,pattern,group。CPU 在解码的时候总要把指令中的特性 field 中的数据取出作为入参(寄存器编号,立即数,操作码等),field 描述一个指令编码中特定的域段,根据描述可以生成取对应域段的函数。
+---------------------------+---------------------------------------------+
| Input | Generated code |
+===========================+=============================================+
| %disp 0:s16 | sextract(i, 0, 16) |
+---------------------------+---------------------------------------------+
| %imm9 16:6 10:3 | extract(i, 16, 6) << 3 | extract(i, 10, 3) |
+---------------------------+---------------------------------------------+
| %disp12 0:s1 1:1 2:10 | sextract(i, 0, 1) << 11 | |
| | extract(i, 1, 1) << 10 | |
| | extract(i, 2, 10) |
+---------------------------+---------------------------------------------+
| %shimm8 5:s8 13:1 | expand_shimm8(sextract(i, 5, 8) << 1 | |
| !function=expand_shimm8 | extract(i, 13, 1)) |
+---------------------------+---------------------------------------------+c一个数据,比如一个立即数,可能是多个域段拼成的,所以就有相应的移位操作,再比如有些立即数是编码域段的数值取出来后再进过简单运算得到的,field 定义中带的函数就可以完成这样的计算。 argument 用来定义数据结构,比如,riscv insn32.decode 里定义的: &b imm rs2 rs1,编译后的 decode-insn32.c.inc 里生成的数据结构如下,这个结构会作为 trans_xxx 函数的入参。
typedef struct {
int imm;
int rs2;
int rs1;
} arg_b;cformat 定义指令的格式。
@r ....... ..... ..... ... ..... ....... &r %rs2 %rs1 %rd
@i ............ ..... ... ..... ....... &i imm=%imm_i %rs1 %rdc比如上面就是对一个 32bit 指令编码的描述,. 表示一个 0 或者 1 的 bit 位,描述里可以用 field、之前定义的 filed 的引用、argument 的引用,field 的引用还可以赋值。field 可以用来匹配,argument 用来生成 trans_xxx 函数的入参。 pattern 用来定义具体指令。比如 riscv32 里的 lui 指令:
lui .................... ..... 0110111 @u
@u .................... ..... ....... &u imm=%imm_u %rd
&u imm rd
%imm_u 12:s20 !function=ex_shift_12
%rd 7:5c上面把相关的 format、argument、field 的定义也列了出来。可以看到 lui 的操作码是 0110111,这个指令的格式定义是@u,这个格式定义使用的参数定义是&u,&u 就是 trans_lui 函数入参结构体里的变量的定义,其中定义的变量名字是 imm、rd,这个 imm 实际的格式是%imm_i, 它是一个在指令编码 31-12bit 定义立即数,要把 31-12bit 的数值左移 12bit 得到最终结果,rd 实际的格式是%rd,是一个在指令编码 11-7bit 定义的 rd 寄存器的标号。可以看到 riscv 里对应的 trans 函数的实现如下,在编译时,脚本只生成一个空函数,函数内容需要前端实现者编写。
static bool trans_lui(DisasContext *ctx, arg_lui *a)
{
if (a->rd != 0) {
tcg_gen_movi_tl(cpu_gpr[a->rd], a->imm);
}
return true;
}ctrans_xxx 函数的逻辑#
trans_xxxx 函数的作用是生成中间码指令。以 riscv 的 add 指令为例,如下是 trans_rvi.c.inc 里 add 指令的模拟。
static bool trans_add(DisasContext *ctx, arg_add *a)
{
return gen_arith(ctx, a, EXT_NONE, tcg_gen_add_tl, tcg_gen_add2_tl);
}
static bool gen_arith(DisasContext *ctx, arg_r *a, DisasExtend ext,
void (*func)(TCGv, TCGv, TCGv),
void (*f128)(TCGv, TCGv, TCGv, TCGv, TCGv, TCGv))
{
TCGv dest = dest_gpr(ctx, a->rd);
TCGv src1 = get_gpr(ctx, a->rs1, ext);
TCGv src2 = get_gpr(ctx, a->rs2, ext);
if (get_ol(ctx) < MXL_RV128) {
func(dest, src1, src2);
gen_set_gpr(ctx, a->rd, dest);
} else {
if (f128 == NULL) {
return false;
}
TCGv src1h = get_gprh(ctx, a->rs1);
TCGv src2h = get_gprh(ctx, a->rs2);
TCGv desth = dest_gprh(ctx, a->rd);
f128(dest, desth, src1, src1h, src2, src2h);
gen_set_gpr128(ctx, a->rd, dest, desth);
}
return true;
}ctcg_gen_add_i32 可以看作为 tcg_gen_add_tl 的函数入参,riscv 的 add 指令从 target CPU 的 rs1,rs2 两个寄存器里取两个加数,相加后放到 rd 寄存器里。这里在 TCG 体系中的操作从直观上看应该是已经操作完成了,但是实际上这里的操作只是保存了这一条指令的操作语义。tcg_gen_add_i32 的实现为:
void tcg_gen_add_i32(TCGv_i32 ret, TCGv_i32 arg1, TCGv_i32 arg2)
{
tcg_gen_op3_i32(INDEX_op_add_i32, ret, arg1, arg2);
}
tcg_gen_add_i32(TCGv_i32 ret, TCGv_i32 arg1, TCGv_i32 arg2)|tcg_gen_add_tl
-> tcg_gen_op3_i32(TCGOpcode opc, TCGv_i32 a1, TCGv_i32 a2, TCGv_i32 a3)
-> tcg_gen_op3(TCGOpcode opc, TCGArg a1, TCGArg a2, TCGArg a3)
-> TCGOp *op = tcg_emit_op(opc, 3);
-> op->args[0] = a1;
-> op->args[1] = a2;
-> op->args[2] = a3;c可以看到最后生成的指令把数据挂到了一个链表里,后面的后端解码会把这些指令翻译成 host 指令。
TCG 体系的数据结构#
这里需要简单介绍一下 TCG 体系的数据结构的定义。
// tcg/tcg.h
// TCG 的核心数据结构之一,它包含了 TCG 状态的所有全局信息
struct TCGContext {
/* ... other fields ... */
TCGOp *ops; // 当前正在生成的 TCG 操作
TCGTemp *temps; // 临时变量数组
int nb_temps; // 临时变量数量
TCGLabel *labels; // 标签数组
int nb_labels; // 标签数量
int code_gen_buffer_size; // 代码生成缓冲区大小
uint8_t *code_gen_buffer; // 代码生成缓冲区
/* ... other fields ... */
};c// tcg/tcg.h
// 表示一个 TCG 操作,通常是翻译后的目标指令
typedef struct TCGOp {
TCGOpcode opc; // 操作码
TCGArg args[TCG_MAX_OP_ARGS]; // 操作的参数
} TCGOp;c// tcg/tcg.h
// 表示一个 TCG 临时变量
struct TCGTemp {
TCGType type; // 临时变量类型
int val_type; // 值类型
int reg; // 寄存器编号
int mem_reg; // 内存寄存器编号
tcg_target_long val; // 临时变量的值
/* ... other fields ... */
};c// tcg/tcg.h
// 表示一个标签,用于标记代码生成中的位置
typedef struct TCGLabel {
tcg_insn_unit *label_ptr; // 标签指针,指向生成代码的位置
} TCGLabel;c// tcg/tcg-opc.h
// 一个枚举类型,表示 TCG 支持的所有操作码
typedef enum TCGOpcode {
INDEX_op_add_i32,
INDEX_op_sub_i32,
/* ... other opcodes ... */
} TCGOpcode;c那么 TCG 创建的变量存在哪里?TCGv cpu_gpr[reg_num]是一个全局变量,它如何索引到 target CPU 的寄存器?
get_gpr(ctx, a->rs2, ext) <==> return cpu_gpr[reg_num]
// cpu_gpr[i] 会在每个TB块CPUStatus初始化时创建
riscv_translate_init
->cpu_gpr[i] = tcg_global_mem_new(tcg_env, offsetof(CPURISCVState, gpr[i]), riscv_int_regnames[i]);c首先 tcg_temp_new 分配的空间是在 TCGContext tcg_ctx 里的,所谓创建一个这样的 TCGv 就是在 tcg_ctx 里用去一个 TCGTemp。cpu_gpr[reg_num]可以索引到 target CPU 寄存器的基本逻辑就是只要在前端和后端约定好描述 target CPU 的软件结构,cpu_gpr[reg_num]描述的就是相关寄存器在这个软件结构里的位置。然后 tcg_env 在 tcg_context_init(unsigned max_cpus)里初始化,得到的是 tcg_ctx 里 TCGTemp temps 的地址。 tcg_global_mem_new 在 tcg_ctx 里从 TCGTemp temps 上分配空间,返回空间在 tcg_ctx 上的相对地址。这样 cpu_gpr[reg_name]就可以作为标记在前端和后端之间建立连接。 后端的代码直接把中间码翻译成 host 指令,中间码中的 TCGv 直接映射到 host CPU 的寄存器上,从逻辑上讲,应该是翻译得到的 host 代码修改中间码对应 TCGv 对应的内存才对。这里的逻辑是 qemu 在生成的中间码中以及 TB 执行后做了 host 寄存器到 target CPU 描述内存之间的同步。
指令添加流程#
通过以上针对 TCG 前端的分析,这里会通过一个例子来展示如何添加 RISC-V 自定义指令,以 SADD 饱和加法指令为例。 首先需要在 insn32.decode 文件中定义指令编码,因为操作数格式与 r 类指令相同,因此不需要添加新的 Argument。
sadd 0000000 ..... ..... 000 ..... 0001011 @rc通过 decodetree.py 脚本生成后会得到 sadd 指令的翻译函数与入参结构体(qemu/build/libqemu-riscv32-softmmu.fa.p/decode-insn32.c.inc):
typedef struct {
int rd;
int rs1;
int rs2;
} arg_r;
typedef arg_r arg_sadd;
static bool trans_sadd(DisasContext *ctx, arg_sadd *a);c同时,sadd 指令的解析也被添加到 decode tree 中:
case 0x0000000b:
/* ........ ........ ........ .0001011 */
switch (insn & 0xf8007000u) {
case 0x00000000:
/* 00000... ........ .000.... .0001011 */
decode_insn32_extract_r(ctx, &u.f_r, insn);
switch ((insn >> 25) & 0x3) {
case 0x0:
/* 0000000. ........ .000.... .0001011 */
/* ../target/riscv/insn32.decode:187 */
if (trans_sadd(ctx, &u.f_r)) return true;
break;
}
break;c具体的 trans_sadd()函数功能需要我们在翻译文件中自己添加实现:
target/riscv/insn_trans/trans_rvi.c.inc
static bool trans_sadd(DisasContext *ctx, arg_sadd *a)
{
return gen_sadd(ctx, a, EXT_NONE);
}
static bool gen_sadd(DisasContext *ctx, arg_r *a, DisasExtend ext)
{
TCGv rd = dest_gpr(ctx, a->rd);
TCGv rs1 = get_gpr(ctx, a->rs1, ext);
TCGv rs2 = get_gpr(ctx, a->rs2, ext);
TCGv temp = tcg_temp_new_i32();
TCGv max_val = tcg_constant_i32(0x7fffffff);
TCGv min_val = tcg_constant_i32(0x80000000);
TCGv zero = tcg_constant_i32(0);
// temp = rs1 + rs2
tcg_gen_add_i32(temp, rs1, rs2);
// checkout overflow = (rs1 > 0 && rs2 > 0 && temp < 0)
TCGv_i32 overflow = tcg_temp_new_i32();
TCGv_i32 temp_cond = tcg_temp_new_i32();
tcg_gen_setcond_i32(TCG_COND_GT, overflow, rs1, zero);
tcg_gen_setcond_i32(TCG_COND_GT, temp_cond, rs2, zero);
tcg_gen_and_i32(overflow, overflow, temp_cond);
tcg_gen_setcond_i32(TCG_COND_LT, temp_cond, temp, zero);
tcg_gen_and_i32(overflow, overflow, temp_cond);
// checkout underflow = (rs1 < 0 && rs2 < 0 && temp >= 0)
TCGv_i32 underflow = tcg_temp_new_i32();
tcg_gen_setcond_i32(TCG_COND_LT, underflow, rs1, zero);
tcg_gen_setcond_i32(TCG_COND_LT, temp_cond, rs2, zero);
tcg_gen_and_i32(underflow, underflow, temp_cond);
tcg_gen_setcond_i32(TCG_COND_GE, temp_cond, temp, zero);
tcg_gen_and_i32(underflow, underflow, temp_cond);
/// if overflow, result = INT32_MAX
tcg_gen_movcond_i32(TCG_COND_NE, rd, overflow, zero, max_val, temp);
// if underflow, result = INT32_MIN
tcg_gen_movcond_i32(TCG_COND_NE, rd, underflow, zero, min_val, temp);
return true;
}c总体的添加流程简述如上。