如果段错误不可恢复,为啥称为错误(而不是中止)?

Posted

技术标签:

【中文标题】如果段错误不可恢复,为啥称为错误(而不是中止)?【英文标题】:Why are segfaults called faults (and not aborts) if they are not recoverable?如果段错误不可恢复,为什么称为错误(而不是中止)? 【发布时间】:2018-08-29 23:36:12 【问题描述】:

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

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

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

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

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

为什么我们称它为segmentation fault 而不是segmentation abort

分段错误 是当您的程序尝试访问内存时 不是由操作系统分配的,或者是其他的 不允许访问。

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

【问题讨论】:

我在那些定义中没有看到任何说明故障应该是可恢复的。它提到 陷阱 是但留下了未解决的故障问题。至于叫什么,只是一个命名的东西,你不妨问为什么有些人会区分番茄酱、番茄酱和酱汁:-) 【参考方案1】:

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

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

x86 手册将此称为 #PF(fault-code) 异常,例如见the list of exceptions add can raise。有趣的事实:在段限制之外访问的 x86 例外是 #GP(0)

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

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

在整理完以上任何一项后,更新 CPU 自行读取的页表,必要时使该 TLB 条目无效。 (例如有效但只读更改为有效+读写)。

只有当内核发现进程在逻辑上确实没有任何东西映射到该地址(或者它是对只读映射的写入)时,内核才会提供 SIGSEGV到过程。 这纯粹是软件的事情,在梳理了硬件异常的原因之后。


SIGSEGV (from strerror(3)) 的英文文本在所有 Unix/Linux 系统上都是“Segmentation Fault”,所以当子进程因此而死时(由 shell)打印出来信号。

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

请注意,您还会获得一个 SIGSEGV,用于尝试在用户空间中执行特权指令(如 wbinvdwrmsr (write model-specific register))。在 CPU 级别,当您不在 ring 0(内核模式)时,x86 异常是 #GP(0) 的特权指令。

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


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

可恢复的。它不会使整个机器/内核崩溃,它只是意味着用户空间进程试图做一些内核不允许的事情。

即使是出现段错误的进程也可以恢复。这就是为什么它是一个可捕获的信号,不像SIGKILL。通常你不能只恢复执行,但你可以有用地记录错误发生在哪里(例如打印精确的异常错误消息,甚至是堆栈回溯)。

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

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


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

Java JVM 可以将 JVM 的线程接收到的 SIGSEGV 转换为正在运行的 Java 代码的 NullPointerException,这对 JVM 没有任何问题。

Effective Null Pointer Check Elimination Utilizing Hardware Trap 来自三位 IBM 科学家的 Java 研究论文。

SableVM: 6.2.4 Hardware Support on Various Architectures 关于空指针检查

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

Implicit Java Array Bounds Checking on 64-bit Architectures。他们讨论了当数组大小不是页面大小的倍数时该怎么做,以及其他注意事项。

陷阱与中止

我认为没有标准术语可以区分。这取决于你在谈论什么样的恢复。显然,在用户空间可以使硬件完成任何操作后,操作系统可以继续运行,否则非特权用户空间可能会使机器崩溃。

相关:开启 When an interrupt occurs, what happens to instructions in the pipeline?,Andy Glew(从事英特尔 P6 微架构工作的 CPU 架构师)说,“陷阱”基本上是由正在运行的代码(而不是外部信号)引起的任何中断,并且同步发生。 (例如,当一条错误指令到达流水线的退出阶段而没有先检测到早期的分支错误预测或其他异常时)。

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

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

SIGABRT can be caught by a signal handler。如果处理程序返回,则进程退出,因此如果您不希望这样,您可以longjmp 退出它。但是 AFAIK 没有错误条件会引发 SIGABRT;它只能由软件手动发送,例如通过调用abort() 库函数。 (这通常会导致堆栈回溯。)


x86 异常术语

如果你看 x86 手册或this exception table on the osdev wiki,在这个上下文中有特定的含义(thanks to @MargaretBloom for the descriptions):

