当前位置: 首页 > 知识库问答 >
问题:

如果分段错误是不可恢复的,为什么它们被称为错误(而不是中止)?

尹臻
2023-03-14

我对术语的理解如下

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

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

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

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

为什么我们称之为分割错误而不是分割中止

分割错误
是指程序试图访问操作系统未分配或不允许访问的内存。

我的经验(主要是在测试C代码时)是,每当程序抛出分段错误时,它都会返回到绘图板上-是否存在这样一种情况,程序员可以真正“捕获”异常并对其做一些有用的事情?


共有2个答案

田仲卿
2023-03-14

有两种例外情况:断层和圈闭。发生故障时,可以重新启动指令。出现陷阱时,指令无法重新启动。

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

如果处理器定义了“分段故障”,则导致异常的指令可以重新启动,但操作系统的处理程序可能无法重新启动该指令。

后焕
2023-03-14

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

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

x86手册将此称为#PF(故障代码)异常,例如,请参阅异常列表添加可以引发的异常。有趣的事实:对于超出段限制的访问,x86例外是#GP(0)

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

  • 写入时复制映射已写入:复制页面并在页面表中将其标记为可写,然后返回到用户空间重试出错的指令。(这是一种“软”即“轻微”页面错误。)

在对上述任何内容进行排序之后,更新CPU自己读取的页表,并在必要时使该TLB条目无效。(例如,有效但只读更改为有效读写)。

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

在所有Unix/Linux系统上,SIGSEGV(来自strirror(3))的英文文本是“分段故障”,所以当一个子进程从该信号中死亡时,这就是(由shell)打印的内容。

这个术语很容易理解,所以即使它主要是因为历史原因而存在,硬件也不使用分段。

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

此外,对于未对齐的SSE指令(如movaps),尽管其他平台上的一些Unix发送未对齐访问故障的SIGBUS(例如SPARC上的Solaris)。

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

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

即使对于分段故障的过程,它也可以恢复。这就是为什么它是一个可捕捉的信号,不像SIGKILL。通常,您不能只是恢复执行,但您可以有效地记录故障发生的地方(例如,打印精确的异常错误消息,甚至堆栈回溯)。

SIGSEGV的信号处理器可以是long jmp或其他什么。或者,如果SIGSEGV是预期的,那么在从信号处理程序返回之前,修改用于加载的代码或指针。(例如,对于Meltdown漏洞利用,尽管有更有效的技术可以在错误预测或其他抑制异常的东西,而不是实际上让CPU引发异常并捕获内核提供的SIGSEGV)

大多数编程语言(除了汇编)都不够低级,不足以在优化访问时给出定义良好的行为,访问可能会以一种允许您编写恢复处理程序的方式分段错误。这就是为什么如果您安装了SIGSEGV处理程序,通常只需要在SIGSEGV处理程序中打印错误消息(可能还有堆栈回溯)。

一些沙盒语言(如Javascript)的JIT编译器使用硬件内存访问检查来消除空指针检查。在正常情况下没有故障,因此无论故障情况有多慢都没有关系。

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

