菜鸡读QEMU源码
当试图找QEMU的源码分析文档来看时↓
博主的代码水平不是很高,代码的高级写法也不是很清楚,所以就是从一个“菜鸡”的水平大致分析了QEMU源码中部分文件以及部分函数的功能,整理在下面。如果大家发现有任何问题,请指正,不胜感激~
QEMU的部分源码架构
qemu5.2.0
├─/hw 仿真所有虚拟机中的虚拟硬件的代码
├─/include 包含一些头文件(其他文件中也包含了剩余的头文件)
├─/softmmu + /accel 应当是system mode相关的源代码,cpu-exec.c文件为入口
├─/linux-user + /bsd-user user mode相关的源代码,分别是Linux系统和BSD系统的用户模式(本博客主要针对Linux系统)
└─/arm/i386/mips 表示目标二进制程序的架构,即要模拟的架构。
├─/target 将所有支持的客户机二进制翻译为TCG中间表示,translate.c【TCG前端】
└─/arm/i386/mips 将相应的客户机指令(ARM、MIPS、i386)翻译为中间指令IR
└─/tcg 包含动态二进制翻译工具TCG源码,作用为将TCG中间表示翻译为宿主机二进制,也包含对应的host的代码,/tcg/xyz/tcg-target.c【TCG后端】
└─/arm/i386/mips 主要的翻译后端代码还是在tcg.c文件中实现,与具体架构相关的会在这些文件中,该文件夹表示宿主机使用的指令集,将IR编译为host code。
QEMU部分源码分析
系统模式
函数调用图如下所示:重点关注TCG二进制动态翻译那块的函数调用
函数参数、所在文件、功能见下表:
函数 | 所在文件 | 函数功能 |
---|---|---|
main() | /softmmu/main.c | 主函数 |
qemu_init(argc,argv,envp) | /softmmu/vl.c | 解析参数,根据参数初始化运行环境,包括虚拟CPU、内存、寄存器、硬件设备、文件系统,KVM(xen)等。 |
qemu_main_loop() | /softmmu/vl.c | QEMU主循环,监控程序的执行状态 |
qemu_cleanup() | /softmmu/vl.c | 与init对应,在仿真结束后需要将初始化的东西关闭并清除占用的资源 |
xxx_cpu_class_init() | /target/xxx/cpu.c | qemu_init不是显式函数调用xxx_cpu_class_init,而是通过函数指针的方式。文件/include/qom/object.h中定义了@class_init,class_init会在所有的父类初始化完成后调用相应的函数,这是一个函数指针。当QEMU在仿真某个架构的系统时,会在/target/xxx/cpu.c中对.class_init赋值,值为xxx_cpu_class_init |
xxx_cpu_realizefn() | /target/xxx/cpu.c | 在xxx_cpu_class_init函数中,通过调用device_class_set_parent_realize(_ , xxx_cpu_realizefn, _)来调用该函数,也是通过函数指针的方式调用。 |
qemu_init_vcpu(cpu) | /softmmu/cpus.c | 直接函数调用,没什么可说的。该函数又通过函数指针的方式,调用create_vcpu_thread(cpu)。cpus_accel -> create_vcpu_thread(cpu) |
tcg_start_vcpu_thread(cpu) | /accel/tcg/tcg-cpus.c | tcg_cpus = {.create_vcpu_thread = tcg_start_vcpu_thread},函数指针赋值,因此实际调用的是tcg_start_vcpu_thread函数。该函数又调用qemu_thread_create函数创建线程 |
tcg_cpu_thread_fn(arg)、tcg_rr_cpu_thread_fn(arg) | /accel/tcg/tcg-cpus.c | qemu_thread_create函数创建的线程函数就是这两个,只不过对应的情况不同。前者是针对每个CPU都创建一个独立的线程,MTTCG后者是所有TCG的CPU共享一个线程。而这两个函数都会调用tcg_cpu_exec(cpu)函数,线程创建好,之后自然就是去调度执行了 |
tcg_cpu_exec(cpu) | /accel/tcg/tcg-cpus.c | |
cpu_exec(cpu) | /accel/tcg/cpu-exec.c | 开始进入翻译执行阶段了,该函数中使用两个while循环判断cpu无异常且无中断的情况下,会调用后续两个函数,寻找翻译块并翻译执行。 |
tb = tb_find(cpu, last_tb, tb_exit, cflags) | /accel/tcg/cpu-exec.c | 首先调用tb_lookup__cpu_state函数在cache中查找tb块,若没有找到,则调用tb_gen_code函数进行二进制动态翻译生成新的翻译块。最后调用tb_add_jump函数生成last_tb和tb之间的块链接。 |
cpu_loop_exec_tb(cpu, tb, &last_tb, &tb_exit) | /accel/tcg/cpu-exec.c | 调用trace_exec_tb和cpu_tb_exec函数执行一个翻译好的TB块。 |
tb_lookup__cpu_state() | /include/exec/tb-lookup.h | 根据pc获取一个hash值作为tb_jump_cache(缓存)中的索引查找是否存在已翻译的tb块。 |
rb=tb_gen_code() | /accel/tcg/translate-all.c | TranslationBlock的定义在/include/exec/exec-all.h文件中。该函数首先调用tcg_tb_alloc函数为将要翻译的TB块申请一块内存,并对TB结构体中的变量进行初始化。然后调用gen_intermediate_code(cpu,tb,max_insns)生成TCG中间表示;调用trace_translate_block(tb,tb->pc,tb->tc.ptr)函数……(找不到其定义);最后调用tcg_gen_code()由TCG中间表示生成宿主机指令,返回值为gen_code_size。 |
tb_add_jump() | 进行块链接 | |
gen_intermediate_code(cpu, tb, max_insns) | /target/xxx/translate.c | 不同的架构有不同的实现。该函数功能为,为基本块tb生成IR中间指令。其调用函数translator_loop针对基本块中的每条指令生成反汇编中间指令。 |
gen_code_size = tcg_gen_code(tcg_ctx, tb) | /tcg/tcg.c | switch case结构针对中间表示的每一个opc(操作码)翻译为对应宿主机的指令并保存。tcg_gen_code(TCGContext *s, TranslationBlock *tb),第一个参数就是TCG的中间表示指令集,将翻译好的指令保存到tb中。 |
translator_loop(ops,db,cpu,tb,max_insns) | /accel/tcg/translator.c | db是DisasContextBase,也就是记录反汇编中间指令的结构体变量。首先调用ops->init_disas_context()进行初始化;调用gen_tb_start(db->tb); ops->tb_start(db, cpu)开始进行翻译;在while(true)的循环中,调用ops->translate_insn(db, cpu)反汇编一条指令。translate_insn为函数指针。最后调用ops->tb_stop(db, cpu); gen_tb_end();退出tb块的翻译 |
translate_insn() | /target/xxx/translate.c | 该函数以函数指针的形式进行调用,在相应架构的translate.c文件中,将其赋值。如ARM架构将其赋值为以下:arm_tr_translate_insn或者thumb_tr_translate_insn函数。以arm_tr_translate_insn函数为例,将依次调用:arm_pre_translate_insn(dc),进行了一个判断,返回为布尔类型。貌似是检查将要翻译的指令是否在合适的地址范围以及是否单步???disas_arm_insn(dc, insn),cond为指令的条件,应该是根据代码中设定的翻译方式进行翻译,对ARM的每一个指令操作码。arm_post_translate_insn(dc),翻译完成后的检查工作。 |
Ps. 使用grep -rn '要找的函数或其他字符串' ./
命令可输出当前文件夹以及子文件夹下所有文件中包含目标字符串的行,对于寻找函数实现和调用位置比较有帮助。
用户模式(主要是Linux系统下)
函数调用图如下所示:
函数参数、所在文件、功能如下表:
函数 | 所在文件 | 函数功能 |
---|---|---|
main() | /linux-user/main.c | |
parse_args() | /main.c | 解析qemu的参数,qemu的命令行命令为:qemu-... [options] program [arguments ...],即qemu后加一些可选操作,最后加目标仿真二进制及其参数。定义在一个静态结构体arg_table中,包括-h/-g/-L等。该函数中定义一个qemu_argument类型的临时变量arginfo(与arg_table同种类型),用于遍历参数中所有的操作。将命令行中的设置的操作以及参数保存到arginfo中,并将目标仿真二进制保存到exec_path中。 |
tcg_exec_init(0) | /accel/tcg/translate-all.c | 该函数为初始化TCG,必须在使用QEMU CPU之前调用。初始化翻译前端、翻译后端、缓存和块链接。 |
loader_exec() | /linux-user/linuxload.c | 加载要执行的程序,即要执行的guest code。参数为:ret = loader_exec(execfd, exec_path, target_argv, target_environ, regs,info, &bprm);目标二进制文件句柄,目标二进制文件路径,目标二进制的参数,target_environ(这个和envlist有关),寄存器、仿真镜像的info、以及二进制的一些信息。首先使用参数对bprm结构体中的变量进行一个赋值,然后调用后续的几个函数完成对二进制的加载。 |
prepare_binprm(bprm) | /linux-user/linuxload.c | 声明了一个struct stat类型的变量st,这个结构体定义在#include <sys/stat.h>头文件中。从中获取了st_mode、st_uid、st_gid的值,应该就是检查文件类型和用户以及用户组的权限吧。然后调用read函数将fd对应的文件读取到bprm->buf中保存起来。 |
load_elf_binary(bprm, infop) | /linux-user/elfload.c | 检查仿真文件的magic number,如果是0x7f E L F的话,说明该文件为ELF格式,调用load_elf_binary函数进行加载。首先调用load_elf_image计算ELF文件LOAD段的地址范围以及合适的内存加载基地址,解析程序头表、符号表的信息。接着调用load_elf_interp函数加载解析interpreter文件,解释器变量为elf_interpreter,如果解释器为libc.so.1或者是ld.so.1时,消耗一个iBCS2的镜像,其他的解释器的话就消耗一个默认的linux镜像。调用create_elf_tables函数.....如果目标二进制存在解释器,那么程序的入口点以及加载偏移都需要设置为解释器的入口点和加载偏移。最后还有部分断点的相关处理。 |
qemu_log_mask() | /include/qemu/log.h | 以mask为粒度,以fmt为记录格式进行记录的函数,调用qemu_log函数进行日志输出。 |
qemu_loglevel_mask() | /include/qemu/log-for-trace.h | 该函数为将参数mask与qemu_loglevel进行相与,两者相等返回1,不等返回0. 该函数用于判断当前设置的日志记录粒度。日志粒度定义在/include/qemu/log.h中。 |
tcg_prologue_init(tcg_ctx) | /tcg/tcg.c | 初始化TCG序言,为动态二进制翻译做准备 |
cpu_loop(env) | /linux-user/xxx/cpu_loop.c | 将guest code翻译成host code执行,其中首先通过translate-all.c中的gen_intermediate_code将guest code翻译成TCG Operation,然后通过调用tcg/tcg.c 中的tcg_gen_code将TCG Operation 转成host code,执行 |
cpu_exec_start(cs)、cpu_exec_end(cs)、process_queued_cpu_work(cs) | /cpus-common.c | 根据CPU的执行状态、任务列表完成相应的操作 |
【函数指针】有助于理解QEMU的源码
void myFun(int);
void (*funP)(int);
void (*funA)(int);
1、其实,myFun的函数名与funP、funA函数指针都是一样的,即都是函数指针。myFun函数名是一个函数指针常量,而funP、funA是函数数指针变量,这是它们的关系。
2、但函数名调用如果都得如(*myFun)(10)这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许myFun(10)这种形式地调用(这样方便多了,并与数学中的函数形式一样)。
3、为了统一调用方式,funP函数指针变量也可以funP(10)的形式来调用。
4、赋值时,可以写成funP=&myFun形式,也可以写成funP=myFun。
5、但是在声明时,void myFun(int )不能写成void (*myFun)(int )。void (*funP)(int )不能写成void funP(int )。
6、函数指针变量也可以存入一个数组内。数组的声明方法:int (*fArray[10]) ( int );
((long REGPARM (*)(void *))code_gen_prologue)(tc_ptr) 就是一个函数调用,参数为tc_ptr,返回值为long类型。前边括号中的内容应该是强制类型转换。REGPARM是gcc汇编指令,表示使用寄存器传递参数,而不使用栈。
QEMU的helper机制
- 首先是一些参考链接
- 插桩之路——为QEMU TCG添加helper
以target/i386/translate.c
中的gen_helper_syscall
为例讲解了添加helper函数的步骤和方法;然后是TCG的定义机制,即gen_helper_xxx
和helper_xxx
函数的定义机制。 - QEMU: Call a Custom Function from TCG
以target/arm/translate.c
中的gen_helper_exception
为例讲解了helper函数的调用模式,包括helper函数参数的特殊变量类型以及创建helper函数的步骤,最后还给了一个创建helper函数的实例。——非常清晰 - 2021::QEMU::AFL++ and TCG
我们知道AFL++存在qemu模式,可以对无源码的二进制程序进行插桩获取覆盖率,而AFL++的二进制插桩功能就依赖于QEMU提供的helper机制。该篇文章中对afl_gen_trace、afl_maybe_log
等函数的实现进行了详细介绍。 - Qemu-TCG instruction emulation
该博客研究的是powerpc架构下的helper函数的应用,原理是一样的。以helper_popcntd
函数为例讲解了helper函数的作用、定义和调用过程。下图为helper函数的调用过程。
- QEMU and U: Whole-system tracing with QEMU customization
利用helper机制实现基本块的执行跟踪功能实例:使用bbmap保存执行的基本块信息(不关系路径,仅关心有哪些基本块被执行)、添加仿真器命令、转换物理地址(使用QEMU内置的一些功能快速开发跟踪工具,可方便获取目标二进制的执行信息,从而使我们的工作中心仍然放在寻找漏洞上面)
- QEMU的helper函数使用方法
helper函数的创建包括声明、定义和调用三个步骤。声明即在helper.h文件中声明(包括helper_和gen_helper_),定义即在op-helper.c等函数中实现其具体要插桩的功能代码(helper_),调用即在translation.c文件中(gen_helper_)。
下面以ARM指令集为例,添加一个每执行一条指令都获取一次指令信息功能的helper函数。-
/target/arm/helper.h文件,添加声明DEF_HELPER_2(obtain_info, void, env, tl),其中obtain_info是helper函数名,void是返回值,env,tl是参数类型,表示函数void helper_obtain_info(CPUARMState *env, target_ulong val)的声明。
- 在include/exec/helper-head.h文件中可以查看可选的返回值和参数的类型。返回值类型在dh_retvar_,参数类型看dh_ctype_。
- include/exec/helper-gen.h,include/exec/helper-proto.h以及include/exec/helper-head.h文件中包含gen_helper_和helper_的定义。
-
/target/arm/op_helper.c文件,实现helper_obtain_info函数。
- 如获取指令执行时间:qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL)
- 获取虚拟CPU寄存器值
- 获取指令机器码等等。
-
/target/arm/translate.c文件,gen_intermediate_code(cpu, tb, max_insns)函数为翻译前端,将guest code翻译为IR,调用translate_insn()函数对每一条指令进行翻译,在这个函数中的相应位置,插入gen_helper_obtain_info函数生成调用helper_obtain_info的TCG指令。
-
QEMU的一些运行选项
用户模式下
-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文件中有部分说明