一.前言
最近对于虚拟化技术在操作系统研究以及在二进制逆向/漏洞分析上的能力很感兴趣。看雪最近两年的sdc也都有议题和虚拟化相关(只不过出于性能的考虑用的是kvm而不是tcg):
2021年的是"基于Qemu/kvm硬件加速下一代安全对抗平台"
2022年的是"基于硬件虚拟化技术的新一代二进制分析利器"
跨平台模拟执行unicorn框架和qiling框架都是基于qemu的tcg,本文的内容就是描述一下qemu tcg与unicorn的原理。
TCG的英文含义是Tiny Code Generator, Qemu可以在没开启硬件虚拟化支持的时候实现全系统的虚拟化,Qemu结合下面几种技术共同实现虚拟化:
- soft tlb / Softmmu/内存模拟
- 虚拟中断控制器/中断模拟
- 总线/设备模拟
- TCG的CPU模拟
qemu进程代表着一个完整的虚拟机在运行,它没有特殊的权限却能正常的运行各种操作系统如windows/linux等,在没有硬件虚拟化支持的时候靠的最主要的角色就是TCG,它满足了Popek/Goldberg对于虚拟化的三大要求:
- 等价性
- 安全性
- 性能
8c8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2L8W2)9J5k6i4N6A6K9$3W2H3k6h3c8A6j5g2)9J5k6h3!0J5k6#2)9J5c8Y4N6A6K9$3W2Q4x3V1k6b7L8%4m8W2K9#2)9#2k6X3q4F1k6q4)9#2k6V1N6G2L8r3c8T1k6i4u0Y4i4K6g2X3N6X3W2J5N6s2g2S2L8r3W2*7j5i4c8A6L8$3&6Q4y4h3k6J5k6i4q4#2K9i4u0W2L8h3g2F1N6s2x3`.
后来出现了硬件支持的虚拟化,kvm因此成为主流,在云平台上运行的虚拟化都是有硬件支持的,但是TCG却仍然是不可替代的,因为硬件虚拟化只能在源ISA和目标ISA都相同的情况下才能工作(比如在x86平台虚拟化x86操作系统,或者在arm平台下虚拟化arm操作系统),而如果源ISA和目标ISA不同的情况下如在x86平台运行arm操作系统,只能靠TCG实现。
学习TCG的好处:
- 可以理解像libhoudini.so这样的转码技术是如何实现的
- 对理解应用层的虚拟机如java虚拟机中的jit技术很有帮助
- 可以帮助理解cpu包括整个计算机体系结构是如何工作的
- 可以帮助理解和定制二进制分析框架如unicorn/qiling,因为它们都是基于TCG
- 某些vmp是基于unicorn来实现的,理解TCG可以基于此实现自己的vmp/加深对vmp的理解
二. QEMU TCG
1. DBT
TCG本质上属于DBT,即dynamic binary translation动态二进制转换,相应的还有SBT,即static binary translation静态二进制转换。拿Android平台举例, SBT就相当于ART虚拟机中的AOT(ahead-of-time compilation),而DBT就相当于ART虚拟机中的JIT(Just-In-Time compilation)。
假如想在x86平台运行arm程序,称arm为source ISA, 而x86为target ISA, 在虚拟化的角度来说arm就是Guest, x86为Host。
最简单的解决方案是实现一个解释器,在一个循环中不断的读入arm程序指令,反汇编并用代码去模拟指令的执行。但是解释器的问题在于性能太低,后来就出来了DBT技术(QEMU也有解释器模块,具体搜索CONFIG_TCG_INTERPRETER),它也需要读入arm程序指令并进行反汇编,不过接下来流程会进入即时编译环节,将arm指令转换成x86指令,最终执行的时候会直接跳转到转换过的x86指令执行,得到媲美于本地执行的性能。
DBT和JIT这两个名词经常可以互换使用,不过我的理解是JIT环境中的输入是特意被设计过的可被模拟的指令格式(更多的是高层虚拟机如java虚拟机中的字节码),而DBT的输入则是不同平台的ISA指令。
对于虚拟化来说,可以采用SBT将Guest代码事先编译好然后直接运行吗? 对于模拟某些ISA如x86来说会遇到问题,因为x86的指令是不定长的,和反汇编器会遇到的问题一样,有时候是无法准确区分出哪些是数据哪些是指令,当遇到一些运行时才知道目标的跳转指令,SBT技术会遇到问题。这种问题被称为Code-Discovery Problem。
而DBT则不会有此问题,以下称source ISA中的pc指针为SPC, target ISA中的pc指针为TPC, 对于模拟一个arm系统来说,arm系统刚上电cpu会从物理地址0从开始执行,此时SPC=0, 假设此处的指令为"mov r0, #0", 而经过DBT转换以后,转换的代码位于Qemu进程的虚拟地址0x7fbdd0000100处,此时的TPC=0x7fbdd0000100, 转换后的指令为x86指令
"movl $0, (%rbp)",DBT技术中会实时记录SPC与TPC的关系,遇到跳转指令的时候可以得到跳转指令的目标地址,因此不会有SBT中的问题。
2.QEMU IR
类似于LLVM,QEMU也定义有自己的IR:
6bcK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2I4k6h3#2#2i4K6u0W2L8%4u0Y4i4K6u0r3k6r3!0U0M7#2)9J5c8X3#2S2M7%4c8W2M7W2)9J5c8X3c8W2N6X3g2D9i4K6u0r3N6r3y4Y4i4K6u0V1L8%4m8K6i4K6u0W2K9s2c8E0L8l9`.`.
转换过程如下:

引入IR的好处自然是当引入一种新的source ISA的时候,只需要完成source binary code到IR的转换,IR到target binary code直接用现成的即可。
从上面QEMU IR的链接中可知,QEMU IR的指令主要分为函数调用指令、跳转指令、算术指令、逻辑指令、条件移动指令、类型转换指令、加载/存储指令等构成,那么问题来了,仅靠固定的IR是无法模拟所有的ISA指令的,比如x86架构的cpuid指令并没有与之对应的IR,遇到这种指令如何生成对应的IR?
这就涉及到执行上下文的概念,QEMU本身是Host上一个普通的进程,运行在QEMU上下文,而执行转换后的目标代码则运行在虚拟机上下文,当运行在虚拟机上下文的程序遇到一些条件时会退出至QEMU上下文处理,像在arm平台执行cpuid指令就是这种情况,需要生成IR调用QEMU中的helper函数来模拟cpuid指令,模拟完了再回退到虚拟机上下文去执行。每个体系结构对应的helper函数在target/xxx/helper.h头文件中定义。
include/tcg/tcg-op.h文件声明了在实现一个生成IR的前端时可以调用的一些函数,这些函数以tcggen开头。
3.Basic Block/Translation Block
TCG的二进制转换是以块为基本单元,即Basic Block,当Guest指令遇到下面几种情况时会被分割成一个Basic Block:
- 遇到分支指令
- 遇到系统调用
- 达到页边界/最大长度限制
而TranslationBlock是QEMU中用来表示转换过的Host指令的数据结构(以下简称TB),执行时的基本控制流程如下:

QEMU TCG Engine运行在QEMU上下文,当一个Basic Block被转换成Tranlated Block以后,QEMU可以直接跳转过去以虚拟化上下文去执行,这种跳转是以函数调用的形式来实现的,因此还需要执行一些prologue"前言"代码来保存函数调用时的信息,需要切换回TCG上下文时需要执行一些epilogue"序言"代码来恢复函数调用前的信息。
拿x86_64平台举例,每次执行上下文切换需要执行大约20条指令(指令还会进行内存的读写),因此DBT的优化措施之一就是减少上下文切换,实现TB之间的直接链接,这种优化措施称为Direct block chaining:

