首页
社区
课程
招聘
QEMU tcg源码分析与unicorn原理
发表于: 2023-5-10 17:19 34752

QEMU tcg源码分析与unicorn原理

2023-5-10 17:19
34752

一.前言

最近对于虚拟化技术在操作系统研究以及在二进制逆向/漏洞分析上的能力很感兴趣。看雪最近两年的sdc也都有议题和虚拟化相关(只不过出于性能的考虑用的是kvm而不是tcg):
2021年的是"基于Qemu/kvm硬件加速下一代安全对抗平台"
2022年的是"基于硬件虚拟化技术的新一代二进制分析利器"

 

跨平台模拟执行unicorn框架和qiling框架都是基于qemu的tcg,本文的内容就是描述一下qemu tcg与unicorn的原理。

 

TCG的英文含义是Tiny Code Generator, Qemu可以在没开启硬件虚拟化支持的时候实现全系统的虚拟化,Qemu结合下面几种技术共同实现虚拟化:

  1. soft tlb / Softmmu/内存模拟
  2. 虚拟中断控制器/中断模拟
  3. 总线/设备模拟
  4. TCG的CPU模拟

qemu进程代表着一个完整的虚拟机在运行,它没有特殊的权限却能正常的运行各种操作系统如windows/linux等,在没有硬件虚拟化支持的时候靠的最主要的角色就是TCG,它满足了Popek/Goldberg对于虚拟化的三大要求:

  1. 等价性
  2. 安全性
  3. 性能

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的好处:

  1. 可以理解像libhoudini.so这样的转码技术是如何实现的
  2. 对理解应用层的虚拟机如java虚拟机中的jit技术很有帮助
  3. 可以帮助理解cpu包括整个计算机体系结构是如何工作的
  4. 可以帮助理解和定制二进制分析框架如unicorn/qiling,因为它们都是基于TCG
  5. 某些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:

  1. 遇到分支指令
  2. 遇到系统调用
  3. 达到页边界/最大长度限制

而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, #4 */
    { 0xe51ff004 }, /* ldr     pc, [pc, #-4] */
    { 0, FIXUP_BOARD_SETUP },
