如果段错误不可恢复,为什么将其称为错误(而不是中止)?

Ala*_*ACK 3 x86 operating-system kernel x86-64 interrupt

我对术语的以下理解是这样的

1) 中断
是由硬件发起的“通知”,用于调用操作系统运行其处理程序

2) 陷阱
是由软件发起的“通知”,用于调用操作系统运行其处理程序

3) 故障
是处理器在发生错误但可恢复时引发的异常

4) 中止
是处理器在发生错误但不可恢复时引发的异常

为什么我们称其为asegmentation fault而不是a segmentation abortthen?

分段错误
是指当您的程序尝试访问操作系统尚未分配的内存或不允许访问的内存时。

我的经验(主要是在测试C代码时)是,每当程序抛出异常时,它segmentation fault都会回到绘图板 - 是否存在程序员实际上可以“捕获”异常并用它做一些有用的事情的场景?

Pet*_*des 7

在 CPU 级别,现代操作系统不使用 x86 段限制来保护内存。(事实上​​,即使他们想在长模式(x86-64)下也不能这样做;段基数固定为 0,限制为 -1)。

操作系统使用虚拟内存页表,因此越界内存访问的真正 CPU 异常是页面错误。

x86 手册将此称为#PF(fault-code)异常,例如,请参阅可以引发的异常列表add。有趣的事实:超出段限制访问的 x86 异常是#GP(0).

由操作系统的页面错误处理程序决定如何处理它。许多#PF异常是正常操作的一部分发生的:

  • 写时复制映射已写入:复制页面并将其在页表中标记为可写,然后返回用户空间重试出错的指令。(这是一种“软”页面错误,也称为“轻微”页面错误。)
  • 其他软页面错误,例如内核很懒,实际上没有更新页表以反映进程所做的映射。(例如mmap(2)没有MAP_POPULATE)。
  • 硬页面错误:找到一些物理内存并从磁盘读取文件(文件映射或匿名页面的交换文件/分区)。

解决上述任何问题后,更新CPU自行读取的页表,并在必要时使该TLB条目无效。(例如有效但只读更改为有效+读写)。

仅当内核发现进程实际上在逻辑上没有任何映射到该地址(或者它是对只读映射的写入)时,内核才会将 a 传递给进程SIGSEGV这纯粹是软件的事情,在整理了硬件异常的原因之后。


SIGSEGV( fromstrerror(3) )的英文文本在所有 Unix/Linux 系统上都是“SegmentationFault”,因此当子进程因该信号而死亡时(由 shell)会打印出这样的内容。

这个术语很好理解,因此尽管它主要只是由于历史原因而存在并且硬件不使用分段。

请注意,您还会获得 SIGSEGV 来执行诸如尝试在用户空间中执行特权指令之类的操作(例如wbinvdwrmsr(写入特定于模型的寄存器))。在 CPU 级别,x86 例外是#GP(0)当您不在环 0(内核模式)时的特权指令。

同样对于未对齐的 SSE 指令(如movaps),尽管其他平台上的某些 Unix 会发送SIGBUS未对齐的访问错误(例如 SPARC 上的 Solaris)。


那么为什么我们称其为分段错误而不是分段中止呢?

可以恢复的。它不会使整个机器/内核崩溃,它只是意味着用户空间进程试图执行内核不允许的操作。

即使对于出现段错误的进程来说,它也是可以恢复的。这就是为什么它是一个可捕获的信号,与SIGKILL. 通常,您不能只是恢复执行,但您可以有效地记录错误所在(例如,打印精确的异常错误消息,甚至堆栈回溯)。

SIGSEGV 的信号处理程序可以longjmp或其他什么。或者,如果需要 SIGSEGV,则在从信号处理程序返回之前修改用于加载的代码或指针。(例如,对于 Meltdown 漏洞利用,尽管有更有效的技术可以在错误预测或其他抑制异常的情况下执行链式加载,而不是实际让 CPU 引发异常并捕获内核提供的 SIGSEGV)

大多数编程语言(汇编语言除外)都不够低级,无法在围绕可能出现段错误的访问进行优化时提供明确定义的行为,从而让您编写可恢复的处理程序。这就是为什么如果您安装了 SIGSEGV 处理程序,通常除了在 SIGSEGV 处理程序中打印一条错误消息(可能还有堆栈回溯)之外,您不会做任何其他事情。


一些沙盒语言(如 Javascript)的 JIT 编译器使用硬件内存访问检查来消除 NULL 指针检查。在正常情况下,没有故障,因此故障情况下速度有多慢并不重要。

Java JVM 可以将SIGSEGVJVM 线程接收到的a 转换NullPointerException为它正在运行的 Java 代码的 a,对于 JVM 来说不会出现任何问题。

另一个技巧是将数组的末尾放在页面的末尾(后面是足够大的未映射区域),因此每次访问的边界检查都是由硬件免费完成的。如果您可以静态地证明索引始终为正,并且它不能大于 32 位,那么您就一切就绪了。


陷阱与中止

我认为没有标准术语来区分。这取决于你所说的恢复类型。显然,在用户空间可以让硬件执行任何操作后,操作系统可以继续运行,否则非特权用户空间可能会使机器崩溃。

相关:On 当中断发生时,流水线中的指令会发生什么?,Andy Glew(曾参与英特尔 P6 微架构的 CPU 架构师)表示,“陷阱”基本上是由正在运行的代码(而不是外部信号)引起的任何中断,并且是同步发生的。(例如,当故障指令到达流水线的引退阶段而没有首先检测到较早的分支错误预测或其他异常时)。

“中止”不是标准的 CPU 架构术语。就像我说的,你希望操作系统无论如何都能继续运行,通常只有硬件故障或内核错误才能阻止这种情况。

AFAIK,“中止”也不是非常标准的操作系统术语。Unix 有信号,其中一些是无法捕获的(如 SIGKILL 和 SIGSTOP),但大多数可以捕获。

SIGABRT可以被信号处理程序捕获。如果处理程序返回,该进程就会退出,因此如果您不希望这样,您可以longjmp退出它。但据我所知,没有错误条件引发 SIGABRT;它只能通过软件手动发送,例如通过调用abort()库函数。(它通常会导致堆栈回溯。)


x86 异常术语

如果您查看 x86 手册或osdev wiki 上的异常表,就会发现在这种情况下有特定的含义(感谢 @MargaretBloom 的描述):

  • trap:指令成功完成后引发,返回地址指向陷阱实例之后。 #DBdebug 和#OF溢出 ( into) 异常都是陷阱。(#DB 的某些来源是错误的)。但是int 0x80或者其他软件中断指令也是陷阱syscall(但它把返回地址放入rcx而不是压入它;syscall不是例外,因此从这个意义上来说并不是真正的陷阱)

  • 错误:尝试执行后引发,然后回滚;返回地址指向错误指令。(大多数异常类型都是故障)

  • 中止是指返回地址指向不相关的位置(即#DF双故障和#MC机器检查)。三重故障无法处理;当 CPU 尝试运行双故障处理程序时遇到异常,并且确实停止了整个 CPU 时,就会发生这种情况。

请注意,即使像 Andy Glew 这样的 Intel CPU 架构师有时也会更普遍地使用术语“陷阱”,我认为在讨论计算机架构理论时,它指的是任何同步异常。不要指望人们会坚持使用上述术语,除非您实际上谈论的是处理 x86 上的特定异常。尽管它是有用且明智的术语,并且您可以在其他上下文中使用它。但如果你想做出区分,你应该澄清每个术语的含义,以便每个人都达成共识。