-
-
[原创]qemu v0.4.4代码情景分析
-
发表于: 2025-9-5 15:09 465
-
为什么使用这个版本
qemu现在已经很庞大了,直接分析最新代码难度太大,所以选择从低版本开始分析。
qemu关键特性
分析的qemu是指在Linux和x86上运行的qemu,qemu载入Linux下的ELF可执行文件,然后仿真运行这个文件。
本文关注的问题是:
- ELF文件中的代码是如何运行的?
- 中断是如何模拟的?
- softmmu是如何工作的?
- 网卡是如何模拟的?
qemu流程
- 载入ELF文件到内存中,此时可以从内存中获得可执行原始代码
- 编译原始代码到中间代码,需要注意的是一条原始代码可能会被翻译为多条中间代码
- 根据中间代码生成可在x86的Linux上运行的二进制代码
原始代码到中间代码
关键函数是
1 | disas_insn |
这个函数每次将一条ELF文件中的指令转换为中间代码,需要注意的是在转换时不会一次性将所有ELF文件中的指令转换,当遇到JMP指令时就停止转换,当转换的指令太多超过大小限制时也会停止转换,转换后的代码块包含多条原始代码的对应代码。
中间代码到可在x86的Linux上运行的二进制代码
关键函数是
1 | dyngen_code |
这个函数的做法是取编译器编译的函数的一部分代码到指令缓存中,比如对于中间代码
1 | INDEX_op_movl_A0_EAX |
会将对应的函数
1 | op_movl_A0_EAX |
中的前3个字节复制到指令缓存中,这个函数如下
1 2 3 4 | void OPPROTO glue(op_movl_A0,REGNAME)(void){ A0 = REG;} |
这是一个宏,当REGNAME和REG为EAX时,该函数变为
1 2 3 4 | void OPPROTO op_movl_A0_EAX(void){ A0 = EAX;} |
该函数完成的功能是将寄存器EAX中的数据拷贝到中间代码寄存器A0中。这个函数会被编译器编译为二进制代码,因此取该函数的前3个字节到指令缓存中,然后执行指令缓存中的代码便完成了对原有ELF代码的执行。
注意
qemu第一次执行一条原始代码翻译后的二进制代码时会翻译该条原始代码及其之后的若干条代码(翻译指原始代码到中间代码和中间代码到可在x86的Linux上运行的二进制代码两个步骤),当下次运行到以该原始代码为第一条代码时会直接执行翻译后的对应代码块而不会去再次翻译,这样提高了性能。
解释完了指令的执行过程,再来介绍中断的模拟。
时钟中断的模拟:
对于模拟操作系统这样的软件而言,时钟中断是必不可少的,操作系统借助时钟中断切换任务。
对于qemu,模拟时钟中断的关键是利用宿主Linux中的时钟事件,当Linux中发出时钟事件时会调用qemu中的对应函数
1 | host_alarm_handler |
在该函数中会设置
1 2 | timer_irq_pending = 1;cpu_x86_interrupt(global_env, CPU_INTERRUPT_EXIT); |
这样系统就知道发生了时钟中断,由于设置了CPU_INTERRUPT_EXIT,所以在cpu_exec函数中
1 2 3 4 5 | if (interrupt_request & CPU_INTERRUPT_EXIT) { env->interrupt_request &= ~CPU_INTERRUPT_EXIT; env->exception_index = EXCP_INTERRUPT; cpu_loop_exit();} |
这段代码中需要关注的是EXCP_INTERRUPT,由于设置了这里,在随后的
1 2 3 4 5 | if (env->exception_index >= EXCP_INTERRUPT) { /* exit request from the cpu execution loop */ ret = env->exception_index; break;} |
这里的break语句会导致退出cpu_exec函数,然后会运行的代码是
1 2 3 4 5 | if (timer_irq_pending) { pic_set_irq(0, 1); pic_set_irq(0, 0); timer_irq_pending = 0;} |
由于在host_alarm_handler函数中设置了timer_irq_pending,所以这里代码会执行,pic_set_irq中产生了定时器中断,该函数中会调用
1 | cpu_x86_interrupt(global_env, CPU_INTERRUPT_HARD); |
该函数设置了中断标志变量
1 | interrupt_request |
然后程序会继续调用cpu_x86_exec函数去翻译和执行,但和平时不同,由于设置了interrupt_request,说明此时有中断产生了,所以会调用
1 | do_interrupt |
去执行中断处理函数,中断就这样被模拟出来了。
softmmu工作原理
mmu将虚拟地址映射为物理地址,一般是一个硬件,qemu中的softmmu则是用软件的方式去实现mmu硬件的工作。
操作系统在用mmu得到地址后会将虚拟地址和物理地址的对应关系保存在tlb中,这样下次遇到相同的虚拟地址就不必重新用mmu得到物理地址,而是直接用tlb中保存的内容。
下面用一条mov指令的执行来介绍softmmu。
1 | mov eax,[0x12345678] |
当cpu执行这条语句时,发现有内存地址0x12345678,则先去tlb中查询是否有对应的物理地址,当第一次执行这条语句时tlb中没有数据,所以会尝试填充tlb,在这个过程中发现softmmu所必须的pde,pte不存在,于是会触发中断,一般操作系统会在中断处理函数中填充pde和pte,当操作系统完成填充工作后,中断返回到这条mov指令,准备重新执行这条指令。当再次执行时,还是会先去tlb中查询是否有对应的物理地址,此时tlb中还是空的,所以还是会先去填充tlb,此时填充会成功,因为操作系统已经在中断处理程序中填充了pde和pte。当tlb被填充后cpu再次通过tlb获取物理地址,此时会成功得到0x12345678对应的物理地址,则mov指令执行成功。当下次再执行mov指令时,由于tlb中缓存有数据,则不必通过mmu获取物理地址,实现了加速。
ne2000网卡模拟
当模拟网卡时,qemu会打开一个本地Linux上的网络设备,qemu会利用Linux上10毫秒的定时器事件,也就是说每10ms,qemu会调用poll函数查看当前本地Linux上的网络设备是否收到数据,当有数据时,读取这些数据然后写入qemu模拟的ne2000网卡中的数据缓冲区中,并产生cpu中断来让操作系统有机会读取这些数据,操作系统通过读qemu模拟的ne2000的寄存器来接收网络数据。当有数据要通过网络发送时,也会通过写ne2000的寄存器来发送数据。
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!