#define BOOTLOADER_NO_BOARD_SETUP_OFFSET 3
    { 0xe3a00000 }, /* mov     r0, #0 */
    { 0xe59f1004 }, /* ldr     r1, [pc, #4] */
    { 0xe59f2004 }, /* ldr     r2, [pc, #4] */
    { 0xe59ff004 }, /* ldr     pc, [pc, #4] */
    { 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的时候,由于它拥有全系统虚拟化的能力,因此需要思考如下几种情况是如何实现的:

  1. 普通算术逻辑运算指令如何更新Host体系结构相关寄存器
  2. 内存读写如何处理
  3. 分支指令(条件跳转、非条件跳转、返回指令)
  4. 目标机器没有的指令、特权指令、敏感指令
  5. 非普通内存读写如设备寄存器访问MMIO
  6. 指令执行出现了同步异常如何处理(如系统调用)
  7. 硬件中断如何处理

6.TCG相关数据结构

qemu中一个tcg线程可以模拟多个vcpu,也可以多个tcg线程每个对应模拟一个vcpu,后者称为Multi-Threaded TCG (MTTCG),是否为MTTCG由全局变量bool mttcg_enabled决定。对于此处的示例MTTCG是开启状态,不过简单起见这里假设机器只有一个vcpu。

 

先来看一下TCG的一些重要数据结构:

  1. TranslationBlock:
    顾名思义,存放编译后的TB相关信息,包括指向目标机器执行码的指针

  2. CPUArchState:
    由于是模拟的cpu,因此存放着体系结构的cpu信息,比如对于arm平台它定义在target/arm/cpu.h文件中,成员包括所有通用寄存器以及状态码等模拟cpu硬件所必需的信息。

  3. TCGContext:
    存放tcg中间存储数据的结构体,包括转换后的IR, tcg的核心就是围绕此结构展开,前端IR以TCGOp列表的形式存放在TCGContext的ops对象中
  4. 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_exectcg_code_gen_epilogue指向(如上图)。

 

对于Host为x86_64来说,它的prologue如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//保存callee需要保存的寄存器
0x7fffac00000055                       pushq    %rbp
0x7fffac00000153                       pushq    %rbx
0x7fffac00000241 54                    pushq    %r12
0x7fffac00000441 55                    pushq    %r13
0x7fffac00000641 56                    pushq    %r14
0x7fffac00000841 57                    pushq    %r15
 
//第一个参数赋值给%rbp
0x7fffac00000a48 8b ef                 movq     %rdi, %rbp
 
//预留栈空间
0x7fffac00000d48 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需要保存的寄存器
0x7fffac00001633 c0                    xorl     %eax, %eax
0x7fffac00001848 81 c4 88 04 00 00     addq     $0x488, %rsp
0x7fffac00001f:  c5 f8 77                 vzeroupper
0x7fffac00002241 5f                    popq     %r15
0x7fffac00002441 5e                    popq     %r14
0x7fffac00002641 5d                    popq     %r13
0x7fffac00002841 5c                    popq     %r12
0x7fffac00002a5b                       popq     %rbx
0x7fffac00002b5d                       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执行逻辑为:

  1. prologue
  2. TranslationBlock.tc.ptr
  3. 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, #0

它编译为微码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, #4]

它对应的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的通用寄存器成员为

1
uint32_t regs[16];

因此arm指令

1
0x00000000:  e3a00000  mov      r0, #0

对应编译过的x86_64代码如下:

1
movl     $0, 0(%rbp)  //rbp指向CPUArchState,更新arm CPUArchState的regs[0]即r0寄存器

同样的对于这条arm指令它最终改变了r2:

1
ldr      r2, [pc, #4]

对应编译过的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, #4]

它对应的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
0x7ff9c000011941 8b fc                 movl     %r12d, %edi
0x7ff9c000011c:  c1 ef 05                 shrl     $5, %edi
0x7ff9c000011f23 bd 10 ff ff ff        andl     -0xf0(%rbp), %edi
0x7ff9c000012548 03 bd 18 ff ff ff     addq     -0xe8(%rbp), %rdi
0x7ff9c000012c41 8d 74 24 03           leal     3(%r12), %esi
0x7ff9c000013181 e6 00 fc ff ff        andl     $0xfffffc00, %esi
0x7ff9c00001373b 37                    cmpl     0(%rdi), %esi
0x7ff9c000013941 8b f4                 movl     %r12d, %esi
0x7ff9c000013c0f 85 9c 00 00 00        jne      0x7ff9c00001de
0x7ff9c000014248 03 77 10              addq     0x10(%rdi), %rsi
0x7ff9c000014644 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平台函数调用的第一个参数寄存器
0x7ff9c000011941 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)
0x7ff9c000011f23 bd 10 ff ff ff        andl     -0xf0(%rbp), %edi
 
//-0xe8为CPUTLBDescFast结构中的table成员的偏移,因此这条指令等于是执行了一个函数叫tlb_entry(CPUArchState *env, uintptr_t mmu_idx,target_ulong addr)
0x7ff9c000012548 03 bd 18 ff ff ff     addq     -0xe8(%rbp), %rdi
 
//addrlo + (s_mask - a_mask)赋值给%esi, esi为x86平台函数调用的第二个参数寄存器
0x7ff9c000012c41 8d 74 24 03           leal     3(%r12), %esi
 
//地址 & (TARGET_PAGE_MASK | a_mask)这样提取出地址的除了页偏移的其他部分
0x7ff9c000013181 e6 00 fc ff ff        andl     $0xfffffc00, %esi
 
//0(%rdi)的值为对应CPUTLBEntry的addr_read成员变量的值,和要取的地址进行比较
0x7ff9c00001373b 37                    cmpl     0(%rdi), %esi
 
//原始地址赋值给%esi
0x7ff9c000013941 8b f4                 movl     %r12d, %esi
 
//如果CPUTLBEntry的addr_read成员变量的值和要取的地址不相等则表示tlb不命中,跳转至tlb慢路径地址0x7ff9c00001de处执行
0x7ff9c000013c0f 85 9c 00 00 00        jne      0x7ff9c00001de
 
//如果没有进入tlb慢路径表示tlb命中,0x10(%rdi)的值为CPUTLBEntry的addend成员变量的值,加上原始地址即为HVA
0x7ff9c000014248 03 77 10              addq     0x10(%rdi), %rsi
 
//读取HVA地址处的值并赋值给%r12d
0x7ff9c000014644 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 */
#ifdef TCG_TARGET_NEED_LDST_LABELS
    i = tcg_out_ldst_finalize(s);
    if (i < 0) {
        return i;
 
    }

[注意]看雪招聘,专注安全领域的专业人才平台!

最后于 2023-5-11 10:54 被飞翔的猫咪编辑 ,原因:
收藏
免费 29
支持
分享
打赏 + 10.00雪花
打赏次数 1 雪花 + 10.00
 
赞赏  IamHuskar   +10.00 2023/05/11 精品文章~
最新回复 (22)
雪    币: 3110
活跃值: (4257)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql
2023-5-10 17:28
0
雪    币: 390
活跃值: (2703)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
看不懂,但大受震撼
2023-5-10 17:53
1
雪    币: 15031
活跃值: (18266)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2023-5-10 18:01
0
雪    币: 2258
活跃值: (4826)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
5

学到了,这里分享一个工具,用来对 qemu 的 log 进行可视化,方便各位学习 Guest code,Tiny code ,Host code 间的对应关系
a22K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1j5H3x3f1y4Z5k6h3&6c8K9h3&6Y4i4K6u0r3f1h3g2E0N6g2)9J5k6s2c8U0k6#2)9J5k6r3I4G2k6#2)9J5k6s2k6A6k6i4N6W2M7R3`.`.