>

  • 利用硬件陷阱有效消除空指针检查一篇关于Java的研究论文,由三位IBM科学家撰写。

    SableVM:6.2.4各种架构上关于空指针检查的硬件支持

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

    • 64位体系结构上的隐式Java数组边界检查。他们讨论了当数组大小不是页面大小的倍数时该怎么做,以及其他注意事项

    我认为没有标准术语来区分这一点。这取决于你说的是什么样的恢复。显然,操作系统可以在用户空间可以让硬件做的任何事情之后继续运行,否则未经授权的用户空间可能会使机器崩溃。

    相关:Andy Glew(致力于英特尔P6微体系结构的CPU架构师)说,“陷阱”基本上是由正在运行的代码(而不是外部信号)引起的、同步发生的任何中断。(例如,当故障指令到达管道的失效阶段时,没有首先检测到早期分支预测失误或其他异常)。

    "中止"不是标准的CPU架构术语。就像我说的,你希望操作系统能够继续,不管发生什么,只有硬件故障或内核错误通常会阻止这种情况。

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

    SIGABRT可以被信号处理器捕获。如果处理程序返回,进程就会退出,所以如果您不想要它,您可以退出它。但是AFAIK无错误条件会引发SIGABRT;它只是通过软件手动发送的,例如通过调用abort()库函数。它通常会导致堆栈回溯。)

    如果您在osdev wiki上查看x86手册或此异常表,则此上下文中有特定含义(感谢@MargaretBloom的描述):

    >

  • 陷阱:在指令成功完成后引发,陷阱指令#DB调试和溢出(进入)异常的#之后的返回地址点是陷阱。(有些#DB的来源是故障)。但是int 0x80或其他软件中断指令也是陷阱,就像syscall一样(但它将返回地址放在rcx中,而不是推送;syscall不是例外,因此在这个意义上也不是真正的陷阱)

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

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

    请注意,即使像Andy Glew这样的英特尔CPU架构师在使用讨论计算机体系结构理论时,有时也会更普遍地使用术语“陷阱”,我认为这意味着任何同步异常。不要期望人们坚持以上术语,除非你实际上是在谈论处理x86上的特定异常。虽然它是有用和明智的术语,你可以在其他上下文中使用它。但是如果你想区分,你应该澄清你所说的每一个术语的意思,这样每个人都在同一页上。

  •  类似资料:
    • 问题内容: 根据手册页: 返回值 成功完成后,将返回0。否则,将返回并且设置全局变量以指示错误。在任何一种情况下,对该流的任何进一步访问(包括对的另一个调用)都会导致未定义的行为。 错误 底层的文件描述符无效。 该函数也可能会失败,并设置为例程指定的任何错误,或者。 当然应该失败,但是我希望它以正常方式返回,而不是直接因分段错误而死亡。是否有任何这种行为的原因? 提前致谢。 更新:我将把代码放在这

    • 当我尝试启动AndEngine活动时,出现以下错误: 该应用程序不会崩溃,但有一个黑屏,设备对按下“返回”或“主页”按钮没有反应。 有人知道问题出在哪里吗?

    • 问题内容: 为什么在下面的代码中没有出现编译错误?我得到一个有点混乱的地方。是因为它们有关系吗? 问题答案: 为什么在下面的代码中没有出现编译错误? 因为编译器只关心您要强制转换的表达式的静态类型。 看这两行: 您 知道在第二行中,由于第一行,该值仅引用类型的对象,而编译器则没有。对于所有的编译器知道(编译第二线时),它 可能 实际上已经: …哪里有扩展和实现的类。因此它是有效的(在编译时),以铸

    • 顺着我之前的一个问题,大部分评论都说“就是不要,你处于冷宫状态,你要杀光一切,重新开始”。还有一个“安全”的变通方法。 我不明白的是为什么分割错误本质上是不可恢复的。 写入受保护内存的时刻被捕获-否则,不会发送。 如果能够捕捉到写入受保护内存的时刻,我不明白为什么——理论上——它不能在某个低级别上恢复,并将SIGSEGV转换为标准软件异常。 请解释为什么在出现分段错误后,程序处于不确定状态,因为很

    • 问题内容: 注意到今天在我们的代码库中有一行代码,我认为肯定会因语法错误而使构建失败,但是测试通过了,显然它实际上是有效的python(在2.x和3中)。 条件表达式有时不需要空格: 如果LHS是变量,则不起作用: 但是它似乎仍然可以与其他类型的文字一起使用: 这是怎么回事,出于某种原因,它是否有意成为语法的一部分?这个奇怪的怪癖是已知/记录的行为吗? 问题答案: 令牌之间的空白 除逻辑行的开头或

    • 突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有 panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。 当出现 panic! 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的