stage-2 页表(第二阶段翻译表)是 arm64 KVM 控制 guest 物理内存访问权限的唯一硬件来源。一次映射能不能被 guest 读、写、执行,全部由 stage-2 PTE(Page Table Entry,页表项)上的属性位决定。主线内核里这套权限体系分三层表示:
- 软件记录层 vma/memslot:VMM 通过 ioctl 传给 KVM 的
memslot
flags(
KVM_MEM_READONLY、KVM_MEM_LOG_DIRTY_PAGES),加上宿主机进程的 VMA(Virtual Memory Area,虚拟内存区域)flags(VM_WRITE)。这是权限的事实源,建页权限全部从这两层现算 - 软件枚举层
prot:
enum kvm_pgtable_prot,KVM 内部传递权限的统一语言(KVM_PGTABLE_PROT_R/W/X) - 硬件位层 PTE:stage-2 PTE 上的 S2AP(Stage-2 Access Permission)、XN(eXecute Never)、AF(Access Flag)
三层之间的翻译经过两个函数:
stage2_set_prot_attr()把枚举编码成硬件位,供 make 时写到实际页表 PTE 中;kvm_pgtable_stage2_pte_prot()把硬件位解码回枚举enum kvm_pgtable_prot。
全文围绕一条 fault 主链展开,下图是地图,每个环节标注了所在章节(代码取自主线,约 7.3-rc2):
1. 权限位的两层表示:prot 枚举与 PTE 硬件位
先看硬件位层。stage-2 叶子描述符的属性位布局(4K 基页,非 LPA2):
1 | +----------+--------+-------+----------+---------------------+-----+-----+------+--------+---------+------+----+ |
- S2AP[1:0](bits
7:6):读写权限。bit7 = S2AP_W(可写),bit6 =
S2AP_R(可读),两位独立编码:
- 架构允许只写映射(S2AP=0b10,D8.4.2.1.1 Table D8-76,原文见附录A)。
- 虽然架构允许,但 KVM 从不产出只写映射,因为 compute_prot 恒加
KVM_PGTABLE_PROT_R(必选项,第 4 节),W 才是可选项
- XN[1:0](bits 54:53):执行权限,两位联合编码四种执行粒度;基线架构只有 bit54 有效,FEAT_XNX 实现后四种组合才全部可用
- AF(bit 10):访问标志,记录"这页被访问过没有",是页框回收的判定依据之一
- SW(bits 58:55):KVM 自留的软件位,不参与硬件翻译,pKVM 与 nested 各有用途(见第 9、10 节)
对应的软件枚举:
1 | /* linux/arch/arm64/include/asm/kvm_pgtable.h */ |
从枚举位prot到 PTE
位的flags->真实PTE属性位bit的忠实映射,是编码器stage2_set_prot_attr来处理的(第
2 节)。
另外头文件里还有一个权限位掩码:
1 | /* linux/arch/arm64/include/asm/kvm_pgtable.h */ |
KVM_PTE_LEAF_ATTR_S2_PERMS
圈出"纯权限位"的集合(S2AP_R、S2AP_W、XN)。它为优化多核并发缺页产生的竞态问题而生,是第
6 节的主角,此处先记住定义。
2. 编码器 stage2_set_prot_attr:从 prot 到 PTE 属性位
stage2_set_prot_attr() 是 stage-2
的编码器,一切新建映射的权限位都出自它:
1 | /* linux/arch/arm64/kvm/hyp/pgtable.c */ |
三件事值得注意:
- AF 恒置:每个新映射都带 AF=1,KVM 同时在 VTCR_EL2
开启硬件访问标志管理(HA 位),运行期由硬件自动维护。AF
唯一被清零的场景是宿主机页老化(mmu notifier
回调清位),清零后的首次访问触发 Access Flag Fault,由
handle_access_fault()置回 - SW
位直通:
prot & KVM_PTE_LEAF_ATTR_HI_SW原样并入 PTE。nested 用这四位携带映射层级信息(第 10 节) - 读写翻译是一对一的:
PROT_R → S2AP_R、PROT_W → S2AP_W,没有任何附加条件。可写性的全部决策都发生在上游的 prot 计算处(第 4 节),编码器只做忠实翻译,不会胡乱修改
从上面我们可知,编译器是服务于建立PTE的,所以我们只在页表重建时调用它。
编码器 stage2_set_prot_attr 只有两个调用方,恰好对应
prot 的两种来源:现算与继承:
1 | /* page fault 建立页表 */ |
- map 路径用于缺页建立页表场景,它的 prot 由 fault 处理现算,来源是 memslot/VMA(第 4 节);
- split 路径用于拆大页场景,它的 prot 从父 PTE 解码直接继承,一解码一编码构成 round-trip(往返,第 9 节验证其无损性)。
prot 从哪里来、怎么算,是接下来三节的主线。
3. page fault 入口:guest 异常如何路由到 KVM
guest 访问一个 GPA(Guest Physical Address)时,若 MMU 硬件处理
stage-2 时不放行这次访问(映射不存在、权限不够、AF 未置),硬件会把异常
ESR 投到 EL2,KVM 根据路由表,从 kvm_handle_guest_abort()
接手处理异常。入口路由在 exit handler
表里,其中ESR_ELx_EC_IABT_LOW是ESR的EC域段:
1 | /* linux/arch/arm64/kvm/handle_exit.c */ |
kvm_handle_guest_abort() 主流程是一棵分诊树:
1 | kvm_handle_guest_abort() /* [mmu.c] */ |
初筛由 gfn_to_hva_memslot_prot() 完成:把 GPA
换算成宿主机虚拟地址,同时报告 memslot 是否只读。结果有三种:
| 初筛结果 | 走向 |
|---|---|
| hva 无效(GPA 不在任何 memslot) | 注入回 guest,或转 io_mem_abort() 设备模拟 |
| 写 fault + memslot 只读 | 注入回 guest,到不了建页路径 |
| 通过 | 进入 user_mem_abort() |
其中,KVM 关心的 fault 类型由 ESR(Exception Syndrome
Register,异常综合征寄存器) 的 FSC(Fault Status
Code)字段区分,白名单只有四类,不在名单内的 FSC 直接
-EFAULT 拒绝处理:
| FSC 编码 | fault 类型 | 含义 | 处理走向 |
|---|---|---|---|
| 0b0001xx | Translation Fault(缺页) | PTE 无效,映射不存在 | 建页(第 5 节) |
| 0b0010xx | Access Flag Fault | AF=0,访问标志未置 | 置 young 恢复 |
| 0b0011xx | Permission Fault(权限异常) | PTE 有效但权限不足 | 放权或注入(第 5、7 节) |
| 0b110101 | Excl. Atomic Fault | 不支持属性的原子访问 | 注入回 guest |
FSC 的低两位还编码了 fault 发生的页表层级(level 0-3),后面
perm_fault_granule
的计算要用它。并用FSC来判断是不是写
fault。
判断时看似只需读 WnR(Write not Read)位无脑确认是不是写 fault,但实际有个特判:
1 | /* linux/arch/arm64/include/asm/kvm_emulate.h */ |
需要特判的根本原因是页面回收:guest 内核做页老化时,会把 stage-1 页表描述符的 AF 清 0。guest 开启硬件 AF/DBM 管理后,下一次 MMU 遍历发现 AF=0,硬件直接对描述符发起一次隐式写(目的是要把 AF 置 1),对 guest 完全无感。
这次隐式写同样要过 stage-2 翻译:若页表页在 stage-2 是只读(dirty logging 写保护、只读 memslot),就被拦下 fault 到 EL2,ESR 里 S1PTW=1。而 WnR 记录的是 guest 原始指令的类型,不是硬件这次隐式写的类型,guest 用 LDR(读指令)触发遍历,WnR 报"读",但硬件实际在做的是"写 AF"。直接按 WnR 判,这次写拿不到 S2AP_W,硬件反复撞墙 fault,活锁。
解法是 S1PTW 置位时无视 WnR,改按 FSC 的 fault 类型裁决:
- Permission Fault:硬件在遍历中更新 AF/DBM 被 S2 权限挡住,视作写 fault,由 relax 给 S2AP_W 放行(第 7 节)
- Translation Fault:硬件只是在读描述符、映射还没建,视作读 fault,按缺页正常建页
S1PTW=0 时异常由 guest 指令直接访问数据触发,WnR 可信,即代码注释里的"常规情况看 WnR"。
4. 权限的事实源:memslot 与 VMA 两层
过了初筛,剩下的权限问题交给
user_mem_abort()。它做的第一件事是问:这块内存到底允不允许写?答案不在
PTE 里(PTE 还没建),而在两层软件记录里。由
user_mem_abort() 进行三步骤处理:
1 | user_mem_abort() |
1)步骤一
在 kvm_s2_fault_pin_pfn()
中查证写权限:
1 | user_mem_abort() /* [mmu.c] */ |
两层各自把守一道门:
- memslot 层:VMM 注册内存区时如果传的是
KVM_MEM_READONLY,那么在kvm_follow_pfn()里会直接拒绝写 fault:
1 | /* Just the flags we need, copied from the kernel internals. */ |
- VMA 层:宿主机进程 mmap 时的真实权限
VM_WRITE。KVM 不显式读这个 flag,而是通过 GUP(get_user_pages,内核抓页接口)返回的FOLL_WRITE来进行判断。- 其中,写 fault 的请求天然带 FOLL_WRITE,GUP 失败即无 VM_WRITE;
- 读 fault 则不需要检查,因为 KVM
已经通过
get_user_pages拿到了物理页了,证明虚机 vmm 对这个 vma 是有访问权限的,Guest 可以放心读。
1 | /* linux/virt/kvm/kvm_main.c, hva_to_pfn_slow() */ |
两层都过,map_writable
才为真。它是可写性的软件事实源:后面的 prot
计算(本节下文)、relax 放权(第 7 节)全部以它为准,PTE 上的 S2AP_W
只是它的投影。
上述是kvm_handle_guest_abort中普通内存的user_mem_abort读写处理方式,而kvm_handle_guest_abort中还有两条旁支路径,在这里也浅谈一下,后文不再展开:
- pkvm_mem_abort():protected VM 的内存由 hypervisor
全权管理,
pin_user_pages(FOLL_WRITE)拿到页后直接以KVM_PGTABLE_PROT_RWX映射,不经过两层查证 - gmem_abort():guest_memfd(CoCo
机密虚拟机的私有内存)路径,可写性只看 memslot 有没有
KVM_MEM_READONLY,权限异常同样走 relax 放权
2)步骤二
事实源在手,第二段 kvm_s2_fault_compute_prot()
把它翻译成 prot:
1 | user_mem_abort |
条件 2 需要一张真值表。前提是
map_writable=1(两层门都过了,说明逻辑上是可写页),变的只是脏页跟踪是否介入。
dirty logging(脏页日志)是热迁移迭代期的脏页跟踪机制,memslot 开启
KVM_MEM_LOG_DIRTY_PAGES
后,已建映射全部会写保护,新建映射的权限也有对应的跟踪策略:
| 场景 | memslot logging | fault 类型 | W | 落表 |
|---|---|---|---|---|
| 普通内存 | 关 | 读/写 | 给 | RW |
| device 页 | 任意 | 读/写 | 给 | RW |
| 迁移迭代期 | 开 | 写 | 给 | RW,顺带记脏页 |
| 迁移迭代期 | 开 | 读 | 不给 | RO,读不标脏 |
由上述的表也可知,普通内存未开 logging 时
!memslot_is_logging 恒真短路,读写 fault 算出的
prot 都是 RW,访问类型不参与计算。
logging 例外:读 fault 故意建 RO,让未来的写还能 fault 出来被标脏;写
fault 则当场给 RW 并记脏,一步到位。map_writable=0 的页连 W
都没有,直接落 RO,不在表内。
3)步骤三
prot 现算完毕,第三段 kvm_s2_fault_map()
拿着它去动页表。怎么动,取决于下一节的分叉。
5. 建页路径:compute_prot 与 relax/map 分叉
kvm_s2_fault_map() 将要处理的有效 PTE 有两种状态:
- 页不存在(Translation Fault 建新页)
- 存在但权限不足(Permission Fault 要放权)。
两种状态的判据是两个尺寸是否相等:
1 | /* linux/arch/arm64/kvm/mmu.c, kvm_s2_fault_map() */ |
perm_fault_granule(权限 fault 粒度)从 FSC 的 level
字段算出:硬件报出"这次权限异常发生在第几级页表",第 n 级对应粒度
BIT(level_shift),正是现有映射的实际大小。缺页时它恒为
0,与任何 mapping_size 都不等,必走 map。两条路径的对比:
| relax_perms 路径 | map 路径 | |
|---|---|---|
| 何时进入 | Permission Fault 且两尺寸相等 | 缺页;或映射尺寸需变化 |
| PTE 状态 | valid,只需放松 | 无效,或需重建 |
| 动作 | 原子改权限位 + TLBI,无 BBM | 完整建页,除了 PERMS 过滤的情况外,都要 BBM |
map 路径的 walker 细节如下所示:
1 | user_mem_abort() |
最后一个容易忽略的细节(也是我们第 6 节要详细讲的内容):
- map
路径在
stage2_pte_needs_update中被PERMS过滤后返回-EAGAIN时(第 6 节的并发让出),kvm_s2_fault_map()最后的 out 处理中会把它转成 0,fault 处理视为成功,vCPU 直接恢复运行。 - guest 指令由硬件重执行,权限够就继续跑,不够则再次 fault 出来,下一轮自然走 relax。
但在此之前必须回答一个问题:map 路径的叶子为什么会遇到已建 PTE而导致被PERMS过滤?缺页时 PTE 不是必然无效吗?答案是并发,这正是 PERMS 掩码存在的理由。
mapping_size 与 perm_fault_granule
相等的场景(relax 分支)第 7 节展开。
6. 并发缺页与 PERMS 过滤
多个 vCPU 几乎同时访问同一个尚未建立映射的 GPA,会全部触发 Translation Fault,各自进入 map 路径建页。先到者建好映射,后到者走到叶子时发现 PTE 已是 valid。
竞态的形态取决于是否开 logging(prot 怎么算见第 4 节真值表):
- 未开 logging:读写 fault 算出的 prot 都是 RW,old == new,后到者无过滤时会用 BBM 重建一模一样的映射,纯浪费(最常见形态)
- 开 logging:prot 可能不同(写 fault RW、读 fault
RO),仅权限位有差异,无过滤时权限被夺 + 恢复 fault +
连锁缺页(下图场景)
- 写 fault:
kvm_s2_fault_compute_prot算出的 prot = RW, fault 处理中顺带记脏页 - 读 fault:
kvm_s2_fault_compute_prot算出的 prot = RO, 保持写保护不走 relax, 让未来的写还能 fault 出来被标脏。
- 写 fault:
过滤逻辑由 commit "Filter out the case of only changing permissions from stage-2 map path" 引入。落到叶子上的差异检查:
1 | /* linux/arch/arm64/include/asm/kvm_pgtable.h */ |
落到调用方 stage2_map_walker_try_leaf()
中判断:stage2_pte_needs_update() 返回假,直接
return -EAGAIN。被让出后 guest 指令由硬件重执行:
- 已建权限满足访问:vCPU 直接跑下去,无事发生
- 权限仍不满足:指令再次陷出,下一轮走 relax_perms 放权(第 7 节)
没有过滤会怎样?以 A 写 fault 先到、B 读 fault 后到为例(logging 下 A 算出 prot=RW、B 算出 prot=RO,两次 make 要写进页表的权限位不同):
后到者 B 若不过滤,会用 BBM 把 PTE 从 RW 重建为 RO。这次重建本身毫无意义(RW 页本来就满足读访问),代价却连环:
- 夺走先到者的写权限:guest 下一次写触发 Permission Fault,等 relax 恢复
- break 窗口伤及旁路:break 把 PTE 置 invalid,到 make 写入前,任何 vCPU 访问该页都会触发本不该发生的缺页
有过滤后:
PTE 一位未动(new 只是局部变量),无 TLBI、无 break 窗口,B 的读重试直接成功。上图是已建权限满足访问的结局;反过来,A 读 fault 先到建 RO、B 写 fault 后到,XOR 同样落在掩码内被过滤,但 B 重试时 RO 不满足写,触发 Permission Fault,下一轮走 relax 恢复 RW:
写权限不会被锁死,因为放权的判断依据不是 PTE 而是 VMA/memslot:PTE 上的 S2AP_W=0 只说明"赢下建图的那次没带写",不说明"写丢了"。fault 路径每轮都从事实源现算 prot(第 4 节),这是一切"写权限丢失"场景的最终兜底。
这一切只是性能优化而非正确性修复:BBM 保证时序正确,relax 对 R/W 只加不减,fault 绕几圈后总能收敛到满足所有访问的权限,不会死循环。commit message 里称无过滤时的现象为 useless small loops(无意义的小循环),PERMS 过滤省掉的就是这些循环里的 BBM/TLBI 和连锁 VM exit。
7. relax_perms:纯权限位的原子修改
Permission Fault 走到 relax 分支时(第 5
节的尺寸相等判定),kvm_pgtable_stage2_relax_perms()
只改权限位,不动输出地址:
1 | /* linux/arch/arm64/kvm/hyp/pgtable.c */ |
三个细节:
- 放权只加不减:set 里的 S2AP_R/S2AP_W 只会被置上,不会被清掉。relax 的调用者全部是"放权"语义(fault 处理、gmem 权限异常),不存在收紧场景
- XN 条件更新:不请求 X 时 set/clr 完全不碰 XN 位。写 fault 引起的 relax 只该动 S2AP,顺带改执行权限会引入预期外的行为变化("Only update XN attr when requested during S2 relaxation" 的主题)
- SW 位拒绝:nested 场景 prot 里携带的 SW 层级位(第 10 节)在这里被剥掉,PTE 里已有的 SW 位则原样保留
实际写入由 stage2_attr_walker() 完成:
1 | /* linux/arch/arm64/kvm/hyp/pgtable.c */ |
为什么 relax 敢不走 BBM? ARM 架构规定(D8.17.1,原文见附录A),多线程共用页表时的修改分两类:
| 修改类型 | 必须 BBM? | 原因 |
|---|---|---|
| 内存类型、OA、block size | 是 | TLB 可能同时持有新旧副本,行为不可预测 |
| 纯权限位放宽 | 否 | 新旧 PTE 指向同一块内存,不存在"翻译到不同内存、踩空、越界访问"的风险 |
所以 relax 一条 pte = (old & ~clr) | set
的原子更新就够。
更新之后仍需 TLBI(TLB 无效化)让新权限对后续访问可见,这是 D8.17 的要求(修改关联 VMID 的页表项后软件必须无效化对应 TLB 条目,原文见附录A)。
KVM 在 relax 成功后调用
__kvm_tlb_flush_vmid_ipa_nsh,并带上 level 作为 TTL
hint(TLB 缓存层级提示,"Fix propagation of TLBI level in
kvm_pgtable_stage2_relax_perms" 修复过它的传播)。
一般情况下,是在热迁移脏页跟踪的场景下会需要relax perms。我们前面说了,迁移完的页会执行wrprotect把原本可写页->只读页。当
guest
访问时,就会在user_mem_abort中给它relax_perms加上写权限,relax
返回后,kvm_s2_fault_map
最后会给这个页标脏,意味着下一轮迁移时需要把脏页迁走并重新wrprotect:
1 | /* linux/arch/arm64/kvm/mmu.c, kvm_s2_fault_map() */ |
写 fault 的 relax(writable 为真)顺带把页记入 memslot 的脏页位图。整条链闭环:写 fault → compute_prot 给 RW → relax 置 S2AP_W → 标脏 → TLBI → vCPU 恢复,写成功。
放权的对偶面是收权。谁会把已建映射的 S2AP_W 清掉?dirty logging,下一节。
8. dirty logging 全周期:写保护、拆分与脏页记账
dirty logging 的目标是热迁移迭代期以 4K
粒度跟踪"哪些页被写过"。开启点在 VMM 调
KVM_SET_USER_MEMORY_REGION 传
KVM_MEM_LOG_DIRTY_PAGES,落到
kvm_arch_commit_memory_region():
1 | /* linux/arch/arm64/kvm/mmu.c */ |
两种模式:
第一种:initially-all-set(脏位图初始全 1,逐批 CLEAR 时逐批写保护)适合大内存 VM 分摊停顿。
第二种:一步到位模式则开启当下全量处理。无论哪种,落到页表上的动作序列相同,先是写保护kvm_pgtable_stage2_wrprotect:
1 | kvm_arch_commit_memory_region() /* [mmu.c] */ |
kvm_pgtable_stage2_wrprotect() 复用
stage2_update_leaf_attrs(),只是这次 clr 位里放的是
S2AP_W。page 和 block 均统一清 S2AP_W,没有按 level
分化的逻辑。清完之后:
- page(4K):RO。写 fault 出来 → 标脏 + relax 恢复 RW(第 7 节闭环),4K 粒度正合跟踪需求
- block(2M):RO。若不拆,每次写都是 2M 粒度的整块恢复,跟踪粒度被放大 512 倍,而且跟标脏的粒度不一致(4KB),所以必然会丢脏页(不拆的话只能跟踪到第一个 4KB 脏页),所以必须拆
拆分大页有 eager 与 lazy 两条:
- eager split:VMM
显式开启(
KVM_CAP_ARM_EAGER_SPLIT_CHUNK_SIZE,默认 0 关闭),开启时kvm_mmu_split_memory_region()分批把 block 全部拆完。走的正是第 2 节调用树里的 split 路径:stage2_split_walker解码父 PTE 得到 prot,kvm_pgtable_stage2_create_unlinked(prot)再编码铺满子表,BBM 换上- 子页权限完全继承父 PTE,不查 VMA/memslot
1
2
3kvm_arch_commit_memory_region()
+-> kvm_mmu_wp_memory_region(kvm, new->id); /* 一步到位: 先写保护 */
+-> kvm_mmu_split_memory_region(kvm, new->id); /* 再拆大页 */
- 子页权限完全继承父 PTE,不查 VMA/memslot
- lazy split:guest 真实写到 block 时触发。logging 下
kvm_s2_resolve_vma_size()强制max_map_size = PAGE_SIZE,写 fault 的 mapping_size(4K) 不等于 perm_fault_granule(2M)(第 5 节分叉),于是走 map 路径。map walker 里 try_leaf 返回 -E2BIG,造新空表替换 block:fault 页当次落位,其余子页留 invalid 等各自的 fault- 子页 prot 由本次 fault 现算(写 fault 给 RW +
标脏)
1
2
3
4
5
6
7
8
9
10user_mem_abort() /* [mmu.c] 写 fault 落在 2M RO block 上 */
+-> kvm_s2_fault_map()
+-> kvm_pgtable_stage2_map() /* mapping_size(4K) != granule(2M): 走 map (第 5 节分叉) */
| +-> stage2_map_walker() /* LEAF 回调 */
| +-> stage2_map_walker_try_leaf() /* 4K 请求装不进 2M block: -E2BIG */
| +-> zalloc_page(memcache) /* 造新空表 */
| +-> stage2_try_break_pte() /* BBM-break: block 失效 */
| +-> stage2_make_pte(表PTE) /* BBM-make: 拆大页,block 换成新表 */
| /* walk 框架重读 PTE 下降进新表: fault 页当次以 RW 落位, 其余子页留 invalid */
+-> mark_page_dirty_in_slot() /* writable && !ret: 顺带记脏页 */ - 其余子页各自的 fault 建页;读 fault 建的 RO 子页再被写时,走 relax
放权:
1
2
3
4
5
6user_mem_abort() /* [mmu.c] Permission Fault, mapping_size == granule(4K) */
+-> kvm_s2_fault_map()
+-> kvm_pgtable_stage2_relax_perms() /* 尺寸相等: 原子放权 (第 7 节) */
+-> stage2_update_leaf_attrs() /* set |= S2AP_W */
+-> stage2_attr_walker() /* 原子置位, 无 BBM */
+-> mark_page_dirty_in_slot() /* 顺带记脏页 */
- 子页 prot 由本次 fault 现算(写 fault 给 RW +
标脏)
一个自然的问题:wp 已经把 block 清成 RO,写 fault 现算 prot
会不会也拿不到 W?不会。compute_prot 的 W 条件(第 4 节真值表)读的是
map_writable(VMA/memslot 事实源),logging 下写 fault 恒给
W。写保护只改 PTE 硬件位,不碰软件记录,这正是 fault
能恢复权限的原因,也是第 6 节"不丢写"论证的同一逻辑。
全周期时间线:
1 | 迁移开始: |
迭代期的循环是 wp 与 fault 的拉锯:wp 收权制造 fault 机会,fault 标脏后放权,CLEAR 再收权。这套机制的全部收支都记在脏页位图上,VMM 读走后用于增量拷贝。
9. 解码器 kvm_pgtable_stage2_pte_prot 与调用点
编码的反方向是解码:软件拿到一个已建
PTE,读回它的权限。(一般拆大页我们就需要读原始 block 的权限作为新 4K
页的权限),kvm_pgtable_stage2_pte_prot()
是编码器的严格逆运算,不改写页表:
1 | /* linux/arch/arm64/kvm/hyp/pgtable.c */ |
解码器 kvm_pgtable_stage2_pte_prot 的调用点按消费的位分三种:
1 | stage2_map_walker_try_leaf() / stage2_attr_walker() /* [pgtable.c] 执行族: 只取 X 位, icache 判断用 */ |
主线是两态世界(RW/RO),编码器产出什么解码器就读回什么,不丢权限。唯一的"降级"来自
wp 在拆分前先清了 S2AP_W(第 8
节),但那是拿到kvm_pgtable_stage2_pte_prot返回值之后的收权动作,不是解码损失。
10. nested:影子 stage-2 的权限
嵌套虚拟化(L0 上跑 L1,L1 里再跑 L2)时,L2 的内存翻译要过两张 stage-2:L1 为 L2 建的 S2,加上 L0 为 L1 建的 S2。KVM 的做法是软件遍历 guest 自建的页表,把两张表的翻译合成一张影子 stage-2(shadow stage-2),权限取两者的交集。
入口在第 3 节分诊树里:kvm_walk_nested_s2() 软件模拟硬件
walk,走 L1 给 L2 建的表,把 guest S2 描述符的 S2AP 读进
out->readable/writable:
1 | /* linux/arch/arm64/kvm/nested.c, walk_nested_s2_pgd() 尾部 */ |
guest 表说"不可写"时,L2 的写 fault 应该注回 L1 处理(L1 可能正等着给 L2 做 demand paging(按需分页)),L0 不越俎代庖:
1 | /* linux/arch/arm64/kvm/nested.c, kvm_s2_handle_perm_fault() */ |
guest 表放行的访问,才允许继续建影子映射,但权限要收窄到两张表的交集(L0 自己的 S2 也要允许):
1 | /* linux/arch/arm64/kvm/mmu.c */ |
kvm_encode_nested_level() 把 guest 翻译的 level 写进
prot 的 SW0/SW1 两位(KVM_NV_GUEST_MAP_SZ),随编码器直通进
PTE 的 SW 区,供 TLBI 时反查这条映射对应 guest 表的哪一级。这也解释了第
7 节 relax 的 SW 位拒绝:relax 换入的 prot 若夹带 SW 位会污染 PTE
里已有的层级记录。
写保护同样覆盖影子表(第 8 节树里的
kvm_nested_s2_wp()),dirty logging 对 L2 一样成立。
11. ptdump:肉眼读权限
调试时想看一个 VM 的 stage-2 权限实况,ptdump(debugfs 的页表转储工具)把位翻译成字符列:
1 | /* linux/arch/arm64/kvm/ptdump.c */ |
dump 出一行映射,扫一眼就能对上全文分析:RW 是普通可写页,R 是写保护或只读槽。
附录A: ARM规则索引
ARM DDI 0487 D8.17 — 页表项修改后的 TLB 无效化义务
When a translation table entry associated with a specific VMID or ASID is modified, software is required to invalidate the corresponding TLB entry to ensure that the modified translation table entry is visible to subsequent execution, including speculative execution.
中文: 关联特定 VMID 或 ASID 的页表项被修改后, 软件必须无效化对应 TLB 条目, 确保修改后的页表项对后续执行(包括投机执行)可见。
ARM DDI 0487 D8.17.1 — 不遵守 BBM 的后果
If translation table entries are changed without appropriate TLB maintenance operations, including in the case where use of the break-before-make sequence is required but software does not follow the break-before-make sequence, it is possible that TLBs concurrently hold multiple different copies of those translation table entries. In this situation, the following behaviors are permitted for a speculative or architectural access to the address resolved by those TLB entries:
- Use of the address matches multiple entries in a TLB, and a TLB conflict abort is detected. In this case, no access is made to memory based on those TLB entries. If the access is architectural, then the TLB conflict abort is reported as an exception.
- The resulting behavior is CONSTRAINED UNPREDICTABLE, and gives a behavior consistent with translation using one of the matching entries, or an amalgamation of more than one of the matching entries, but cannot permit access to memory regions with permissions or attributes that would not be possible in the current Security state at the current Exception level.
中文: 页表项在缺少适当 TLB 维护(含要求 BBM 而未走 BBM)的情况下被修改时, TLB 可能同时持有多个不同副本。此时对相应地址的投机或架构访问允许的行为: (1) 命中多条 TLB 条目, 检测出 TLB conflict abort(架构访问则报告为异常, 不访问内存); (2) 行为 CONSTRAINED UNPREDICTABLE,翻译结果与使用某一条或若干条的混合一致, 但不得访问超出当前 Security state 与 Exception level 下本不可能的权限或属性的内存区域。
ARM DDI 0487 D8.4.2.1.1 — stage-2 S2AP 数据访问权限编码(Table D8-76)
The following table shows encoding of the S2AP descriptor field: S2AP[1:0] = 00, No data access; 01, RO; 10, WO; 11, RW. If an attempt is made to access memory that is not permitted by the value of the S2AP field, then a stage 2 Permission fault is generated.
中文: S2AP 描述符字段的编码为:00 = 无数据访问,01 = 只读,10 = 只写,11 = 读写。若访问不为 S2AP 字段值所允许,则产生 stage-2 Permiss