QEMU简介
一、QEMU基本介绍
QEMU是一种通用的开源计算机仿真器和虚拟软件。
1.1操作模式
QEMU共有两种操作模式:全系统仿真和用户模式仿真
全系统仿真:能够在任意支持的架构上为任何机器运行一个完整的操作系统,包括跑操作系统以及操作系统上的软件,启动的流程要涉及bootloader。
用户模式仿真:能够在任意支持的架构上为另一个Linux/BSD运行程序。这属于用户级的模拟,程序跑在用户态,相当于跑裸机程序,用户态的程序调用采用调用语义进行模拟。
1.2 虚拟化方式
QEMU提供两种方式的CPU虚拟化:基于KVM实现的和基于中间代码实现的
基于KVM实现:全虚拟化是一种QEMU通过模拟完整计算机硬件的方式来运行客户机操作系统的虚拟化技术。QEMU使用KVM(内核虚拟机)来加速虚拟化。这种方法在没有硬件虚拟化支持的系统上可以实现虚拟化,但是其性能通常比较低。此类虚拟化能够支持不同类型的客户机操作系统,这需要处理器支持VMX功能。基于KVM的方式,直接使用host CPU执行target CPU的指令,性能接近host上的性能,但是需要target CPU和host CPU是相同的构架。
基于中间代码实现(TCG,Tiny Code Generator):当处理器不支持虚拟化技术(VMX)时,QEMU使用全虚拟化技术来实现CPU虚拟化。在这种情况下,QEMU会完全模拟虚拟机的硬件环境,包括模拟CPU、内存、磁盘、网络和其它硬件设备等。QEMU通过软件来模拟CPU指令集,安装并运行客户机操作系统,当指令执行时,QEMU的CPU执行模拟程序可以在主机CPU上进行模拟。因此,在非虚拟化CPU上执行的指令被QEMU模拟并转换为可以在CPU上运行的指令。实现的具体方式时通过纯软的方式将目标架构代码转换为中间代码(相当于前端),然后将中间代码转换为宿主机架构的代码(相当于后端),这种方式虽然会造成仿真效率的降低,但是不需要强制CPU支持VMX。
中间代码实现方式简介
TCG(tiny code generator),这种方式的基本思路是用纯软件的方式把targetCPU的指令先翻译成所谓的中间码,然后再把中间码翻译成host CPU 的指令,把targetCPU指令翻译成中间码的过程叫整个过程的前端,中间码翻译成hostCPU的过程对应的叫做后端。给qemu增加一个新CPU的模型需要既增加前端也增加后端,如果要把整个系统支持起来,还要增加基础设备以及user mode的支持。
为了移植性和通用性方面的考虑,qemu定义了micro-op,qemu代码翻译流程如下:target instruction->micro-op->tcg->host
instruction。
源码结构分布
hw:基础外设和machine代码
tcg:后端代码
target:前端代码
二、qemu tcg前端解码逻辑
将target cpu指令翻译成host cpu指令有两种方式,一种是使用helper函数,另一种是使用tiny code generator函数的方式。
所谓的target cpu运行,就是根据target CPU指令流去不断的改变target CPU软件描述结构里的数据状态,实际的代码是要运行在host CPU上。target代码要被翻译成host代码,才可以执行。qemu为了解耦先将target CPU翻译成中间码,本质上中间码的语义就是改变target CPU数据状态的一组描述语句,所以target CPU状态参数会被当做入参传入中间码描述语句。
tcg的方式,我们要使用tcg_gen_xxx的函数组织逻辑描述target CPU指令对target CPU状态的改变。一些公共的代码(trans的声明和入参的结构体等)是可以自动生成的,qemu里使用decode tree的方式自动生成这一部代码。
以riscv的代码来具体说明。qemu定义了一组target CPU指令的描述格式,说明文档在:docs/devel/decodetree.rst,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指令翻译成中间码的时候就需要调用上面的解码函数。
2.1 tcg翻译流程
整个tcg前后端的翻译流程按照指令块的粒度来执行,收集一个指令块翻译成中间码,然后再把中间码翻译成host CPU指令,这是一个动态执行的过程,tcg前端解码的时候,先在缓存里找,如果找到就i执行。
2.1.1 decode tree语法
因为cpu指令编码具有唯一性,可以用decode去描述这些固定的结构,然后qemu根据这些指令定义,使用在scripts目录下的decodetree.py脚本在qemu编译的时候生成解码函数的框架。
decode
tree里定义了几个描述:field,argument,format,pattern,group。细节描述文件在decodetree.rst这个文件。
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)) |
±--------------------------±--------------------------------------------+
argument用来定义数据结构,比如,riscv insn32.decode里定义的: &b imm rs2 rs1,
编译后的decode-insn32.c.inc里生成的数据结构如下,这个结构可以做trans_xxx函数的入参:
typedef struct
{
int imm;
int rs2;
int rs1;
} arg_b;
format定义指令的格式。比如;
@opr ...... ra:5 rb:5 ... 0 ....... rc:5
@opi ...... ra:5 lit:8 1 ....... rc:5
比如上面就是对一个32bit指令编码的描述,.表示一个0或者1的bit位,描述里可以用field、之前定义的filed的引用、argument的引用,field的引用还可以赋值。field可以用来匹配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:5
2.1.2 trans_xxx函数的逻辑
tcg的trans_xxx函数在qemu/tcg/README里很好的介绍,这个文档里介绍了中间码的整套指令(本质就是一些函数),这样可以将指令和对应的trans_xxx函数对上,trans_xxx函数的作用是生成这些中间码指令。以下以riscv的add指令为例,看下qemu对这条指令的
static bool trans_add(DisasContext *ctx, arg_add *a)
{ return gen_arith(ctx, a, &tcg_gen_add_tl);
}
static bool gen_arith(DisasContext *ctx, arg_r *a,
void(*func)(TCGv, TCGv, TCGv))
{
TCGv source1, source2;
source1 = tcg_temp_new();
source2 = tcg_temp_new();
gen_get_gpr(source1, a->rs1);
gen_get_gpr(source2, a->rs2);
(*func)(source1, source1, source2);
gen_set_gpr(a->rd, source1);
tcg_temp_free(source1);
tcg_temp_free(source2);
return true;
}
static inline 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可以看作是tcg_gen_add_tl函数的入参,riscv的add指令从target从 CPU的rs1和rs2中取两个加数,相加后放到rd寄存器中,add的实现如下:
tcg_gen_add_tl
->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_op3(opc, tcgv_i32_arg(a1), tcgv_i32_arg(a2), tcgv_i32_arg(a3))
-> TCGOp *op = tcg_emit_op(opc);
-> op->args[0] = a1;
-> op->args[1] = a2;
->op->args[2] = a3;
上面 gen_get_gpr也是中间码:把rs1/2位置上的数据放到source1/2位置上,它的实现如下
tcg_gen_mov_tl(t, cpu_gpr[reg_num])
-> tcg_gen_mov_i64
-> tcg_gen_op2_i64(INDEX_op_mov_i64, ret, arg)
-> tcg_gen_op2(opc, tcgv_i64_arg(a1), tcgv_i64_arg(a2))
-> TCGOp *op = tcg_emit_op(opc);
-> op->args[0] = a1;
-> op->args[1] = a2;
可以看到最后生成的mov指令被挂载到一个链表中,后端在解码后会将这些指令翻译成host指令,生成的指令就是qemu/tcg/README里介绍的mov_i32/i64 t0, t1这个指令。其中有两个重点1、tcg_temp_new()创建的变量使用TCGTemp这个结构体完成的;2、 cpu_gpr[reg_num]是寄存器的描述,需要索引到target寄存器的基本逻辑,需要在前端和后端约定号描述target CPU的软件结构,cpu_gpr[reg_num]描述的就是相关寄存器在这个软件里的位置。这个数组的初始化流程在translate里:
void riscv_translate_init(void) {
int i;
cpu_gpr[0] = NULL;
for (i = 1; i < 32; i++) {
cpu_gpr[i] = tcg_global_mem_new(cpu_env,
offsetof(CPURISCVState, gpr[i]),
riscv_int_regnames[i]);
}
cpu_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对应的内存才对。
三、编译相关
3.1 代码拉取(拉取自己想要的版本)
git clone https://gitlab.com/qemu-project/qemu.git
cd qemu
git submodule init
git submodule update --recursive
3.2 编译参数
…/qemu/configure --target-list=riscv64-softmmu --extra-cxxflags=-O0
–extra-cflags=-O0 --enable-debug --enable-debug-info --enable-debug-tcg --prefix=/home/s29363/qemu4/install
3.3 依赖包
Ninja glib libffi pcre pixman….
PKG_CONFIG_PATH环境配置:
export PKG_CONFIG_PATH= P K G C O N F I G P A T H : PKG_CONFIG_PATH: PKGCONFIGPATH:HOME/tmp/libffi-3.1/install/lib
export PKG_CONFIG_PATH= P K G C O N F I G P A T H : PKG_CONFIG_PATH: PKGCONFIGPATH:HOME/tmp/pcre-8.45/install/lib/pkgconfig
export PKG_CONFIG_PATH= P K G C O N F I G P A T H : PKG_CONFIG_PATH: PKGCONFIGPATH:HOME/tmp/glib/install/lib/pkgconfig
export PKG_CONFIG_PATH= P K G C O N F I G P A T H : PKG_CONFIG_PATH: PKGCONFIGPATH:HOME/tmp/glib/install/lib/pkgconfig
export PKG_CONFIG_PATH= P K G C O N F I G P A T H : PKG_CONFIG_PATH: PKGCONFIGPATH:HOME/tmp/pixman-0.40.0/install/lib/pkgconfig
3.4 运行相关
-d op,out_asm,in_asm 可以显示qemu的log信息
用户模式下
-d QEMU_LOG,使能对特定事件的日志记录,其后参数加一些item
> Log items (comma separated):
out_asm show generated host assembly code for each compiled TB
in_asm show target assembly code for each compiled TB
op show micro ops for each compiled TB
op_opt show micro ops after optimization
op_ind show micro ops before indirect lowering
int show interrupts/exceptions in short format
exec show trace before each executed TB (lots of logs)
cpu show CPU registers before entering a TB (lots of logs)
fpu include FPU registers in the 'cpu' logging
mmu log MMU-related activities
pcall x86 only: show protected mode far calls/returns/exceptions
cpu_reset show CPU state before CPU resets
unimp log unimplemented functionality
guest_errors log when the guest OS does something invalid (eg accessing a non-existent register)
page dump pages at beginning of user mode emulation
nochain do not chain compiled TBs so that "exec" and "cpu" show complete traces
-D QEMU_LOG_FILENAME,指定日志输出文件,默认为stderr
-g QEMU_GDB,后加端口号,可以连接GDB或者IDA进行远程调试
-strace QEMU_STRACE,将二进制文件的系统调用输出到终端。这样做的好处是,可以帮助了解二进制代码正在从事哪些活动。即查看目标二进制文件调用了哪些系统调用函数。
-trace QEMU_TRACE,在源文件目录/docs/devel/tracing.txt文件中有部分说明
四、KVM和TCG对比
在QEMU中,地址空间的处理方式受到不同CPU类型的影响,TCG和KVM的处理方式会有所不同。
对于TCG类型的CPU,因为它是在QEMU用户空间中运行的,所以可以允许多个虚拟CPU同时运行,每个虚拟CPU拥有各自的AddressSpace。在内存访问时,每个虚拟CPU会使用自己的AddressSpace映射虚拟地址到物理地址,不同虚拟CPU之间彼此独立、互不干扰。因此,在TCG模式下,一个CPU可以拥有多个地址空间。
相反,对于KVM类型的CPU,因为它是在Linux内核中运行的,和其它Linux进程一样,只能拥有一个AddressSpace。在内存访问时,KVM虚拟CPU使用的AddressSpace来自于它所运行的进程,即QEMU进程,所有虚拟CPU共享同一份AddressSpace映射表,每个虚拟CPU使用的是同一份映射关系,从而实现对模拟器内的地址空间的统一管理和访问。
所以,无论采用哪种CPU类型,AddressSpace在QEMU中都是非常重要的,因为它是QEMU模拟器内存管理的关键组成部分。在使用QEMU进行模拟时,需要根据具体需求和CPU类型等因素来选择使用相应的地址空间处理方式。