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/*,具体的调用过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| main +-> cpu_loop +-> cpu_exec +-> tb_gen_code | | +-> gen_intermediate_code | | +-> translator_loop(&riscv_tr_ops, xxx) | | | | +-> ops->translator_insn | | +-> decode_ops | | +-> decode_insn16 | | +-> decode_insn32 | +-> tcg_gen_code | +-> tcg_out_xxx +-> cpu_loop_exec_tb
|
gen_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描述一个指令编码中特定的域段,根据描述可以生成取对应域段的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| +---------------------------+---------------------------------------------+ | 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)) | +---------------------------+---------------------------------------------+
|
一个数据,比如一个立即数,可能是多个域段拼成的,所以就有相应的移位操作,再比如有些立即数是编码域段的数值取出来后再进过简单运算得到的,field定义中带的函数就可以完成这样的计算。
argument用来定义数据结构,比如,riscv insn32.decode里定义的: &b imm rs2 rs1,编译后的decode-insn32.c.inc里生成的数据结构如下,这个结构会作为trans_xxx函数的入参。
1 2 3 4 5
| typedef struct { int imm; int rs2; int rs1; } arg_b;
|
format定义指令的格式。
1 2
| @r ....... ..... ..... ... ..... ....... &r %rs2 %rs1 %rd @i ............ ..... ... ..... ....... &i imm=%imm_i %rs1 %rd
|
比如上面就是对一个32bit指令编码的描述,. 表示一个0或者1的bit位,描述里可以用field、之前定义的filed的引用、argument的引用,field的引用还可以赋值。field可以用来匹配,argument用来生成trans_xxx函数的入参。
pattern用来定义具体指令。比如riscv32里的lui指令:
1 2 3 4 5 6 7 8
| lui .................... ..... 0110111 @u
@u .................... ..... ....... &u imm=%imm_u %rd
&u imm rd
%imm_u 12:s20 !function=ex_shift_12 %rd 7:5
|
上面把相关的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函数的实现如下,在编译时,脚本只生成一个空函数,函数内容需要前端实现者编写。
1 2 3 4 5 6 7
| 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; }
|
trans_xxx函数的逻辑
trans_xxxx函数的作用是生成中间码指令。以riscv的add指令为例,如下是trans_rvi.c.inc里add指令的模拟。
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
| 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; }
|
tcg_gen_add_i32可以看作为tcg_gen_add_tl的函数入参,riscv的add指令从target CPU的rs1,rs2两个寄存器里取两个加数,相加后放到rd寄存器里。这里在TCG体系中的操作从直观上看应该是已经操作完成了,但是实际上这里的操作只是保存了这一条指令的操作语义。tcg_gen_add_i32 的实现为:
1 2 3 4 5 6 7 8 9 10 11 12
| 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;
|
可以看到最后生成的指令把数据挂到了一个链表里,后面的后端解码会把这些指令翻译成host指令。
TCG体系的数据结构
这里需要简单介绍一下TCG体系的数据结构的定义。
1 2 3 4 5 6 7 8 9 10 11 12 13
|
struct TCGContext { TCGOp *ops; TCGTemp *temps; int nb_temps; TCGLabel *labels; int nb_labels; int code_gen_buffer_size; uint8_t *code_gen_buffer; };
|
1 2 3 4 5 6
|
typedef struct TCGOp { TCGOpcode opc; TCGArg args[TCG_MAX_OP_ARGS]; } TCGOp;
|
1 2 3 4 5 6 7 8 9 10
|
struct TCGTemp { TCGType type; int val_type; int reg; int mem_reg; tcg_target_long val; };
|
1 2 3 4 5
|
typedef struct TCGLabel { tcg_insn_unit *label_ptr; } TCGLabel;
|
1 2 3 4 5 6 7
|
typedef enum TCGOpcode { INDEX_op_add_i32, INDEX_op_sub_i32, } TCGOpcode;
|
那么TCG创建的变量存在哪里?TCGv cpu_gpr[reg_num]是一个全局变量,它如何索引到target CPU的寄存器?
1 2 3 4
| get_gpr(ctx, a->rs2, ext) <==> return cpu_gpr[reg_num]
riscv_translate_init ->cpu_gpr[i] = tcg_global_mem_new(tcg_env, offsetof(CPURISCVState, gpr[i]), riscv_int_regnames[i]);
|
首先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。
1
| sadd 0000000 ..... ..... 000 ..... 0001011 @r
|
通过decodetree.py脚本生成后会得到sadd指令的翻译函数与入参结构体(qemu/build/libqemu-riscv32-softmmu.fa.p/decode-insn32.c.inc):
1 2 3 4 5 6 7
| typedef struct { int rd; int rs1; int rs2; } arg_r; typedef arg_r arg_sadd; static bool trans_sadd(DisasContext *ctx, arg_sadd *a);
|
同时,sadd指令的解析也被添加到decode tree中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| case 0x0000000b: switch (insn & 0xf8007000u) { case 0x00000000: decode_insn32_extract_r(ctx, &u.f_r, insn); switch ((insn >> 25) & 0x3) { case 0x0: if (trans_sadd(ctx, &u.f_r)) return true; break; } break;
|
具体的trans_sadd()函数功能需要我们在翻译文件中自己添加实现:
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 34 35 36 37 38 39 40 41 42 43 44 45 46
| 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);
tcg_gen_add_i32(temp, rs1, rs2);
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);
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);
tcg_gen_movcond_i32(TCG_COND_NE, rd, overflow, zero, max_val, temp);
tcg_gen_movcond_i32(TCG_COND_NE, rd, underflow, zero, min_val, temp);
return true; }
|
总体的添加流程简述如上。