trap:在一条指令成功完成后引发,返回地址指向陷阱 inst 之后。 #DB 调试和 #OF 溢出 (into) 异常是陷阱。 (Some sources of #DB are faults instead)。但是int 0x80 或其他软件中断指令也是陷阱,syscall 也是陷阱(但它把返回地址放在rcx 中而不是推送它;syscall 不是一个例外,因此不是真正的陷阱感觉)

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

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

请注意,即使是像 Andy Glew 这样的英特尔 CPU 架构师有时也会更普遍地使用“陷阱”一词,我认为这意味着在使用讨论计算机架构理论时任何同步异常。不要期望人们会坚持使用上述术语,除非您实际上是在谈论处理 x86 上的特定异常。尽管它是有用且合理的术语,但您可以在其他情况下使用它。但是,如果您想进行区分,您应该澄清每个术语的含义,以便每个人都在同一页面上。

【讨论】:

当我引起你的注意时,就像一个快速的理智检查一样。您认为我在术语部分中自己的“定义”是否正确?或者你不同意其中的一些观点? @AlanSTACK:不是真的。你的陷阱定义太窄了。例如,Andy Glew 说(在我链接的答案中)异常可以是“例如页面错误或 未定义的指令陷阱”。我想你可以说硬件页面错误一个陷阱。该术语包括但不限于进行系统调用的故意陷阱,例如 x86 的 intsyscall 指令。 (int 是一个不幸的名字。trap 会是一个比软件中断更好的名字。MMIX's equivalent instruction is called trap。) @AlanSTACK:如果你在谈论 CPU,它不会假设用户空间和内核空间如何相互交互/信任,也不了解进程。因此,除了硬件故障之外,将任何东西称为abort 是没有意义的。 (但x86 calls those "machine-check exceptions" 用于监控 ECC 内存和其他内容)。至少,内核总是可以打印导致错误的原因,或者让调试器挂钩异常,以便您可以手动修改代码或数据,这样重试时不会出错。 如果答案仅与 Intel 机器有关,那么术语 trapabortfault 具有精确的定义: 在一条指令成功完成后引发一个trap,返回地址指向陷阱inst 之后(只有#DB e #OF 是陷阱)。 fault 在尝试执行然后回滚后引发,返回地址指向错误的 inst(几乎所有东西都是陷阱)。 abort 是指返回地址指向不相关的位置(即#DF 和#MC)。所以#UD 不能成为陷阱。 @MargaretBloom:谢谢,最后是一些有意义的精确定义。在底部添加了 x86 异常术语部分。 (希望我基本上是正确的;我并没有真正做 x86 操作系统开发的东西,我只是在 SO 上读到它:P)【参考方案2】:

有两种类型的异常:故障和陷阱。当发生故障时,可以重新启动指令。当陷阱发生时,指令不能重新启动。

例如,当页面错误发生时,操作系统异常处理程序会加载丢失的页面并重新启动导致错误的指令。

如果处理器定义了“分段错误”,则导致异常的指令是可重新启动的,但操作系统的处理程序可能不会重新启动指令。

【讨论】:

我的理解是错误的还是你把trapabort混淆了? @AlanSTACK:我认为这个答案是使用trap 来谈论像intsyscall 这样引发异常的指令,但这是有意的,内核应该返回指令之后。但是错误(如页面错误)应在解决问题后通过重新运行指令来处理。这不是很好的术语,因为您将什么称为非法指令异常?您可以重新启动它,但它会导致另一个异常,除非异常处理程序修改了代码字节或跳转到其他地方。 如果这些术语有标准的计算机体系结构或操作系统定义,那么如果澄清定义的上下文和确切含义,这个答案会更好。实际上,它主要是说硬件页面错误与 SIGSEGV 不同,但不是很明确。

以上是关于如果段错误不可恢复,为啥称为错误(而不是中止)?的主要内容,如果未能解决你的问题,请参考以下文章

为啥大型静态数组会产生段错误而动态却不会? (C++)

线程被中止错误

为啥这段代码试图调用复制构造函数?

为啥写作主要;在 C 中给出一个段错误

为啥我的代码在 Windows 7 上不会出现段错误?

什么时候最好在看门狗线程中导致段错误而不是正常退出以停止进程?