--- 以下是2007年做相关工作的学习笔记(留意,版本比较早了,其后可能有各种改动)。多年过去了,世事变迁,加上我的水平非常有限,现在翻出来仅供批评,勿认真,而且现在我自己也看不懂了。 --->
QEMU是一款虚拟机软件,模拟了包括CPU在内的各种硬件系统,包括:

  1. 指令解释和执行
  2. 异常、中断、时钟等CPU相关模块
  3. 内存、网卡、硬盘,显示系统,以及键盘和鼠标输入

QEMU虚拟机的初始化过程:

  1. 分配、初始化虚拟的CPU、内存等数据结构。
  2. 分配、初始化其他硬件设备
  3. 如指定了CPU状态和内存镜像则将两者加载
  4. 启动时钟系统
  5. 调用cpu_exec运行guest系统

以下分若干小节介绍具体工作流程。

指令的翻译和执行过程

Guest即被虚拟的系统,Guest的指令系统不一定跟Host机相同,比如可以在x86上虚拟出一台arm机器来。
对guest指令的翻译和执行过程如下:

  1. 根据guest的CPU状态得到当前指令指针的guest逻辑地址;
  2. 根据mmu状态得到在guest物理地址;
  3. 加上guest内存在host内存的基址得到待执行指令的host逻辑地址;
  4. 取出待执行指令,在微操作函数库中查找待执行指令的微操作函数(一小段C代码,但是已经编译为二进制)并拷贝到执行缓冲区TB;
  5. 根据指令执行流程,对下一条指令重复上面步骤,直到发现一条jump指令,或者一条修改CPU状态并无法预测结果的指令,或者发现TB满了;
  6. 给执行缓冲区TB生成一段段落结束代码,其中包含jump的跳向;
  7. 调用dyngen函数对上述生成的代码做重定位,使其能寻址到guest数据区;
  8. 执行上述生成的代码;

以上是虚拟机工作的最核心过程。

指令翻译过程的传送图

  1. Op.c文件编译为obj格式的微操作函数库。
  2. 每条guest指令对应一个微操作序列。
  3. 拷贝guest指令对应的微操作序列到TB中,形成host指令序列。

Guest和host的寻址转换关系

说明:

  1. Disas_insn(): 对guest代码逐条翻译,并存放在TB中。
  2. Dyngen_code(): 重定向翻译后的代码。

TB相关数据结构

  1. TB是一个数组:TranslationBlock tbs[CODE_GEN_MAX_BLOCKS];
  2. 使用一个计数器来指向空闲TB:int nb_tbs;
  3. 使用一个hash索引的指针数组来指向各个TB:TranslationBlock *tb_phys_hash[CODE_GEN_PHYS_HASH_SIZE];

从地址索引到TB的过程如下:

  1. phys_pc = get_phys_addr_code(env, pc);//从逻辑地址得到物理地址
  2. h = tb_phys_hash_func(phys_pc);//取低位的hash操作;
  3. ptb1 = &tb_phys_hash[h];//用提炼的h做hash表索引得到指针

函数调用主路径

几个关键函数:

  1. Cpu_exec(): 放入CPU状态,然后看TB表中有没有翻译好的代码,没有则调用tb_find_fast()去生成翻译好的代码,其后调用gen_func()执行翻译好的代码。
  2. Disas_insn(): 对guest代码逐条翻译。
  3. Dyngen_code(): 重定向翻译后的代码。
  4. gen_op_ret_jmp_T0(): 翻译后的跳转指令,在其中插入argos检查代码。

对guest代码逐条翻译

函数disas_insn()对guest代码进行逐条的翻译。翻译的过程如下:

  1. 首先处理指令前部分的prefix;
  2. 解析操作码;
  3. 调用gen_op_*()函数生成一段微操作代码;
  4. 返回(此刻指令指针指向下一条指令);

最典型的一个指令解析如下:

[cpp] view plaincopy
  1. case 0x89: /* mov Gv, Ev */
  2. if ((b & 1) == 0)         ot = OT_BYTE;
  3. else             ot = dflag + OT_WORD;
  4. modrm = ldub_code(s->pc++);
  5. reg = ((modrm >> 3) & 7) | rex_r;
  6. gen_ldst_modrm(s, modrm, ot, reg, 1);
  7. break;

但是涉及到跳转指令的时候有些变化,即此刻的指令解析应该告一个段落,返回到TB并执行。