当时随手写的可能有bug,有胜于无

最后于 2023-5-10 19:40 被小白养的菜鸡编辑 ,原因:
2023-5-10 19:37
1
雪    币: 288
活跃值: (782)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
mark
2023-5-10 19:39
0
雪    币: 1143
活跃值: (3131)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
7
感谢分享,太详细了
2023-5-11 09:30
0
雪    币: 2310
活跃值: (1621)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
8
mark
2023-5-11 10:15
0
雪    币: 1372
活跃值: (5677)
能力值: ( LV13,RANK:240 )
在线值:
发帖
回帖
粉丝
9
楼主出一些续集吧。
2023-5-11 14:58
0
雪    币: 5143
活跃值: (6954)
能力值: ( LV12,RANK:200 )
在线值:
发帖
回帖
粉丝
10
IamHuskar 楼主出一些续集吧。
你不都要告别逆向了吗
2023-5-11 15:03
0
雪    币: 2122
活跃值: (4279)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
11
2023-5-11 15:39
0
雪    币: 1372
活跃值: (5677)
能力值: ( LV13,RANK:240 )
在线值:
发帖
回帖
粉丝
12
文章很好。建议分系列 。楼主应该限于篇幅还有很多没有显示出来。
2023-5-11 15:41
0
雪    币: 1372
活跃值: (5677)
能力值: ( LV13,RANK:240 )
在线值:
发帖
回帖
粉丝
13
飞翔的猫咪 你不都要告别逆向了吗[em_48]
这是正向哈,告别逆向不一定不玩正向啊。
2023-5-11 16:00
0
雪    币: 180
活跃值: (3851)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
14
mark
2023-5-11 17:11
0
雪    币: 477
活跃值: (1412)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
tql
2023-5-11 17:30
0
雪    币: 2581
活跃值: (13169)
能力值: ( LV12,RANK:312 )
在线值:
发帖
回帖
粉丝
16
Unicorn好东西,还可以做轻量级脱壳引擎和沙箱引擎,但是API处理数量太庞大了。
2023-5-12 08:20
0
雪    币: 4033
活跃值: (31446)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
mark
2023-5-12 09:07
1
雪    币: 48
活跃值: (3625)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
tql
2023-5-12 15:18
0
雪    币: 48
活跃值: (3625)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
楼主出一些续集吧。
2023-5-12 15:19
0
雪    币: 3929
活跃值: (4197)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
20
感谢分享!
2023-5-12 15:31
0
雪    币: 1511
活跃值: (403)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
21
感谢分享,把qemu讲得恰到好处,期待笔者还能继续把文中限于篇幅没有提及的其他机制写下来
2023-8-8 20:12
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
23
膜拜
2024-1-6 19:58
0
游客
登录 | 注册 方可回帖
返回