goto tb分析
qemu tcg跳转的处理中分析了qemu中模拟跳转的逻辑,其中提到了chained tb的概念,
chained tb中有一个限制,这个在它的代码实现里看出来:两个chained tb对应的guest
指令需要在同一个guest的page里。下面分析这样限制的原因。
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
| VA1 ---> +----------+ | | map1 | | ----------+ VA2 ---> +----------+ map2 | +----------+ <--- PA1 | | ------+ +---> | | | | map3 | | | +----------+ ----+ | +----------+ <--- PA2 | | | | | +-------> | | | +----------+ <--- PA3 | | | +---------> | | +----------+ tb1 key: pa1 +-------+ <---------------------+ <--- htable | | | +-------+ | | | v tb2 key: pa2 | +-------+ <-------------+ | | | | | +-------+ | | | | tb_jmp_cache key: va | | +----------------------------------------+ | va2 va1 | +----------------------------------------+
|
qemu在翻译执行的时候,对于tb有两个cache,首先所有生成的tb都会放到htable这个哈希表,
这个是我们这里说的第一个cache,这个哈希表的key是guest代码块的起始pa以及一些其它的
flag,另外一个cache叫tb_jmp_cache,这个是很简单的用guest代码块的起始va做key(这个
cache和本文分析的问题无关,对于它的分析,我们单独放到最后)。
htable用pa以及flag做key,在翻译执行的时候首先会用va查tlb得到pa,如果tlb里没有缓存,
那就触发page table walk或者取指令异常,终归是要把pa找到并填到tlb里,因为是物理地址
做key,所以有可能不同虚拟地址映射到相同物理地址上时,对应的tb也只有一个。
当程序运行时,一段代码的位置,从va看是不变的,但是代码的物理存储位置可能是变化的,
比如上图中从map2到map3映射的改变。从htable的设计上看,这种映射关系改变和tb之间并
没有直接的联系,因为改变的只是va到pa的映射,而tb在htable上是用pa做key,退一步讲,
如果多个不同的va映射到一个pa上,其中一个映射改变了,其它的映射还在,对应的tb应该
继续保留。
如上的情况是va映射不同pa上的代码是相同的情况,最常见的是页换出换入的情况。如果chain
tb跨越了这两个虚拟页,chain tb在语意上已经不对了,因为改变映射关系后,被指向的tb
应该是以PA3页面上的PA作为key的,但是实际上因为改变映射的两个物理页上的指令是一样
的,运行可能不会出问题。
但是,如果改变映射前后的物理页上的指令不一样,chain tb跨越不同页就会出错,当然
这种情况很少会有实际的应用场景。
chained tb还需要面对的一个问题是,如果被指向的tb对应的guest指令被修改了怎么办,
比如,上面tb2对应的guest指令被修改了,之前得到tb2显然是不能用了。qemu使用了一个叫
PageDesc的软件结构管理guest的物理内存,管理的单位是guest的页大小,一个PageDesc
对应一个guest页,PageDesc记录着相关guest页对应的所有tb,理论上看,如果一个guest
页上的指令有修改,qemu只要找到修改的guest指令对应的tb块,把这个tb块从chained tb
的链条里移除就好。
这里具体上是靠tb里的jmp_reset_offset、jmp_list_head、jmp_list_next以及jmp_desc
域段实现的,这个几个域段的语意分别是,jmp_list_head是指向tb的tb组成的链表,jmp_desc
是本tb的两个可能的直接跳转的地址,jmp_list_next用于把tb链入jmp_list_head链表,因为
一个tb可能指向两个tb,一个tb可能被链入两个jmp_list_head,所以jmp_list_next数组有
两个元素,jmp_reset_offset是本tb两个可能的直接跳转地址的复位值。
在两个tb相连的时候更新如上的信息,jmp_reset_offset在后端翻译里计算和更新。
1 2 3 4 5 6 7 8 9 10
| tb_add_jump(tb, n, tb_next /* 把跳转地址记录在jmp_dest[n] */ +-> qatomic_cmpxchg(&tb->jmp_dest[n], (uintptr_t)NULL, (uintptr_t)tb_next)
/* 更新host指令中的跳转地址 */ +-> tb_set_jmp_target(tb, n, (uintptr_t)tb_next->tc.ptr)
/* 把指向tb_next的tb插入tb_next里的jmp_list_head链表 */ +-> tb->jmp_list_next[n] = tb_next->jmp_list_head +-> tb_next->jmp_list_head = (uintptr_t)tb | n
|
chained tb之间的关系可以用如下的图来描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| +----------------+ +----------------+ +----------------+ |tb0 | |tb1 | |tb2 | | | | | | | |jmp_list_next[0]|<--|jmp_list_next[0]|<+ |jmp_list_next[0]|<-------------+ |jmp_list_next[1]| |jmp_list_next[1]| +-|jmp_list_next[1]|<-----------+ | +----------------+ +----------------+ +----------------+ | | \ | / \ | | \ | / \ 0 | | 0 \ 0 | / 1 v | | \ | / +----------------+ | | v v v |tb4 | | | +----------------+ | | | | |tb3 | |... | | | | | |jmp_list_head --+--+-+ |... | +----------------+ | |jmp_list_head --+---------------------------------+ +----------------+
|
从如上数据结构中去掉一个tb的逻辑如下, 比如我们这里要去掉tb3,具体的逻辑是:
1 2 3 4 5 6 7 8 9 10
| do_tb_phys_invalidate /* * 找到tb3指向的节点,把tb3指向的tb中的jmp_list_head上的tb3节点去掉, 分别 * 对tb3可能指向的两个tb节点做这样的操作。 */ +-> tb_remove_from_jmp_list(tb, 0) +-> tb_remove_from_jmp_list(tb, 1)
/* 从tb3的jmp_list_head的到指向他的tb, 更新这些tb的指向 */ +-> tb_jmp_unlink(tb)
|
如上已经介绍了goto tb相关的关键逻辑,下面进一步看下qemu PageDesc相关的整体实现。
PageDesc初始化的地方是page_init函数,可以看出相关的位置是各种tcg资源初始化的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| /* accel/tcg/tcg-all.c */ tcg_init_machine +-> page_init /* * 这个函数里初始化PageDesc的各级表项的配置,它和页表类似的用一个多级表 * 管理整个物理地址空间。 * * 对于riscv 44bit的物理地址空间来说,如果一个PageDesc管理4KB的页面, * 整个物理地址空间的管理分为两级,第二级一项管理1K个页面,那么第一级有 * 44 - 12 - 10 = 22,2 ^ 22项。 */ +-> page_table_config_init +-> tb_htable_init /* tcg后端翻译需要的资源 */ +-> tcg_init
|
通过物理地址找见PageDesc的函数:
把一个tb加入到一个PageDesc里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| tb_page_add /* * 把tb插入first_tb指向的链表的第一个节点,n是1表示有指令跨域了一个page, * 那么就要在两个page对应的PageDesc中都要做记录。 * * 实际上在riscv的qemu tcg模型里,只有一种跨page边界的情况,我们把这个逻辑 * 独立放到下面。 */ +-> tb->page_next[n] = p->first_tb +-> p->first_tb = tb | n /* * 如果是第一次给这个PageDesc加tb,就是这个page上的指令第一次翻译出tb块, * 把对应page的dirty bit清理(初始化)下。 * * 如下的dirty bit和qemu里虚拟机的热迁移有关系。 */ +-> tlb_protect_code(page_addr) /* softmmu/physmem.c */ +-> cpu_physical_memory_test_and_clear_dirty
|
从PageDesc里去掉一个tb,直接从链表里去掉对应的tb即可:
PageDesc里tb被去掉的调用路径:(to do test)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| store_helper +--> notdirty_write(env_cpu(env), addr, size, full, retaddr); +-> tb_invalidate_phys_page_fast(pages, ram_addr, size, retaddr); +-> tb_invalidate_phys_page_range__locked +-> tb_phys_invalidate__locked(tb); /* 如上讲的guest代码被修改后,需要移除对应的tb,相关逻辑的入口是这里*/ +-> do_tb_phys_invalidate(tb, true); /* 移除htable里的tb */ +-> qht_remove(&tb_ctx.htable, tb, h) /* 移除PageDesc里的tb */ +-> tb_page_remove /* 从tb链条里移除tb */ +-> tb_remove_from_jmp_list(tb, 0); +-> tb_remove_from_jmp_list(tb, 1); +-> tb_jmp_unlink(tb);
|
我们可以把gdb的断点设在tb_invalidate_phys_page_range__locked上,然后运行一段自修改
代码(自修改代码可以参考这里),看看实际运行的效果。
指令垮页分析
实际上,在riscv 7.1.50的qemu tcg模拟中,只有一个4B指令跨越页边界这一种情况存在了。
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 34 35 36 37 38 39 40 41 42
| riscv_tr_translate_insn [...] /* Only the first insn within a TB is allowed to cross a page boundary. */ if (ctx->base.is_jmp == DISAS_NEXT) { /* * pc_first pc_next * | | * | v | v | * +-----------+-----------+ * | Page | Page | * */ if (!is_same_page(&ctx->base, ctx->base.pc_next)) { ctx->base.is_jmp = DISAS_TOO_MANY; } else { unsigned page_ofs = ctx->base.pc_next & ~TARGET_PAGE_MASK; /* * pc_first pc_next * | | * | v v | | * +-----------+-----------+ * | Page | Page | * * 这里就是pc_next为页尾 - 2Byte的情况,这种情况下,如果pc_next指向 * 的指令是一个2B的压缩编码指令,最后这个指令还可以放到当前tb,如果是 * 一个4B的普通指令,那么pc_next + len就到了下一个页里,需要结束当前 * tb。 * * 对于下个4B编码的指令,下次翻译先取16bit,得到是一个32bit指令后再 * 再把后16bit取出来,前端翻译后走到上面第一个分支里,发现已经跨越了 * 页边界,于是停止当前tb的翻译,这个tb里只翻译了一个跨越页边界的32bit * 指令。 */ if (page_ofs > TARGET_PAGE_SIZE - MAX_INSN_LEN) { uint16_t next_insn = cpu_lduw_code(env, ctx->base.pc_next); int len = insn_len(next_insn); if (!is_same_page(&ctx->base, ctx->base.pc_next + len)) { ctx->base.is_jmp = DISAS_TOO_MANY; } } } }
|
tb_jmp_cache分析
tb_jmp_cache用va做key是存在问题的,一个系统里可能有相同的va映射到不同的pa,比如,
两个进程的va相同,映射的pa就可能不同。所以,要保证在tb_jmp_cache里,va只唯一映射
一个pa,这个在user mode似乎没有问题,在system mode的完整逻辑还有待分析。(todo)