这种优化措施可以显著的增加性能,但是这种优化方式还需要解决自修改代码引发的问题,在收到硬件中断时还需要快速的返回至QEMU上下文处理,等后面具体分析代码的时候会描述。
不过chained tb有一个限制: 两个chained tb对应的guest指令需要在同一个guest的page里。
将指令分割为Basic Block的一个主要原因是TB的缓存机制,当一个Basic Block被DBT转换为TB以后,下次再执行到相同的Basic Block直接从缓存中获取TB执行即可,无需再经过转换:

4.代码环境
以x86_64平台上运行一段arm程序做为研究对象,使用的QEMU源码分支为stable-8.0。
需要准备三个文件startup.s, test.c,test.ld:
startup.s文件内容:
1
2
3
4
5
|
. global _Reset
_Reset:
LDR sp, = stack_top
BL c_entry
B .
|
test.c文件内容:
1
2
3
4
5
6
7
8
9
10
|
volatile unsigned int * const UART0DR = (unsigned int * ) 0x101f1000 ;
void print_uart0(const char * s) {
while ( * s ! = '\0' ) { / * Loop until end of string * /
* UART0DR = (unsigned int )( * s); / * Transmit char * /
s + + ; / * Next char * /
}
}
void c_entry() {
print_uart0( "Hello world!\n" );
}
|
test.ld文件内容:
1
2
3
4
5
6
7
8
9
10
11
12
|
ENTRY(_Reset)
SECTIONS
{
. = 0x10000 ;
.startup . : { startup.o(.text) }
.text : { * (.text) }
.data : { * (.data) }
.bss : { * (.bss COMMON) }
. = ALIGN( 8 );
. = . + 0x1000 ; / * 4kB of stack memory * /
stack_top = .;
}
|
编译:
1
2
3
4
|
arm - none - eabi - gcc - c - mcpu = arm926ej - s - g test.c - o test.o
arm - none - eabi - as - mcpu = arm926ej - s - g startup.s - o startup.o
arm - none - eabi - ld - T test.ld test.o startup.o - o test.elf
arm - none - eabi - objcopy - O binary test.elf test. bin
|
以上代码的用途是往串口0x101f1000处写入Hello World,代码的链接地址为0x10000,它期望被加载到物理内存的地址也是0x10000,很多arm机器将内核加载至此。
启动:
1 |
qemu - system - arm - M versatilepb - m 128 - kernel test. bin - nographic
|
会在屏幕上打印出Hello World!,此时退出QEMU的快捷键为Ctrl+A X
qemu-system-arm程序是由QEMU源码编译出来的,-M versatilepb表示模拟的arm硬件为versatilepb(Arm Versatile boards),-m 128参数表示指定的机器内存为128M, -kernel参数为QEMU的Direct Linux Boot机制,由QEMU而不是磁盘上的Bootloade来将内核加载至内存。这种情况下启动,arm会从物理地址0开始执行,事实上0地址处是qemu实现的一小段bootloader,只是用来将控制跳转到0x10000内核处执行(test.bin),代码在hw/arm/boot.c文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/ * A very small bootloader: call the board - setup code ( if needed),
* set r0 - r2, then jump to the kernel.
* If we 're not calling boot setup code then we don' t copy across
* the first BOOTLOADER_NO_BOARD_SETUP_OFFSET insns in this array.
* /
static const ARMInsnFixup bootloader[] = {
{ 0xe28fe004 }, / * add lr, pc,
{ 0xe51ff004 }, / * ldr pc, [pc,
{ 0 , FIXUP_BOARD_SETUP },
{ 0xe3a00000 }, / * mov r0,
{ 0xe59f1004 }, / * ldr r1, [pc,
{ 0xe59f2004 }, / * ldr r2, [pc,
{ 0xe59ff004 }, / * ldr pc, [pc,
{ 0 , FIXUP_BOARDID },
{ 0 , FIXUP_ARGPTR_LO },
{ 0 , FIXUP_ENTRYPOINT_LO },
{ 0 , FIXUP_TERMINATOR }
};
|
5.打印出TCG转换的文件
从qemu 7.1开始反汇编引擎已经替换为Capstone,因此需要安装capstone:
1 |
sudo apt install libcapstone - dev
|
qemu提供了一些调试手段可以显示出TCG转换过程的内容:
1
2
3
4
5
|
qemu - system - arm - M versatilepb - m 128 - kernel test. bin - nographic - d in_asm - D in_asm.txt
qemu - system - arm - M versatilepb - m 128 - kernel test. bin - nographic - d op - D op.txt
qemu - system - arm - M versatilepb - m 128 - kernel test. bin - nographic - d out_asm - D out_asm.txt
|
in_asm.txt为arm反汇编程序的结果
op.txt为生成的IR指令的内容
out_asm为转换后的Host指令的内容
分析TCG的时候,由于它拥有全系统虚拟化的能力,因此需要思考如下几种情况是如何实现的:
- 普通算术逻辑运算指令如何更新Host体系结构相关寄存器
- 内存读写如何处理
- 分支指令(条件跳转、非条件跳转、返回指令)
- 目标机器没有的指令、特权指令、敏感指令
- 非普通内存读写如设备寄存器访问MMIO
- 指令执行出现了同步异常如何处理(如系统调用)
- 硬件中断如何处理
6.TCG相关数据结构
qemu中一个tcg线程可以模拟多个vcpu,也可以多个tcg线程每个对应模拟一个vcpu,后者称为Multi-Threaded TCG (MTTCG),是否为MTTCG由全局变量bool mttcg_enabled决定。对于此处的示例MTTCG是开启状态,不过简单起见这里假设机器只有一个vcpu。
先来看一下TCG的一些重要数据结构:
-
TranslationBlock:
顾名思义,存放编译后的TB相关信息,包括指向目标机器执行码的指针
-
CPUArchState:
由于是模拟的cpu,因此存放着体系结构的cpu信息,比如对于arm平台它定义在target/arm/cpu.h文件中,成员包括所有通用寄存器以及状态码等模拟cpu硬件所必需的信息。
- TCGContext:
存放tcg中间存储数据的结构体,包括转换后的IR, tcg的核心就是围绕此结构展开,前端IR以TCGOp列表的形式存放在TCGContext的ops对象中
- TCGTemp:
对应于tcg IR中的变量,存放在TCGContext的temps数组中,变量有几种不同的作用域类型
tcg_temp_new_internal
分配TEMP_EBB, TEMP_TB
类型的TCGTemp变量
tcg_global_alloc
分配TEMP_GLOBAL
类型的TCGTemp变量
tcg_global_reg_new_internal
分配TEMP_FIXED
类型的TCGTemp变量
tcg_constant_internal
分配TEMP_CONST
类型的TCGTemp变量
TCPTemp每个类型的含义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
typedef enum TCGTempKind {
/ *
* Temp is dead at the end of the extended basic block (EBB),
* the single - entry multiple - exit region that falls through
* conditional branches.
* /
TEMP_EBB,
/ * Temp is live across the entire translation block, but dead at end. * /
TEMP_TB,
/ * Temp is live across the entire translation block, and between them. * /
TEMP_GLOBAL,
/ * Temp is in a fixed register. * /
TEMP_FIXED,
/ * Temp is a fixed constant. * /
TEMP_CONST,
} TCGTempKind;
|
那么编译后的Host代码存放在哪里?先看一下这幅图:

在qemu启动的早期会执行一个函数叫tcg_init_machine
在这个函数中会调用qemu_memfd_create()
函数创建出一个匿名文件,该匿名文件的大小是根据当前Host机器的物理内存计算出来的,比如我的电脑是64G,最终计算出来的匿名文件大小为1G。
然后对匿名文件做两次映射,一次映射为读写:(PROT_READ | PROT_WRITE)
,称之为buf_rw。
一次映射为写执行(PROT_READ | PROT_EXEC)
,称之为buf_rx, buf_rw和buf_rx之间的差值由全局变量tcg_splitwx_diff
表示。
tcg在翻译代码的过程中会利用buf_rw写这1G的空间,而执行的过程中则依赖于buf_rx在这1G的空间中执行代码。由于buf_rw和buf_rx映射的是同一个文件且指定了MAP_SHARED参数,因此对buf_rw做出的修改会在buf_rx的空间可见。
tcg_init_machine
函数还会调用tcg_target_qemu_prologue
函数创建出对应于Host的prologue和epilogue,并且分别由全局变量tcg_qemu_tb_exec
和tcg_code_gen_epilogue
指向(如上图)。
对于Host为x86_64来说,它的prologue如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/ / 保存callee需要保存的寄存器
0x7fffac000000 : 55 pushq % rbp
0x7fffac000001 : 53 pushq % rbx
0x7fffac000002 : 41 54 pushq % r12
0x7fffac000004 : 41 55 pushq % r13
0x7fffac000006 : 41 56 pushq % r14
0x7fffac000008 : 41 57 pushq % r15
/ / 第一个参数赋值给 % rbp
0x7fffac00000a : 48 8b ef movq % rdi, % rbp
/ / 预留栈空间
0x7fffac00000d : 48 81 c4 78 fb ff ff addq $ - 0x488 , % rsp
/ / 跳转到第二个参数地址处执行,第二个参数即为TranslationBlock.tc.ptr
0x7fffac000014 : ff e6 jmpq * % rsi
|
它的epilogue如下:
1
2
3
4
5
6
7
8
9
10
11
|
/ / 恢复栈空间及callee需要保存的寄存器
0x7fffac000016 : 33 c0 xorl % eax, % eax
0x7fffac000018 : 48 81 c4 88 04 00 00 addq $ 0x488 , % rsp
0x7fffac00001f : c5 f8 77 vzeroupper
0x7fffac000022 : 41 5f popq % r15
0x7fffac000024 : 41 5e popq % r14
0x7fffac000026 : 41 5d popq % r13
0x7fffac000028 : 41 5c popq % r12
0x7fffac00002a : 5b popq % rbx
0x7fffac00002b : 5d popq % rbp
0x7fffac00002c : c3 retq
|
假设现在正在翻译第一个TB,TranslationBlock
结构也是在1G的空间内分配,第一个TB紧接着epilogue,并且分配了TB以后TCGContext的code_gen_ptr将会指向TB的末端,该TB对应的Host机器码地址存放在TranslationBlock.tc.ptr
中,属于buf_rx空间。
而buf_rw空间中TB对应的Host机器码的开头由TCGContext的code_buf指向,末端由TCGContext的code_ptr指向,两者之差则为机器码的长度。需要翻译第二个TB时,第二个TranslationBlock结构则会在TCGContext.code_ptr
的后面再分配,TCGContext的code_buf和code_ptr则再指向第二个TB对应的Host机器码的开头和末端,此时TCGContext的code_gen_ptr则再更新为第二个TB末端的位置。
如何执行编译后的代码?直接执行tcg_qemu_tb_exec()
函数即可,该函数接受两个参数,第一个参数为CPUArchState
,第二个参数为TranslationBlock.tc.ptr
,因此TB执行逻辑为:
- prologue
- TranslationBlock.tc.ptr
- epilogue
如果做了Direct block chaining优化则不会再有epilogue,会跳转到下一个TB执行。
7.tcg执行流程
tcg线程始于mttcg_cpu_thread_fn
,执行流程为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
mttcg_cpu_thread_fn:
do{
if (cpu_can_run(cpu)) {
...
tcg_cpus_exec(cpu)
cpu_exec_start(cpu)
cpu_exec(cpu)
cpu_exec_enter(cpu)
cpu_exec_setjmp(cpu, &sc)
sigsetjmp(cpu - >jmp_env, 0 ) / / 设置同步异常退出点
cpu_exec_loop(cpu, sc)
cpu_exec_exit(cpu)
cpu_exec_end(cpu)
...
}
} while (!cpu - >unplug || cpu_can_run(cpu));
|
主要执行函数在cpu_exec_loop
中,它的执行过程为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
cpu_exec_loop:
while (!cpu_handle_exception(cpu, &ret)) { / / 处理同步异常
while (!cpu_handle_interrupt(cpu, &last_tb)) { / / 处理异步中断
cpu_get_tb_cpu_state()
tb = tb_lookup() / / 查找tb缓存
if (tb = = NULL) {
tb = tb_gen_code() / / 进行dbt转换
setjmp_gen_code()
gen_intermediate_code() / / 将Guest代码转换为IR
tcg_gen_code() / / 根据IR生成Host代码
}
tb_add_jump() / / Direct block chaining优化
cpu_loop_exec_tb() / / 执行Host目标代码
}
}
|
tcg_gen_code在生成Host代码之前还会基于当前的IR做一些优化。优化函数有tcg_optimize, reachable_code_pass,liveness_pass_0,liveness_pass_1,liveness_pass_2
等。
8.普通算术逻辑运算指令如何更新Host体系结构相关寄存器
对于一条Guest指令来说,tcg的处理是将它翻译为语义等价的多条IR(称为微码),比如
in_asm.txt
文件中显示出0地址处的arm指令为:
1 |
0x00000000 : e3a00000 mov r0,
|
它编译为微码IR的结果为:
1
2
3
|
- - - - 00000000 00000000 00000000
mov_i32 loc5,$ 0x0 / / 0x0 赋值给loc5变量
mov_i32 r0,loc5 / / loc5再赋值给r0
|
loc5这种变量为tcg的TCGTemp,而r0则对应着arm的r0寄存器,因此tcg的IR其实并非和llvm中的IR那样和平台完全无关,它是和平台相关的。
这种翻译方式的优点是可以避免处理不同指令集的复杂性,但是缺点是以性能为代价(通常减慢 5-10 倍)。
再来看一下这条指令:
1 |
0x00000004 : e59f1004 ldr r1, [pc,
|
它对应的IR:
1
2
3
4
5
|
- - - - 00000004 00000000 00000e04
add_i32 loc6,pc,$ 0x10 / / loc6 = pc + 0x10
mov_i32 loc9,loc6 / / loc9 = loc6
qemu_ld_i32 loc8,loc9,leul, 2 / / loc9处的内存加载至loc8变量,leul的含义为Little Endian unsigned long
mov_i32 r1,loc8 / / loc8赋值给r1寄存器
|
因此通过组合微码以及结合qemu的helper函数,可以将Guest的所有指令都编译为语义等价的IR,在微码的基础上进行一些优化以后再根据微码一条一条的翻译成Host指令。
DBT需要解决的一个问题是如何进行state mapping状态绑定,拿0x00000000处的指令举例,这条指令将r0寄存器的值赋给0,当执行完编译过的Host指令以后,需要相应的在某个状态中记录下r0寄存器值为0,如果Host的寄存器数量很多,完全可以选一个x86_64寄存器作为arm中r0寄存器的对应物(寄存器绑定),否则就需要保存在内存中了。
对于tcg来说有个特殊的寄存器叫TCG_AREG0
,它表示用哪个Host寄存器来指向Guest体系结构的CPUArchState
,对于x86_64来说TCG_AREG0为%rbp(对于arm来说TCG_AREG0为r6寄存器),也就是说通过rbp寄存器可以找到arm的CPUArchState
。qemu中有专门的TEMP_FIXED类型的TCGTemp用于表示TCG_AREG0
:
1
2
|
ts = tcg_global_reg_new_internal(s, TCG_TYPE_PTR, TCG_AREG0, "env" );
cpu_env = temp_tcgv_ptr(ts);
|
cpu_env在IR中被使用的话,在生成Host代码阶段对于x86_64来说将会绑定到rbp寄存器。
事实上qemu中一共只有两个TEMP_FIXED类型的TCGTemp,一个叫env,一个叫_frame。
来看一下CPUArchState的通用寄存器成员为
因此arm指令
1 |
0x00000000 : e3a00000 mov r0,
|
对应编译过的x86_64代码如下:
1 |
movl $ 0 , 0 ( % rbp) / / rbp指向CPUArchState,更新arm CPUArchState的regs[ 0 ]即r0寄存器
|
同样的对于这条arm指令它最终改变了r2:
对应编译过的x86_64代码如下:
1 |
movl % r12d, 8 ( % rbp) / / rbp指向CPUArchState,更新arm CPUArchState的regs[ 2 ]即r2寄存器
|
当然如果在一个TB内如果每遇到一个指令改变了寄存器都要写入x86_64的rbp对应的内存地址是非常慢的,tcg有个优化措施是可以在TB结束之前只执行一次更新操作从而减少写内存的操作。
因此通过TCG_AREG0
寄存器,x86_64指令在执行的时候可以找到CPUArchState结构从而更新所有Guest体系结构的CPU状态。
9. 内存读写如何处理
对于qemu来说读写内存涉及到内存模拟模块,qemu还模拟了tlb,因此读写一块arm的虚拟内存地址(Guest Virtual Address -> GVA)首先会查询tlb,如果tlb不命中的话会走tlb慢路径。tlb慢路径要经由guest的mmu经页表转换为物理内存地址(Guest Physics Address -> GPA),再经过qemu内存管理模块转换为qemu进程的虚拟地址(Host Virtual Address -> HVA)。
那么读写GVA的arm指令编译成X86_64指令就是读写对应的HVA即可。
tlb相应的数据结构在include/exec/cpu-defs.h
文件中定义,其中结构体CPUTLB由ArchCPU中的CPUNegativeOffsetState neg
所引用。
TLB命中时对应CPUTLBEntry对象的addend + GVA = HVA。
CPUTLBEntry对象的addr_read, addr_write, addr_code
分别对应着读写执行指令的地址,地址的构成部分注释中有描述:
1
2
3
4
5
6
|
/ * bit TARGET_LONG_BITS to TARGET_PAGE_BITS : virtual address
bit TARGET_PAGE_BITS - 1. . 4 : Nonzero for accesses that should not
go directly to ram.
bit 3 : indicates that the entry is invalid
bit 2. . 0 : zero
* /
|
以如下指令举例:
1 |
0x00000004 : e59f1004 ldr r1, [pc,
|
它对应的IR为:
1
2
3
4
5
|
- - - - 00000004 00000000 00000e04
add_i32 loc6,pc,$ 0x10
mov_i32 loc9,loc6
qemu_ld_i32 loc8,loc9,leul, 2
mov_i32 r1,loc8
|
上面最主要的是qemu_ld_i32这条IR,loc9的值为GVA,qemu_ld_i32则将loc9地址处的内存加载至loc8变量中并最终赋值给r1寄存器。
qemu_ld_i32这条IR它对应的x86_64代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
- - guest addr 0x00000004
0x7ff9c0000119 : 41 8b fc movl % r12d, % edi
0x7ff9c000011c : c1 ef 05 shrl $ 5 , % edi
0x7ff9c000011f : 23 bd 10 ff ff ff andl - 0xf0 ( % rbp), % edi
0x7ff9c0000125 : 48 03 bd 18 ff ff ff addq - 0xe8 ( % rbp), % rdi
0x7ff9c000012c : 41 8d 74 24 03 leal 3 ( % r12), % esi
0x7ff9c0000131 : 81 e6 00 fc ff ff andl $ 0xfffffc00 , % esi
0x7ff9c0000137 : 3b 37 cmpl 0 ( % rdi), % esi
0x7ff9c0000139 : 41 8b f4 movl % r12d, % esi
0x7ff9c000013c : 0f 85 9c 00 00 00 jne 0x7ff9c00001de
0x7ff9c0000142 : 48 03 77 10 addq 0x10 ( % rdi), % rsi
0x7ff9c0000146 : 44 8b 26 movl 0 ( % rsi), % r12d
|
乍一看相当复杂的不知道在做什么,其实上面执行的逻辑是创建出调用qemu tlb的环境,先去tlb查询是否有对应的HVA,如果没有的话会生成一段tlb slow path的代码并跳转到tlb slow path去执行。生成这段x86_64的代码位于tcg/i386/tcg-target.c.inc
文件的tcg_out_qemu_ld
函数。
一条条来解释:
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
|
- - guest addr 0x00000004
/ / r12寄存器包含着要读取的地址的低位部分addrlo(这里要读取的地址为 0x10 ),赋值给edi,edi为x86平台函数调用的第一个参数寄存器
0x7ff9c0000119 : 41 8b fc movl % r12d, % edi
/ / 地址 >> (TARGET_PAGE_BITS - CPU_TLB_ENTRY_BITS) = 5
0x7ff9c000011c : c1 ef 05 shrl $ 5 , % edi
/ / - 0xf0 为偏移量,rbp为CPUArchState, - 0xf0 分为两部计算,首先获取neg.tlb.f[IDX]在CPUArchState中的偏移,再获取CPUTLBDescFast结构中mask成员的偏移, 因此 - 0xf0 就为CPUTLBDescFast结构中mask成员的偏移,因此这条指令等于是执行了一个函数叫tlb_index(CPUArchState * env, uintptr_t mmu_idx,target_ulong addr)
0x7ff9c000011f : 23 bd 10 ff ff ff andl - 0xf0 ( % rbp), % edi
/ / - 0xe8 为CPUTLBDescFast结构中的table成员的偏移,因此这条指令等于是执行了一个函数叫tlb_entry(CPUArchState * env, uintptr_t mmu_idx,target_ulong addr)
0x7ff9c0000125 : 48 03 bd 18 ff ff ff addq - 0xe8 ( % rbp), % rdi
/ / addrlo + (s_mask - a_mask)赋值给 % esi, esi为x86平台函数调用的第二个参数寄存器
0x7ff9c000012c : 41 8d 74 24 03 leal 3 ( % r12), % esi
/ / 地址 & (TARGET_PAGE_MASK | a_mask)这样提取出地址的除了页偏移的其他部分
0x7ff9c0000131 : 81 e6 00 fc ff ff andl $ 0xfffffc00 , % esi
/ / 0 ( % rdi)的值为对应CPUTLBEntry的addr_read成员变量的值,和要取的地址进行比较
0x7ff9c0000137 : 3b 37 cmpl 0 ( % rdi), % esi
/ / 原始地址赋值给 % esi
0x7ff9c0000139 : 41 8b f4 movl % r12d, % esi
/ / 如果CPUTLBEntry的addr_read成员变量的值和要取的地址不相等则表示tlb不命中,跳转至tlb慢路径地址 0x7ff9c00001de 处执行
0x7ff9c000013c : 0f 85 9c 00 00 00 jne 0x7ff9c00001de
/ / 如果没有进入tlb慢路径表示tlb命中, 0x10 ( % rdi)的值为CPUTLBEntry的addend成员变量的值,加上原始地址即为HVA
0x7ff9c0000142 : 48 03 77 10 addq 0x10 ( % rdi), % rsi
/ / 读取HVA地址处的值并赋值给 % r12d
0x7ff9c0000146 : 44 8b 26 movl 0 ( % rsi), % r12d
|
生成tlb slow path的代码在tcg/tcg.c文件的tcg_gen_code函数中的:
1
2
3
4
5
6
7
|
/ * Generate TB finalization at the end of block * /
i = tcg_out_ldst_finalize(s);
if (i < 0 ) {
return i;
}
|
[注意]看雪招聘,专注安全领域的专业人才平台!
最后于 2023-5-11 10:54
被飞翔的猫咪编辑
,原因: