在 CPU 级别,现代操作系统不使用 x86 段限制来保护内存。 (事实上,即使他们想在长模式(x86-64)下也不能这样做;段基数固定为 0,限制为 -1)。
操作系统使用虚拟内存页表,因此越界内存访问的真正 CPU 异常是页面错误。
x86 手册称其为#PF(fault-code)
例外,例如看例外情况清单add可以提高。有趣的事实:超出段限制访问的 x86 异常是#GP(0)
.
由操作系统的页面错误处理程序决定如何处理它。许多#PF
异常发生是正常操作的一部分:
- 写时复制映射已写入:复制页面并将其在页表中标记为可写,然后返回用户空间重试出错的指令。 (这是一种“软”页面错误,也称为“轻微”页面错误。)
- 其他软页面错误,例如内核很懒,实际上没有更新页表来反映进程所做的映射。 (例如。mmap(2)没有
MAP_POPULATE
).
- 硬页面错误:找到一些物理内存并从磁盘读取文件(文件映射或匿名页面的交换文件/分区)。
解决上述任何问题后,更新CPU自行读取的页表,并在必要时使该TLB条目无效。 (例如,有效但只读更改为有效+读写)。
仅当内核发现该进程在逻辑上确实没有任何映射到该地址(或者它是对只读映射的写入)时,内核才会传递SIGSEGV
到这个过程。这纯粹是一个软件的事情,整理出硬件异常的原因后。
英文文本为SIGSEGV
(from strerror(3)) 是“分段错误”在所有 Unix/Linux 系统上,这就是当子进程因该信号而死亡时(由 shell)打印的内容。
这个术语很好理解,因此尽管它主要只是由于历史原因而存在并且硬件不使用分段。
请注意,对于尝试在用户空间中执行特权指令(例如wbinvd
or wrmsr(写入特定型号寄存器))。在 CPU 级别,x86 例外是#GP(0)
当您不在环 0(内核模式)时获取特权指令。
也适用于未对齐的 SSE 指令(例如movaps
),尽管其他平台上的一些 Unix 会发送SIGBUS
用于未对齐的访问故障(例如 SPARC 上的 Solaris)。
那么为什么我们称其为分段错误而不是分段中止呢?
It is可恢复的。它不会使整个机器/内核崩溃,它只是意味着用户空间进程试图执行内核不允许的操作。
即使对于出现段错误的进程can是可以恢复的。这就是为什么它是一个可捕捉的信号,与SIGKILL
。通常,您不能只是恢复执行,而是可以有效地记录错误所在(例如,打印精确的异常错误消息,甚至堆栈回溯)。
SIGSEGV 的信号处理程序可以longjmp
管他呢。或者,如果需要 SIGSEGV,则在从信号处理程序返回之前修改用于加载的代码或指针。 (例如。对于 Meltdown 漏洞利用,尽管有更有效的技术可以在错误预测或其他抑制异常的情况下执行链式加载,而不是实际让 CPU 引发异常并捕获内核提供的 SIGSEGV)
大多数编程语言(汇编语言除外)都不够低级,无法在围绕可能出现段错误的访问进行优化时提供明确定义的行为,从而让您编写可恢复的处理程序。这就是为什么如果您安装了 SIGSEGV 处理程序,通常除了在 SIGSEGV 处理程序中打印一条错误消息(可能还有堆栈回溯)之外,您不会做任何其他事情。
一些沙盒语言(如 Javascript)的 JIT 编译器使用硬件内存访问检查来消除 NULL 指针检查。在正常情况下,没有故障,因此故障情况下速度有多慢并不重要。
Java JVM 可以将SIGSEGV
由 JVM 的线程接收到NullPointerException
对于它运行的 Java 代码来说,JVM 没有任何问题。
另一个技巧是将数组的末尾放在页面的末尾(后面是足够大的未映射区域),因此每次访问的边界检查都是由硬件免费完成的。如果您可以静态地证明索引始终为正,并且它不能大于 32 位,那么您就一切就绪了。
-
64 位上的隐式 Java 数组边界检查
架构。他们讨论了当数组大小不是页面大小的倍数时该怎么做,以及其他注意事项。
陷阱与中止
我认为没有标准术语来区分。这取决于你所说的恢复类型。显然,在用户空间可以让硬件执行任何操作后,操作系统可以继续运行,否则非特权用户空间可能会使机器崩溃。
相关:开当中断发生时,流水线中的指令会发生什么情况?,Andy Glew(曾参与英特尔 P6 微架构的 CPU 架构师)表示,“陷阱”基本上是由正在运行的代码(而不是外部信号)引起的任何中断,并且是同步发生的。 (例如,当故障指令到达流水线的退休阶段而没有首先检测到较早的分支错误预测或其他异常时)。
“中止”不是标准的 CPU 架构术语。就像我说的,您希望操作系统无论如何都能继续运行,通常只有硬件故障或内核错误才能阻止这种情况。
AFAIK,“中止”也不是非常标准的操作系统术语。 Unix 有信号,其中一些是无法捕获的(如 SIGKILL 和 SIGSTOP),但大多数可以捕获。
SIGABRT可以被信号处理程序捕获。如果处理程序返回,进程就会退出,所以如果您不希望这样,您可以longjmp
出来了。但据我所知,没有错误条件引发 SIGABRT;它只能通过软件手动发送,例如通过致电abort()
库函数。 (它通常会导致堆栈回溯。)
x86 异常术语
如果您查看 x86 手册或osdev wiki 上的此异常表,在这种情况下有特定的含义(感谢@MargaretBloom 的描述):
-
trap:指令成功完成后引发,返回地址指向捕获实例之后。#DB
调试和#OF
溢出(into
) 异常是陷阱。 (#DB 的某些来源是错误的)。但int 0x80
或者其他软件中断指令也是陷阱,原样syscall
(但它将返回地址放入rcx
而不是推动它;syscall
不是一个例外,因此从这个意义上来说并不是真正的陷阱)
-
fault:尝试执行后引发,然后回滚;返回地址指向错误指令。 (大多数异常类型都是故障)
-
abort是当返回地址指向不相关的位置时(即#DF
双故障和#MC
机器检查)。三重故障无法处理;当 CPU 尝试运行双故障处理程序时遇到异常,并且确实停止了整个 CPU 时,就会发生这种情况。
请注意,即使像 Andy Glew 这样的 Intel CPU 架构师有时也会更普遍地使用术语“陷阱”,我认为在讨论计算机架构理论时,它的意思是任何同步异常。不要指望人们会坚持使用上述术语,除非您实际上谈论的是处理 x86 上的特定异常。尽管它是有用且明智的术语,并且您可以在其他上下文中使用它。但如果你想做出区分,你应该澄清每个术语的含义,以便每个人都达成共识。