Qemu机制与自定义指令添加指南
QEMU 基础逻辑
QEMU 虚拟机提供两种 CPU 实现方式:一种是基于中间码的实现,一种是基于 KVM 的实现。

第一种方式一般被称为 TCG(tiny code generator),基本思路是用纯软件的方式把 target CPU 的指令先翻译成中间码,再把中间码翻译成 host CPU 的指令。通常把 target CPU 指令翻译成中间码的过程称为前端,把中间码翻译成 host CPU 指令的过程称为后端。给 QEMU 增加一个新 CPU 模型,需要同时增加前端和后端;如果要模拟整个系统,还要增加基础设备以及 user mode 的支持。如果目的是在一个成熟的平台上验证另一个新的 CPU,比如在 x86 机器上跑 RISC-V 的虚拟机,验证 RISC-V 的逻辑,只需要加上 RISC-V 指令到中间码这个前端支持即可,因为中间码到 x86 的后端已经存在;如果目的是在一台 RISC-V 的机器上模拟 x86 架构,就需要添加中间码到 RISC-V 的后端支持。
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 来模拟设备。
我们暂时只关注第一种方式。RISC-V 体系相关的前端代码在 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 的方式自动生成这部分代码。
RISC-V 的指令描述在 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 函数只是声明,具体功能需要自己实现,RISC-V 的这部分实现放在 target/riscv/insn_trans/* 里。生成的文件中有两个大的解码函数 decode-insn32.c.inc 和 decode-insn16.c.inc,QEMU 把 target CPU 指令翻译成中间码时,会调用这两个解码函数,通过查找解码树来调用对应的翻译函数。
下面以 RISC-V 架构下 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 用来定义数据结构。比如 RISC-V 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、之前定义的 field 的引用、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_u,它是指令编码 31-12bit 定义的立即数,需要把 31-12bit 的数值左移 12bit 得到最终结果;rd 实际的格式是 %rd,是指令编码 11-7bit 定义的 rd 寄存器标号。RISC-V 里对应的 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_xxx 函数的作用是生成中间码指令。以 RISC-V 的 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;
}c在 gen_arith 中,tcg_gen_add_tl 作为函数指针入参传入。RISC-V 的 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;ctrans_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