sav*_*bug 68 virtualization paging x86 assembly kvm
我试图弄清楚是否有可能运行一个 Linux VM,其 RAM 仅由单个物理页面支持。
为了模拟这一点,我修改了 KVM 中的嵌套页错误处理程序,以从所有嵌套页表 (NPT) 条目中删除当前位,但与当前处理的页错误对应的条目除外。
在尝试启动 Linux 客户机时,我观察到使用内存操作数的汇编指令,例如
add [rbp+0x820DDA], ebp
Run Code Online (Sandbox Code Playgroud)
导致页面错误循环,直到我恢复包含指令的页面以及操作数中引用的页面的当前位(在本例中[rbp+0x820DDA])。
我想知道为什么会这样。CPU不应该按顺序访问内存页,即先读取指令,然后访问内存操作数吗?还是 x86 要求指令页和所有操作数页可以同时访问?
我正在 AMD Zen 1 上进行测试。
Pet*_*des 59
是的,它们确实需要机器代码和所有内存操作数。
CPU不应该按顺序访问内存页,即先读取指令,然后访问内存操作数吗?
是的,逻辑上会发生这种情况,但是页面错误异常会中断该两步过程并丢弃任何进度。CPU 无法记住发生页面错误时它在中间的指令是什么。
当页面错误处理程序在处理了有效页面错误后返回时,RIP= 错误指令的地址,因此 CPU 重试从头开始执行它。
操作系统修改错误指令的机器代码并期望它iret在页面错误处理程序(或任何其他异常或中断处理程序)之后执行不同的指令是合法的。因此,AFAIK 在架构上要求 CPU 从 CS:RIP 重新执行代码提取,以防万一。(假设它甚至确实返回到有故障的 CS:RIP,而不是在等待磁盘出现硬页面错误时调度另一个进程,或者在无效页面错误时将 SIGSEGV 传递给信号处理程序。)
管理程序进入/退出在架构上也可能是必需的。即使纸面上没有明确禁止,这也不是 CPU 的工作方式。
@torek 评论说,某些 (CISC) 微处理器会在页面错误时部分解码指令并转储微寄存器状态,但 x86 并非如此。
一些指令是可中断的并且可以取得部分进展,例如rep movs(罐中的 memcpy)和其他字符串指令,或收集负载/分散存储。但唯一的机制是更新架构寄存器,如用于字符串操作的 RCX / RSI / RDI,或用于收集的目标和掩码寄存器(例如AVX2 的vpgatherdd手册)。不将操作码/解码结果保留在一些隐藏的内部寄存器中,并在从页面错误处理程序 iret 之后重新启动它。这些是执行多个单独数据访问的指令。
还要记住,x86(像大多数 ISA 一样)保证指令是原子的。中断/异常:在中断之前,它们要么完全发生,要么根本不发生。 正在运行时中断汇编指令。因此,例如add [mem], reg,如果存储部分出现故障,即使没有lock前缀,也需要丢弃负载。
在最坏的情况下,存在的来宾用户空间页面数量可能是 6 个(加上每个单独的来宾内核页表子树):
movsq或movsw跨页边界的 2 字节指令,因此需要两个页才能解码。[rsi]也是分页[rdi]也是分页如果这 6 页中的任何一页出现错误,我们将回到第一页。
rep movsd也是一个 2 字节的指令,在它的一个步骤上取得进展会有相同的要求。类似的情况,例如push [mem]或pop [mem]可以用未对齐的堆栈构造。
使收集加载/分散存储“可中断”(用其进度更新掩码向量)的原因(或附带好处)之一是避免增加此最小占用空间以执行单个指令。还可以提高在一次聚集或分散期间处理多个故障的效率。
@Brandon 在评论中指出来宾将需要内存中的页表,并且用户空间页面拆分也可以是 1GiB 拆分,因此两侧位于顶级 PML4 的不同子树中。HW page walk 需要接触所有这些访客页面表页面才能取得进展。这种病态的情况不太可能偶然发生。
TLB(和 page-walker 内部)被允许缓存一些页表数据,并且不需要从头开始重新启动 page-walk,除非操作系统这样做invlpg或设置了一个新的 CR3 顶级页面目录。将页面从不存在更改为存在时,这些都不是必需的;纸上的 x86 保证不需要它(因此不允许对不存在的 PTE 进行“负缓存”,至少对软件不可见)。因此,即使某些来宾物理页表页面实际上不存在,CPU 也可能不会 VMexit。
可以启用和配置PMU 性能计数器,以便该指令还需要一个 perf 事件来写入该指令的 PEBS 缓冲区。将计数器的掩码配置为仅计算用户空间指令,而不是内核,很可能每次返回用户空间时它都会尝试使计数器溢出并将样本存储在缓冲区中,从而产生页面错误。