我试图弄清楚是否有可能运行一个其RAM仅由单个物理页面支持的Linux VM。
为了模拟这种情况,我修改了KVM中的嵌套页错误处理程序,从所有嵌套页表(NPT)条目中删除当前位,除了与当前处理的页错误相对应的条目。
在尝试启动一个Linux客户机时,我观察到使用内存操作数的汇编指令,如
add [rbp+0x820DDA], ebp
导致页面错误循环,直到我恢复包含指令的页面以及操作数中引用的页面的当前位(在本例中为[rbp+0x820DDA]
)。
我想知道为什么会这样。难道CPU不应该顺序地访问内存页面吗?首先读取指令,然后访问内存操作数?或者x86是否要求指令页和所有操作数页都可以同时访问?
AMD Zen 1测试
1条答案
按热度按时间jhdbpxl91#
是的,它们需要机器码和所有内存操作数。
难道CPU不应该顺序地访问内存页面吗?首先读取指令,然后访问内存操作数?
是的,这是逻辑上会发生的事情,但是页面错误异常会中断这两个步骤的过程并丢弃任何进度。CPU没有任何方法来记住页面错误发生时它正在执行的是什么指令。
当页面错误处理程序在处理了一个有效的页面错误后返回时,RIP=错误指令的地址,因此CPU会重新尝试 * 从头开始 * 执行它。
操作系统修改出错指令的机器码并期望它执行来自页面错误处理程序(或任何其他异常或中断处理程序)的
iret
之后的不同指令是法律的。所以AFAIK,在你所说的情况下,从架构上来说,CPU需要从CS:RIP重新获取代码。(假设它甚至返回到故障CS:RIP,而不是在等待磁盘出现硬页故障时调度另一个进程,或者在出现无效页故障时向信号处理程序发送SIGSEGV。它可能也是hypervisor进入/退出在架构上所需要的。即使它没有在纸上被明确禁止,这也不是CPU的工作方式。
@torek评论说 * 一些(CISC)微处理器部分解码指令并在页面错误时转储微寄存器状态 ,但x86不是这样的。m68 k确实做到了这一点,例如,在未对齐存储的一些但不是所有字节已经进入存储器之后,让它在存储器间接存储的中间发生页面错误,从而将指针重新定位到所使用的存储器间接寻址模式。(x86没有内存间接寻址,错误发生在指令边界。
一些指令是可中断的,可以进行部分处理,如
rep movs
(罐中的memcpy)和其他字符串指令,或收集加载/分散存储。但唯一的机制是更新架构寄存器,如用于字符串操作的RCX / RSI / RDI,或用于聚集/分散的目的地和掩码寄存器(如AVX2vpgatherdd
)。不保留操作码/解码会导致一些隐藏的内部寄存器,并在iret
之后从页面错误处理程序重新启动它。这些是执行多个单独数据访问的指令。当故障发生时更新架构寄存器状态的那些指令的语义使得用新reg值从头开始重新执行产生相同的最终结果,就好像它已经执行了整个过程而没有故障一样。
还要记住,x86(像大多数ISA一样)保证指令是原子写的。中断和异常:在中断之前,它们要么完全发生,要么根本不发生。 中断正在运行的汇编指令 *。因此,例如,如果存储部分发生故障,即使没有
lock
前缀,也需要add [mem], reg
来丢弃负载。(ISAs类似于M68 K,在堆栈上保存/恢复用于部分进程微体系结构状态仍然只在指令边界处采用外部中断,而不采用像页面错误的异常。显然,一条m68 k指令最坏的情况下可以接触到的页面数是16,但它们不必同时驻留,所以它更接近于保存部分进度的x86 gather或rep movs。
保证转发进度的最小驻留页面数
最坏的情况下,用于向前推进的客户机用户空间页面数可能是6(加上每个页面的单独客户机内核页面表子树):
movsq
或movsw
2字节指令跨越页边界,因此需要两个页才能解码。[rsi]
也是一个页拆分[rdi]
也是一个页拆分如果这6页中有任何一页出错,我们就得从头开始。
rep movsd
也是一个2字节的指令,在其中的一个步骤上执行也有同样的要求。类似的情况,如push [mem]
或pop [mem]
,可以用未对齐的堆栈来构建。使聚集加载/分散存储“可中断”(用其进度更新掩码向量)的原因之一(或附带好处)是避免增加执行单个指令的最小占用空间。还提高了在一次聚集或分散期间处理多个故障的效率。
正如在评论中指出的那样,客户内核的页面错误处理程序中的
iret
可能也必须存在,但是如果我们谈论的是嵌套的页面表和hypervisor玩的把戏,它可以交换出一个客户内核页面来支持rep movsd
本身所需的6个客户页面。这些页面错误是针对管理程序的,而不是针对客户内核的,客户内核认为它拥有所有驻留的页面和自己的内核内存。@布兰登在评论中指出,一个客户机将需要它的页表在内存中,用户空间的页分割也可以是1GiB分割,这样两边就在顶层PML 4的不同子树中。HW页面遍历将需要触及所有这些客户机页表页面才能取得进展。这种病态的情况不太可能偶然发生。
TLB(和页面遍历器内部)被允许缓存一些页面表数据,并且不需要从头开始重新启动页面遍历,除非操作系统做了
invlpg
或设置了新的CR 3顶级页面目录。当将页面从不存在更改为存在时,这两个都不是必需的; x86在纸面上保证它是不需要的(因此不允许对不存在的PTE进行“负缓存”,至少对软件不可见)。因此,即使某些访客物理页表页实际上不存在,CPU也可能不会VM退出。PMU性能计数器可以启用和配置,以便指令还需要perf事件才能写入PEBS缓冲区。如果将计数器的掩码配置为只计数用户空间指令,而不计数内核指令,那么很可能它会一直尝试使计数器溢出,并在每次返回用户空间时将样本存储在缓冲区中,从而产生页面错误。