Qemu tcg中间码优化与后端机制
TCG基本逻辑
qemu tcg的基本翻译思路是把guest指令先翻译成中间码(IR),然后再把IR翻译成host指令。guest->IR->host这种三段式实现的好处是把前端翻译,优化和后端翻译拆开了,降低了开发的难度。
IR指令的理解是比较直白的,qemu定义了一套IR的指令,具体的定义在tcg/README里说明,在一个tb里,qemu前端翻译得到的IR被串联到一个链表里,中间码优化和后端翻译都靠这个链表得到IR,中间码优化时,需要改动IR时(比如,删掉不可达的IR),对这个链表做操作就好。
中间码不只是定义了对应的指令,也有寄存器的定义,它形成了一个独立的逻辑空间,在IR这一层,可以认为都在中间码相关的寄存器上做计算的。IR这一层定义了几个寄存器类型,它们分别是:global, local temp, normal temp, fixed, const, ebb
一般guest的gpr也被定义为IR这一层的global寄存器,中间码做计算的时候,会用到一些临时变量,这些临时变量就保存在local temp或者是normal temp这样的寄存器里,计算的时候要用到一些常量时,需要定义一个TCG寄存器,创建一个常量并把它赋给TCG寄存器。
riscv下global寄存器一般如下定义:
1 |
|
这里分配了对应的TCG寄存器,返回值是这些寄存器存储地址相对tcg_ctx的偏移。注意这里得到的是global寄存器的描述结构,类型是TCGTemp,而global寄存器实际存储在CPURISCVState内具体定义的地方,TCGTemp内通过mem_base和mem_offset指向具体存储地址。
实际上,所有TCG寄存器的分配都是在TCGContext里分配了对应的存储空间,并且配置上相关参数,这些参数和IR一起交给后端做IR优化和后端翻译,后端使用TCGContext的地址和具体寄存器的偏移可以找见具体的TCG寄存器。
normal temp只在一个BB中有效,local temp在一个TB中有效。fixed要结合host寄存器分配来看,首先IR中分配的这些寄存器都是虚拟的寄存器,IR翻译到host指令都要给虚拟寄存器分配对应的host物理寄存器,当一个TCG寄存器有TEMP_FIXED标记表示在后端翻译时把这个虚拟寄存器固定映射到一个host物理寄存器上,一般fixed寄存器都是翻译执行时经常要用到的参数。
中间码优化
前端翻译得到的IR可能会有优化的空间存在,所以qemu在进行后端翻译之前会先做中间码
优化,优化以一个TB为单位,优化的输入就是一个TB对应的IR和用到的TCG寄存器。
1 |
|
tcg_optimize是做着一些常量的检查,进而做指令优化(折叠常量表达式), 我们取其中的一个case,比如fold_add具体看下,大概知道下这里是在干什么。可以看到这个case检测add_32/64这个IR的两个操作数是不是常量,如果是常量,那么在这里直接把常量相加后的结果放到一个常量类型TCG寄存器,然后把之前的add_32/64改成一条mov指令。
从名字就可以看出reachable_code_pass应该做的是一些死代码的删除,这里检测到运行不到的IR就直接从IR链表里把他们删掉。
中间码优化的输出还是IR链表和相关的TCG寄存器,可见我们也可以把这两个函数注释掉,从而把中间码优化关掉。可以看出,中间码优化和编译器IR优化的逻辑是类似的。
tcg后端功能
qemu用tcg模拟guest指令执行,qemu把guest指令先翻译成中间码,然后再把中间码翻译成host指令,host指令可以最终在host cpu上执行,这样就完成了翻译。
此部分关注的是后端翻译模型,也就是中间码翻译成host指令的过程。中间码是一套完整的指令集定义,使用中间码可以完整的表述guest指令的行为,看一个小例子对这种描述会有更直观的感受。
1 |
|
如上guest那条store指令,它被翻译成了两条中间码,第一条add_i64是用来计算sd要store的地址,计算出的地址保存在tmp4这个虚拟寄存器里,第二条中间码把s0的值store到tmp4描述的内存上,qemu用中间码和虚拟寄存器完整的表述guest的逻辑。这里qemu_st_i64这个中间码表示一个store操作,store的数据和地址都用虚拟寄存器描述,所以在qemu_st_i64之前要用add_i64先计算出store的地址,并保存在虚拟寄存器里。
qemu中,其它的guest指令也是这样先翻译成中间码和虚拟寄存器的表示,后端翻译基于中间码和虚拟寄存器进行。上面的中间码表述中,x2/sp和x8/s0还是guest上寄存器的名字,但是逻辑上guest上的寄存器都已经映射到qemu虚拟寄存器,所以中间码指令中的所有寄存器都是qemu的虚拟寄存器。
qemu模拟的guest cpu系统说到底就是host内存里表示的guest cpu的软件结构体的状态以及guest内存的状态,qemu中间码已经完整的描述了guest状态改变的激励,拿上面addi和sd guest指令的模拟为例,模拟addi的中间码是addi_64 x2/sp,x2/sp,$0xffffffffffffffe0 ,表示要把guest的sp加上-32,sd的中间码表示要把guest sp + 24指向的地址上的值改成s0的值。我们拿到如上中间码或者guest指令,甚至可以直接写c代码去完成模拟。qemu为了追求效率把中间码翻译成host指令来完成模拟。
1 |
|
这几条中间码只是表意,实际真正更新guest cpu的数据结构和guest地址还需要host指令完成,所以实际翻译后的host指令可能是这样的:
1 |
|
qemu的后端翻译就是完成如上功能,总结起来就是:
- 分配host物理寄存器;
- 生成host指令;
- host和guest之间的状态同步。
分配host物理寄存器
虚拟寄存器和host物理寄存器是两个独立的概念,虚拟寄存器可能会很多,而物理寄存器的个数是有限的,虚拟寄存器有自己的生命周期,虚拟寄存器生命周期结束后,它所使用的物理寄存器就可以给其它虚拟寄存器使用。因为host物理寄存器数目有限,就有可能出现host物理寄存器不够分的情况,这时候就需要把已经分配但是目前还没有用到的host物理寄存器的值保存到内存,这样就可以腾出host物理寄存器来使用。
qemu在处理host物理寄存器分配的时候,分了两步处理,第一步先确定虚拟寄存器的生命周期,一般叫做寄存器活性分析,第二步根据虚拟寄存器活性分析的结果具体分配物理寄存器。
针对一段中间码,qemu对其做逆序遍历,依此确定虚拟寄存器的生命周期。如果一个虚拟寄存器后续还中间码使用,那它还是live的,如果后面没有中间码用了,它就dead了。
所以,一个虚拟寄存器dead与否是和具体中间码一起看的,一个虚拟寄存器可能在前几个中间码中是live的(虽然这几个中间码并没有使用这个虚拟寄存器),最后一个使用它的中间码后这个虚拟寄存器就dead了。qemu里只要记录虚拟寄存器被引用时的状态就好。
生成host指令以及状态同步
我们把状态同步和host指令生成放到一起看,因为所谓状态同步也要生成host指令进行。
对于中间码的输入虚拟寄存器,需要先判断这个输入寄存器的值是保存在内存上,还是已经保存在host物理寄存器上了,如果还在内存上,qemu就要分配host物理寄存器,然后插入host上的load指令把内存上的值load到host物理寄存器上,如果虚拟寄存器的值已经在host物理寄存器上,那么它直接就可以参与计算。对于中间码的输出虚拟寄存器,qemu需要为它分配host物理寄存器。
中间码的输入和输出寄存器都有着落了,qemu就可以尝试把中间码翻译成host指令。这个翻译可能直接就可以翻译成一条host指令,也可能需要再插入几条host指令调整下。
guest指令对应的中间码执行完后,需要把guest指令的输出同步回guest CPU数据结构,所以qemu在这里还需要插入host store指令把数据刷回guest CPU。qemu在寄存器活性分析的时候会把需要做同步的虚拟寄存器打上sync的标记,生成host指令的时候遇见sync标记就可以直接插入host指令做同步。
并不需要每个guest指令执行完都要把信息刷回guest CPU数据结构,虽然guest CPU的信息是定义在guest CPU数据结构中的,但是我们是模拟guest CPU,只要不破坏模拟的逻辑,host物理寄存器上的值就可以先不刷回guest CPU数据结构。那什么时候需要刷回guest CPU,整个TB执行完时,虚拟寄存器需要被同步回guest CPU,当中间码可能导致guest CPU异常时,需要做同步,因为触发异常后,guest CPU跳转到异常处理地址,并且向软件报告异常处理的上下文,其中guest CPU的通用寄存器就都是从guest CPU数据结构获取。
加入BB的概念
上面讲的寄存器分配和状态同步其实还不完整,qemu的一个翻译块(TB)里是可以存在跳转中间码的,在有跳转中间码的情况下,上面逆序遍历确定虚拟寄存器活性的办法就会有问题。为此qemu中在TB的基础上又引入了Basic Block(BB)的概念,简单讲在一个BB内中间码都是顺序执行的,这样如上的逻辑在BB内还是成立的。所以,在BB的结尾就要dead全部虚拟寄存器,并且把guest CPU对应的虚拟寄存器向guest CPU数据结构做同步。