特点---动态翻译、重定位

Qemu文档中声称的不同于其他虚拟机的特点:

  1. QEMU is a dynamic translator. The basic idea is to split every x86 instruction into fewer simpler instructions.(target-i386/op.c)把一条guest指令分解为功能相同的一小段host代码,也即指令的微操作函数
  2. A key idea to get optimal performances is that constant parameters can be passed to the simple operations. For that purpose, dummy ELF relocations are generated with gcc for each constant parameter. Then, the tool (`dyngen‘) can locate the relocations and generate the appriopriate C code to resolve them when building the dynamic code.常量参数可以带入微操作函数,dyngen可以根据重定位信息并生成恰当的动态代码供执行

指令翻译的优化过程

将多个微操作串接,这样可以省略每个微操作的调用和返回开支
确保只有一个出口,这样可以消除入口和出口的开销。如下的出入口部分就可以消除了:—entrance—body—exit—entrance—body—exit—entrance—body—exit—
注意要确保这个出口在最后。一个普通函数的出口不一定在最后,即使在C代码的最后,也不一定是编译后的最后一条指令。比如:

[cpp] view plaincopy
  1. int global, global2;
  2. void quux(int a)
  3. {
  4. if (a > 0) {
  5. global = 0;
  6. goto out;
  7. } else {
  8. global = 1;
  9. goto out;
  10. }
  11. out:
  12. global2 = 1;
  13. return;
  14. }

可能编译为如下:

[cpp] view plaincopy
  1. quux         CMPL a+0x0(FP), $0x0
  2. quux+0x5     JLE quux+0x1c(SB)
  3. quux+0x7     MOVL $0x0, global(SB)
  4. quux+0x11     MOVL $0x2, global2(SB)
  5. quux+0x1b     RET
  6. quux+0x1c     MOVL $0x1, global(SB)
  7. quux+0x26     JMP quux+0x11(SB)

重定位原理---常量

立即数指令在翻译时的麻烦:

  1. 微操作二进制段是直接拷贝到TB的,本身无法带入参数;
  2. 只能先拷贝过去,然后修改指令的立即数部分;

问题:待翻译指令中有立即数,可否拷贝之后从待翻译指令中取立即数?

常数重定位的意义在于,无需堆栈或者变量来中转,直接生成了host的立即数指令

  1. 首先obj文件解析为符号表,重定位条目,代码区;
  2. 每个微操作码的常量参数使用特殊的符号名:__op_paramN,N是个序列号,这样就可以在重定位条目中找到对应项;
  3. 编译后的微操作码被拷贝到指定位置,然后从重定位条目中找到重定位信息,实现运行时的正确重定向;
[cpp] view plaincopy
  1. case INDEX_op_addl_T0_im:
  2. {
  3. long param1;
  4. extern void op_addl_T0_im();
  5. memcpy(gen_code_ptr, (char *)&op_addl_T0_im+0, 6);
  6. param1 = *opparam_ptr++;
  7. *(uint32_t *)(gen_code_ptr + 2) = param1;
  8. gen_code_ptr += 6;
  9. break;
  10. }

其中opparam_ptr指向运行时参数列表,在本例中该值被带入gen_code_ptr指向的指令立即数位置。

重定位原理---变量和子函数

微操作可能需要引用helper类子函数来实现复杂功能,引用变量的重定位也一样,只要需要修改相对偏移量即可。

[cpp] view plaincopy
  1. {
  2. extern void op_cpuid();
  3. extern char helper_cpuid;
  4. memcpy(gen_code_ptr, (void *)((char *)&op_cpuid+0), 13);
  5. *(uint32_t *)(gen_code_ptr + 5) = (long)(&helper_cpuid) -(long)(gen_code_ptr + 5) + -4;
  6. gen_code_ptr += 13;
  7. }

以上参见dyngen.c的gen_code()函数,解释如下:

  1. 前面两条语句说明微操作op_cpuid及子函数helper_cpuid都在host内存的进程空间中,是随qemu编译加载的,里面的偏移量是对的,即op_cpuid能调用到helper_cpuid。
  2. 第三条语句拷贝op_cpuid代码段到TB区,由于op_cpuid是相对寻址helper_cpuid,由于调用者自身移动了,故这个拷贝版本op_cpuid不能寻址到helper_cpuid了。
  3. 第四条语句修改op_cpuid的call指令的参数(被调子函数的相对偏移量),使之正确指向helper_cpuid,这样op_cpuid就可以调用到helper_cpuid了。
  4. 最后一条语句移动下面一块空地,准备接收翻译的下一条指令。

重定位实际过程

指令重定位实际过程

  1. 调用dyngen_labels(gen_labels, nb_gen_labels, gen_code_buf, gen_opc_buf)设置某些标签,为下一步编译做准备;
  2. 参见函数dyngen_code(uint8_t *gen_code_buf,uint16_t *label_offsets, uint16_t *jmp_offsets,const uint16_t *opc_buf, const uint32_t *opparam_buf, const long *gen_labels);
  3. 最后生成的适合在host上跑的文件存放在TB里面;

Dyngen函数

[cpp] view plaincopy
  1. int dyngen_code(uint8_t *gen_code_buf, uint16_t *label_offsets, uint16_t *jmp_offsets,
  2. const uint16_t *opc_buf, const uint32_t *opparam_buf, const long *gen_labels)
  3. {
  4. for(;;) {
  5. switch(*opc_ptr++) {
  6. {在gen_code(name, sym->st_value, sym->st_size, outfile, 1)输出如下信息(不全):
  7. extern char __dot_%s __asm__(\"%s\");
  8. memcpy(gen_code_ptr, (void *)((char *)&%s+%d), %d);//是拷贝二进制的地方
  9. /* 输出偏移量信息 emit code offset information */
  10. label_offsets[%d] = %ld + (gen_code_ptr - gen_code_buf);
  11. param%d = *opparam_ptr++;
  12. jmp_offsets[%d] = %d + (gen_code_ptr - gen_code_buf);
  13. *(uint32_t *)(gen_code_ptr + %d) = %s + %d;
  14. *(uint32_t *)(gen_code_ptr + %d) = %s - (long)(gen_code_ptr + %d) + %d;
  15. gen_code_ptr += %d;
  16. *gen_opparam_ptr++ = param%d;
  17. *gen_opc_ptr++ = INDEX_%s;
  18. }
  19. case INDEX_op_nop:            break;
  20. case INDEX_op_nop1:            opparam_ptr++;            break;
  21. case INDEX_op_nop2:            opparam_ptr += 2;            break;
  22. case INDEX_op_nop3:            opparam_ptr += 3;            break;
  23. default:            goto the_end;
  24. }
  25. }
  26. the_end:
  27. flush_icache_range((unsigned long)gen_code_buf, (unsigned long)gen_code_ptr);
  28. return (gen_code_ptr - gen_code_buf);
  29. }

从上面的代码看,似乎dyngen()函数的功能是为前面生成的二进制微操作码生成重定位辅助结构。
在dyngen.c中用fprintf()打印dyngen()函数为文件,命令行上可以确定文件名。

invalidate指令

某些指令在执行的过程中会修改自身

  1. 该指令在TB中有一份翻译后的,在guest内存中有一份原始的,修改指令自身是修改在guest内存中的那一份(这里可能有疑问);
  2. 此类情况发生后则立即invalidate,也即从内存往TB更新,始终保证TB中含有最新的指令;
  3. 给指令所在页面做了bitmap,注意该页面可能也有数据区,该页面被修改时可以根据bitmap判定究竟是指令被修改了还是数据被修改了,只invalidate指令。

Invalidate更新流程

  1. 将指令区域设置为不可写,在signal信号处理函数中添加对应的写错误函数,在该函数中invalidate对应的TB区;
  2. 自修改指令运行后将写TB区,导致signal信号处理函数被调用;

异常和中断处理

在每个小循环中处理中断,在每个大循环中处理异常。

[cpp] view plaincopy
  1. For(;;)
  2. {
  3. 如果有异常则处理,入口是do_interrupt ();
  4. For(;;)
  5. {
  6. 如果有中断则处理,入口和异常相同:do_interrupt()
  7. tb = tb_find_fast(); gen_func(); 执行逐个TB里面的代码;
  8. 怎样跳出并到上级循环的?不清楚。
  9. }
  10. }

Guest的异常通常用host的信号来模拟

QEMU虚拟机关键源代码学习相关推荐

  1. elipse调试linux内核,debug eclipse cdt + qemu虚拟机调试linux内核

    debug eclipse cdt + qemu虚拟机调试linux内核 (17页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 9.90 积分 A scr ...

  2. BT源代码学习心得(八):跟踪服务器(Tracker)的代码分析(用户请求的实际处理) - 转贴自 wolfenstein (NeverSayNever)

    BT源代码学习心得(八):跟踪服务器(Tracker)的代码分析(用户请求的实际处理) author: wolfenstein 通过上一次的分析,我们已经知道了Tracker采用http协议和客户端通 ...

  3. 转贴: wolfenstein工作室-eMule源代码学习心得

    1, eMule源代码学习心得(1):eMule代码的总体风格和其它相关工程 eMule的官方首页上写着:2002年05月13日 一个叫做 Merkur 的人,他不满意原始eDonkey2000客户端 ...

  4. QEMU 虚拟机管理软件Web版,入门教程

    当代的计算机系统中,虚拟化技术的应用,大大提升计算机的性能效率,减少计算机性能浪费. 在现代的计算机软件中,已经诞生出众多基于虚拟化技术应用的虚拟机软件.vmware.VirtualBox.QEMU. ...

  5. 第一章 QEMU虚拟机与ARM64平台搭建

    系列文件目录 <ARM64体系结构结构编程与实践>学习与应用记录 第一章 QEMU虚拟机与ARM64平台搭建 文章目录 系列文件目录 本章前言 一.ubuntu虚拟机安装 1.ubuntu ...

  6. 初步了解qemu虚拟机

    QEMU简介     QEMU是一套以GPL许可证分发源码的模拟处理器,在GNU/Linux平台上使用广泛.     Bochs,PearPC等与其类似,但不具备其许多特性,比如高速度及跨平台的特性, ...

  7. php7.0 cli,PHP-7.1 源代码学习:php-cli 启动流程

    前言 php cli main 函数 configure & make 默认构建目标为 php-cli,相关代码在 sapi/cli 目录下,php_cli.c 文件中能够找到 main(入口 ...

  8. eos 源代码学习笔记一

    文章目录 eos 源代码学习笔记 1.eos 中的常见合约类型 2.语言环境局部( locale )变量的使用简介(目的是通过 gettext 软件包 来实现软件的全球化) 3.eos 源代码的一些优 ...

  9. linux qemu 运行win10,只用ISO镜像,在Linux系统上用Qemu虚拟机给另一块硬盘安装Windows 10...

    以免以后遇到同样情况忘掉,这是给自己写的教程. 以 Windows 10 为例. 1. 准备好Win10的ISO镜像. 2. 以root身份给对应的硬盘对普通用户增加rw权限 # chmod 666 ...

最新文章

  1. 【怎样写代码】偷窥高手 -- 反射技术(二):窥视内部
  2. 样条表示---OpenGL的逼近样条函数
  3. 简单几何(线段相交+最短路) POJ 1556 The Doors
  4. Java学习笔记11
  5. ET框架笔记 (笑览世界写)(转)
  6. centos设置开机自启动
  7. python创建数组并运行_python-Cython中从现有数组和变量创建新数组...
  8. “火星人”马斯克推论:世界或是被编码而成,上帝可能是个程序员!
  9. CodeDom系列--事件(event)定义和反射调用
  10. linux 清除mysql relay_MySQL 小版本升级
  11. 统计学习(四):多重检验与控制程序
  12. python实战项目
  13. 树莓派自带摄像头OpenCV图像识别-二维码识别
  14. 如何获得免费卡巴斯基激活码?
  15. 面经分享:网友问我,怎样才能在谷歌匹兹堡办公室里写代码?下篇
  16. win10系统还原点怎么设置
  17. PIPIOJ1166PIPI的棋盘
  18. Ceres库运行,模板内报内存冲突问题。(已解决)
  19. product相关函数(excel)
  20. html选择文件夹控件,File文件控件,选中文件(图片,flash,视频)即立即预览显示

热门文章

  1. 企业架构(EA)美国之行
  2. 冷到穿棉袄?数据中心其实可以“暖”一点
  3. 2019年年初iOS招人心得笔记(附面试题)
  4. hadoop快速入门之DKH安装准备
  5. linux下将目录授权给其他用户的步骤
  6. java下实现调用oracle的存储过程和函数
  7. AliOS Things KV组件的写平衡特性
  8. p2p webrtc服务器搭建系列1: 房间,信令,coturn打洞服务器
  9. 利用Eclipse开发Linux驱动
  10. 【哈佛商学院和斯坦福要求学生必看的20部电影】中/英字幕