为什么分段错误不可恢复?
Why is a segmentation fault not recoverable?
正在关注 , most comments say "just don't, you are in a limbo state, you have to kill everything and start over". There is also a "safeish" workaround。
我不明白的是为什么分段错误本质上是不可恢复的。
写入受保护内存的时刻 - 否则,SIGSEGV
将不会被发送。
如果可以捕捉到写入受保护内存的时刻,我不明白为什么 - 理论上 - 它不能在某个低级别恢复,并且无法将 SIGSEGV 转换为标准软件异常。
请解释为什么在出现分段错误后程序处于不确定状态,很明显,错误是在 内存实际更改之前抛出的(我可能错了,不要'明白为什么)。如果它在之后被抛出,人们可以创建一个程序来更改受保护的内存,一次一个字节,出现分段错误,并最终重新编程内核 - 一种不存在的安全风险,因为我们可以看到世界仍然存在。
- 分段错误具体何时发生(=何时发送
SIGSEGV
)?
- 为什么在该点之后进程处于未定义行为状态?
- 为什么无法恢复?
- 为什么 this solution 避免这种不可恢复的状态?甚至吗?
When exactly does segmentation fault happen (=when is SIGSEGV sent)?
当您尝试访问您无权访问的内存时,例如越界访问数组或取消引用无效指针。信号 SIGSEGV
是标准化的,但不同的 OS 可能会以不同的方式实现它。 “Segmentation fault”主要是*nix系统中使用的一个术语,Windows称之为“access violation”。
Why is the process in undefined behavior state after that point?
因为程序中的一个或多个变量没有按预期运行。假设您有一些应该存储多个值的数组,但您没有为所有这些值分配足够的空间。因此,只有那些你分配了空间的才能正确写入,其余写入数组边界之外的可以容纳任何值。 OS 到底有多重要才能知道这些越界值对您的应用程序的运行有多重要?它对它们的目的一无所知。
此外,在允许的内存之外写入通常会破坏其他不相关的变量,这显然是危险的并且可能导致任何随机行为。此类错误通常很难追踪。例如,堆栈溢出是此类易于覆盖相邻变量的分段错误,除非错误被保护机制捕获。
如果我们查看没有任何 OS 且没有虚拟内存功能,只有原始物理内存的“裸机”微控制器系统的行为 - 它们将完全按照指示默默地执行 - 例如,覆盖不相关的变量并继续前进。如果应用程序是关键任务,这反过来可能会导致灾难性行为。
Why is it not recoverable?
因为 OS 不知道你的程序应该做什么。
虽然在上面的“裸机”场景中,系统可能足够聪明,可以将自身置于安全模式并继续运行。不允许汽车和医疗技术等关键应用程序停止或重置,因为这本身可能很危险。他们宁愿尝试使用有限的功能“跛行回家”。
Why does this solution avoid that unrecoverable state? Does it even?
该解决方案只是忽略错误并继续进行。它不能解决导致它的问题。这是一个非常脏的补丁,setjmp/longjmp 通常是非常危险的函数,无论出于何种目的都应避免使用。
我们必须认识到,分段错误是错误的症状,而不是原因。
这可能不是一个完整的答案,它绝不是完整或准确的,但它不适合发表评论
因此,当您尝试以不应该的方式访问内存时(例如在内存为只读时写入或从未映射的地址范围读取),可能会发生 SIGSEGV
。如果您对环境有足够的了解,这样的错误可能是可以恢复的。
但是您首先要如何确定无效访问发生的原因。
在对另一个答案的评论中,您说:
short-term practice, I think my errors are only access to null and nothing more.
没有一个应用程序是没有错误的,所以为什么你假设如果空指针访问可能发生,你的应用程序不会,例如还有一种情况是,在释放后使用或越界访问“有效”内存位置时,不会立即导致错误或 SIGSEGV
.
释放后使用或越界访问也可能将指针修改为指向无效位置或变为 nullptr,但它也可能同时更改了内存中的其他位置.如果您现在只假设指针未初始化并且您的错误处理仅考虑这一点,那么您将继续使用处于不符合您的预期状态或其中一个编译器在生成代码时所处状态的应用程序。
在这种情况下,应用程序将在最好的情况下在“恢复”后不久崩溃,在最坏的情况下,某些变量具有错误值,但它会继续 运行。这种疏忽对关键应用程序的危害可能比重新启动它更大。
但是,如果您知道某个操作在某些情况下可能会导致 SIGSEGV
,您可以处理该错误,例如您知道内存地址是有效的,但是内存映射到的设备可能不完全可靠并且可能会导致 SIGSEGV
,因此从 SIGSEGV
中恢复可能是一种有效的方法.
到目前为止,答案和评论都是通过更高级别的编程模型来响应的,这从根本上限制了程序员的创造力和潜力,以方便他们使用。所述模型定义了它们自己的语义并且不出于它们自己的原因处理分段错误,无论是简单性、效率还是其他原因。从这个角度来看,段错误是一种不寻常的情况,表明程序员错误,无论是用户空间程序员还是语言实现的程序员。然而,问题不在于它是否是一个好主意,也不是询问您对此事的任何想法。
实际上,您说的是正确的:分段错误是可恢复的。您可以像任何常规信号一样,使用 sigaction
为它附加一个处理程序。而且,是的,您的程序肯定可以以处理分段错误作为正常功能的方式制作。
一个障碍是分段错误是 错误,而不是异常,这与错误发生后控制流 returns 的位置不同被处理了。具体来说,故障处理程序 returns 到同一个故障指令,它将无限期地继续故障。不过,这不是真正的问题,因为它可以手动跳过,您可以 return 到指定位置,您可以尝试修补错误指令使其变得正确,或者您可以将所述内存映射到存在,如果你相信故障代码。只要对机器有适当的了解,就没有什么能阻止你,即使是那些手持规格的骑士。
在机器代码级别,许多平台允许在某些情况下“预期”出现分段错误的程序调整内存配置并恢复执行。这对于实现堆栈监控之类的东西可能很有用。如果需要确定应用程序曾经使用过的最大堆栈量,可以将堆栈段设置为仅允许访问少量堆栈,然后通过调整堆栈段的边界和恢复代码执行。
然而,在 C 语言级别,支持这种语义将极大地阻碍优化。如果要写这样的东西:
void test(float *p, int *q)
{
float temp = *p;
if (*q += 1)
function2(temp);
}
编译器可能会将 *p
的读取和 *q
上的读取-修改-写入序列视为彼此无序,并生成仅读取 *p
的代码在 *q
的初始值不是 -1 的情况下。如果 p
有效,这不会对程序行为产生任何影响,但如果 p
无效,则此更改可能会导致在 *q
之后访问 *p
时出现段错误即使触发错误的访问是在递增之前执行的,也会递增。
对于一种有效且有意义地支持可恢复段错误的语言,它必须比 C 标准更详细地记录允许和不允许的优化范围,我认为没有理由期望C 标准的未来版本将包含此类细节。
它是可以恢复的,但通常是个坏主意。
例如,Microsoft C++ 编译器可以选择将段错误转换为异常。
你可以看看微软的SEH documentation,但是连他们都不建议用
Please explain why after a segmentation fault the program is in an undetermined state
我认为这是您根本性的误解——SEGV 不会导致 未确定状态,它是未确定状态的一个症状。所以问题是(通常)程序在 SIGSEGV 发生之前就处于非法、不可恢复的状态,并且从 SIGSEGV 恢复不会改变这一点。
- When exactly does segmentation fault happen (=when is SIGSEGV sent)?
SIGSEGV 发生的唯一标准方式是调用 raise(SIGSEGV);
。如果这是 SIGSEGV 的来源,那么显然可以使用 longjump 恢复。但这是现实中从未发生过的微不足道的案例。有特定于平台的处理方式可能会导致定义明确的 SEGV(例如,在 POSIX 系统上使用 mprotect),并且这些 SEGV 可能是可恢复的(但可能需要特定于平台的恢复)。然而,与未定义行为相关的 SEGV 的危险通常意味着信号处理程序将非常仔细地检查信号附带的(平台相关的)信息,以确保它是预期的。
- Why is the process in undefined behavior state after that point?
在那之前它(通常)处于未定义的行为状态;它只是没有被注意到。这是 C 和 C++ 中未定义行为的大问题——没有与之关联的特定行为,因此可能不会立即注意到它。
- Why does this solution avoid that unrecoverable state? Does it even?
它不会,它只是返回到较早的某个点,但不会执行任何操作来撤消甚至识别导致问题的未定义行为。
关于分段错误,您必须了解的是它们不是问题。他们是主近乎无限怜悯的一个例子(据我在大学时的一位老教授说)。分段错误是出现严重错误的标志,您的程序认为访问没有内存的内存是个好主意。这种访问本身并不是问题;问题出现在某个不确定的时间之前,当出现问题时,最终导致您的程序认为此访问是一个好主意。访问不存在的内存在这一点上只是一个症状,但是(这就是上帝的怜悯发挥作用的地方)它是一个 容易检测到的 症状。情况可能更糟;它可能正在访问有内存的内存,只是错误的内存。 OS 无法挽救你。
OS 没有办法弄清楚是什么导致你的程序相信如此荒谬的事情,它唯一能做的就是在它以某种方式做其他疯狂的事情之前关闭它。 =16=] 没那么容易检测到。通常,大多数 OSes 还提供核心转储(程序内存的保存副本),理论上可以用来弄清楚程序认为它在做什么。这对于任何重要的程序来说都不是很简单,但这就是 OS 这样做的原因,以防万一。
您的程序处于未确定状态,因为 C 无法定义该状态。导致这些错误的错误是未定义的行为。这是最恶劣的 class 不良行为。
从这些事情中恢复的关键问题是,作为未定义的行为,编译器没有义务以任何方式支持它们。特别是,它可能已经进行了优化,如果仅发生定义的行为,则可证明具有相同的效果。编译器完全有权重新排列行、跳过行和执行各种花哨的技巧来使您的代码 运行 更快。它所要做的就是根据C++虚拟机模型证明效果是一样的。
当发生未定义的行为时,所有这些都会消失 window。您可能会遇到编译器重新排序操作的困难情况,现在 不能 让您进入可以通过执行程序一段时间来达到的状态。请记住,赋值会删除旧值。如果分配在发生段错误的行之前向上移动,则无法恢复旧值以“展开”优化。
只要没有未定义的行为发生,此重新排序的代码的行为确实与原始代码相同,。一旦发生未定义的行为,它就会暴露发生重新排序并可能改变结果的事实。
这里的权衡是速度。因为编译器不是如履薄冰,害怕某些未指定的 OS 行为,所以它可以更好地优化您的代码。
现在,因为未定义的行为总是未定义的行为,无论您多么希望它不是,都没有规范的 C++ 方法来处理这种情况。 C++ 语言永远无法引入解决此问题的方法,至少不能使其定义行为并为此付出代价。在给定的平台和编译器上,您可能能够识别出这种未定义的行为实际上是由您的编译器定义的,通常以扩展的形式。事实上,我之前链接的答案显示了一种将信号转换为异常的方法,它确实适用于至少一个 platform/compiler 对。
但它总是必须像这样处于边缘。 C++ 开发人员重视优化代码的速度而不是定义这种未定义的行为。
老实说,如果我能告诉计算机忽略分段错误。我不会选择这个选项。
通常发生分段错误是因为您正在取消引用空指针或已解除分配的指针。当取消引用 null 时,行为是完全未定义的。引用已释放指针时,您提取的数据可能是旧值、随机垃圾,或者在最坏情况下来自另一个程序的值。在任何一种情况下,我都希望程序出现段错误而不是继续并报告垃圾计算。
多年来,段错误一直困扰着我。我主要在嵌入式平台上工作,因为我们 运行 在裸机上,所以没有文件系统来记录核心转储。系统刚刚锁定并死机,可能从串口输出了一些离别字符。那些年最有启发性的时刻之一是当我意识到分段错误(和类似的致命错误)是一件好事。体验一个是不好的,但是拥有它们是难免的失败点。
这样的故障不是轻易产生的。硬件已经尽了一切努力来恢复,故障是硬件警告您继续操作是危险的方式。这么多,事实上,让整个 process/system 崩溃实际上比继续 更安全 。即使在具有 protected/virtual 内存的系统中,在出现此类故障后继续执行也会破坏系统其余部分的稳定性。
If the moment of writing to protected memory can be caught
除了写入受保护的内存之外,还有更多方法可以进入段错误。您也可以通过例如读取具有无效值的指针来到达那里。这要么是由于之前的内存损坏(损坏已经造成,因此恢复为时已晚)造成的,要么是由于缺少错误检查代码(应该已被静态分析器 and/or 测试捕获)造成的。
Why is it not recoverable?
您不一定知道导致问题的原因或问题的严重程度,因此您无法知道如何从中恢复。如果你的记忆已经损坏,你就不能相信任何东西。可以恢复的情况是您可以提前检测到问题,因此使用异常不是解决问题的正确方法。
请注意,其中一些类型的问题 可以在 C# 等其他语言中恢复。这些语言通常有一个额外的运行时层,它会提前检查指针地址并在硬件产生故障之前抛出异常。但是,对于像 C 这样的低级语言,你没有任何这些。
Why does this solution avoid that unrecoverable state? Does it even?
该技术“有效”,但仅在人为的、简单化的用例中有效。继续执行不等于恢复。有问题的系统仍处于故障状态,内存损坏不明,您只是选择继续前进,而不是听从硬件的建议以认真对待问题。不知道您的程序此时会做什么。在潜在的内存损坏后继续执行的程序将是攻击者的早期圣诞礼物。
即使没有任何内存损坏,该解决方案也会在许多不同的常见用例中失效。您不能在已经在其中的情况下输入第二个受保护的代码块(例如在辅助函数内部)。在受保护的代码块之外发生的任何段错误都将导致跳转到代码中不可预测的点。这意味着每一行代码都需要放在一个保护块中,并且您的代码将令人讨厌。您不能调用外部库代码,因为该代码不使用此技术并且不会设置 setjmp
锚点。您的“处理程序”块不能调用库函数或执行任何涉及指针的操作,否则您可能需要无休止地嵌套块。在 longjmp
.
之后,自动变量等某些东西可能处于不可预测的状态
One thing missing here, about mission critical systems (or any
system): In large systems in production, one can't know where, or
even if the segfaults are, so the reccomendation to fix the bug and
not the symptom does not hold.
我不同意这个想法。我见过的大多数分段错误都是由取消引用指针(直接或间接)引起的,而没有先验证它们。在使用指针之前检查指针会告诉您段错误在哪里。将 my_array[ptr1->offsets[ptr2->index]]
之类的复杂语句拆分为多个语句,以便您也可以检查中间指针。像 Coverity 这样的静态分析器非常适合查找使用指针但未经验证的代码路径。这不会保护您免受完全内存损坏引起的段错误的影响,但在任何情况下都无法从这种情况中恢复。
In short-term practice, I think my errors are only access to
null and nothing more.
好消息!整个讨论都没有实际意义。指针和数组索引可以(并且应该!)在使用之前进行验证,并且提前检查比等待问题发生并尝试恢复要少得多。
当您的程序试图取消引用错误指针时会发生段错误。 (请参阅下面的更多技术版本,以及其他可能出现段错误的内容。)到那时,您的程序已经被导致指针错误的错误绊倒了;取消引用它的尝试通常不是真正的错误。
除非你故意做一些可能会导致段错误的事情,并打算捕获和处理这些情况(见下文),否则你不会知道什么在错误的访问实际上出错之前被程序中的错误(或宇宙射线翻转了一下)搞砸了。(这通常需要用 asm 编写,或者 运行ning 你自己 JIT 的代码,不是 C 或 C++。)
C 和 C++ 没有定义导致分段错误的程序行为,因此编译器不会生成预期恢复尝试的机器代码。即使在手写的 asm 程序中,除非您 预期 某些类型的段错误,否则尝试尝试是没有意义的,没有理智的方法可以尝试真正恢复;至多你应该在退出前打印一条错误消息。
如果您在尝试访问的访问方式的任何地址映射一些新内存,或者将其从只读保护为读+写(在 SIGSEGV 处理程序中),这可以让错误指令执行,但这非常不太可能让执行恢复。大多数只读存储器出于某种原因是只读的,让某些东西写入它不会有帮助。并且尝试通过指针读取某些内容可能需要获取一些实际上位于其他地方的特定数据(或者根本不读取,因为没有什么可读取的)。因此,将一个新的零页映射到该地址将使执行继续,但不会有用正确执行。与在 SIGSEGV 处理程序中修改主线程的指令指针相同,因此它会在错误指令之后恢复。然后任何加载或存储都不会发生,使用以前在寄存器中的任何垃圾(用于加载),或 CISC add reg, [mem]
或其他类似的其他结果。
(您链接的捕获 SIGSEGV 的示例取决于编译器以明显的方式生成机器代码,而 setjump/longjump 取决于知道哪个代码将发生段错误,并且它发生时没有先覆盖一些有效内存,例如 printf 所依赖的 stdout
数据结构,在到达未映射的页面之前,就像循环或 memcpy 可能发生的那样。)
预期的 SIGSEGV,例如 JIT 沙箱
像 Java 或 Javascript 这样的语言(没有未定义的行为)的 JIT 需要以明确定义的方式处理空指针取消引用,通过 (Java) 在来宾机器中抛出 NullPointerException。
实现 Java 程序逻辑的机器代码(由 JIT 编译器创建,作为 JVM 的一部分)需要在使用前至少检查一次每个引用,在任何情况下都不能在 JIT 编译时证明它是非空的,如果它想避免 JIT 代码错误的话。
但这很昂贵,因此 JIT 可以通过允许在它生成的来宾 asm 中发生错误来消除一些空指针检查,即使这样的错误首先会陷入 OS,并且只有在那时到 JVM 的 SIGSEGV 处理程序。
如果 JVM 在布局其生成的 asm 指令时小心翼翼,那么任何可能的空指针 deref 都会在正确的时间发生。对其他数据产生副作用,并且仅对应该发生的执行路径产生副作用(请参阅@supercat 的回答示例),那么这是有效的。 JVM 必须从信号处理程序中捕获 SIGSEGV 和 longjmp 或任何其他内容,以编写向来宾传递 NullPointerException 的代码。
但这里的关键部分是 JVM 假设它自己的代码没有错误,因此唯一可能“损坏”的状态是来宾实际状态,而不是 JVM 关于来宾的数据。这意味着 JVM 能够处理来宾中发生的异常,而不依赖于可能已损坏的数据。
来宾本身可能无能为力,但是,如果 它 没有预料到 NullPointerException,因此不知道如何修复这种情况。它可能不应该做更多的事情,只是打印一条错误消息并自行退出或重新启动。 (几乎是普通的提前编译 C++ 程序的限制。)
当然JVM需要检查SIGSEGV的故障地址,准确找出它在哪个guest代码中,才能知道在哪里传递NullPointerException。 (哪个 catch 块,如果有的话。)如果错误地址根本不在 JIT 客户代码中,那么 JVM 就像任何其他发生段错误的提前编译 C/C++ 程序一样,并且除了打印错误消息并退出之外,不应该做更多的事情。 (或 raise(SIGABRT)
触发核心转储。)
作为一个 JIT JVM 并不能更容易地从 意外 段错误中恢复,因为你自己的逻辑中有错误。关键是有一个沙盒来宾,您已经确保它不会弄乱主程序,并且它的错误对于主机 JVM 来说并不意外。 (您不能允许来宾中的“托管”代码具有可以指向任何地方的完全野指针,例如指向来宾代码。但这通常很好。但是您仍然可以使用空指针,使用实际上确实如此的表示如果硬件试图取消引用它,则会出错。这不会让它写入或读取主机的状态。)
有关此的更多信息,请参阅 以了解段错误的 asm 级视图。并链接到 JIT 技术,让访客代码页面错误而不是进行 运行 时间检查:
Effective Null Pointer Check Elimination Utilizing Hardware Trap 三位 IBM 科学家为 Java 撰写的关于此的研究论文。
SableVM: 6.2.4 Hardware Support on Various Architectures 关于 NULL 指针检查
另一个技巧是将数组的末尾放在页面的末尾(后面是足够大的未映射区域),因此对每次访问进行边界检查由硬件免费完成。如果你能静态地证明索引总是正的,并且它不能大于 32 位,那么你已经准备好了。
- 隐式 Java 64 位数组边界检查
架构。他们讨论了当数组大小不是页面大小的倍数时该怎么办,以及其他注意事项。
背景:什么是段错误
OS 传递 SIGSEGV 的通常原因是在您的进程触发页面错误之后 OS 发现它是“无效的”。 (也就是说,这是你的错,而不是 OS 的问题,所以它无法通过分页换出到磁盘的数据(硬页面错误)或写时复制或将新的匿名归零来修复它首次访问页面(软页面错误),并更新该虚拟页面的硬件页面 tables 以匹配您的进程逻辑映射的内容。)。
页面错误处理程序无法修复这种情况,因为 user-space 线程正常,因为 user-space 没有向 OS 请求任何内存映射到该虚拟地址。如果它只是尝试恢复 user-space 而没有对页面 table 做任何事情,相同的指令将再次出错,因此内核会发送一个 SIGSEGV。该信号的默认操作是终止进程,但如果 user-space 安装了信号处理程序,它可以捕获它。
其他原因包括(在 Linux 上)尝试 运行 user-space 中的特权指令(例如 x86 #GP
“一般保护错误”硬件异常),或在 x86 Linux 上未对齐的 16 字节 SSE 加载或存储(同样是 #GP 异常)。这可能发生在使用 _mm_load_si128
而不是 loadu
的手动矢量化代码中,甚至是在具有未定义行为的程序中自动矢量化的结果:(其他一些 OSes,例如 MacOS / Darwin,为未对齐的 SSE 提供 SIGBUS。)
段错误通常只发生在之后你的程序遇到了错误
所以你的程序状态已经一团糟,这就是为什么有一个 NULL 指针,你希望它是非 NULL 或其他无效的。 (例如,某些形式的释放后使用,或者用一些不代表有效指针的位覆盖的指针。)
如果幸运的话,它会尽早发生段错误并大声失败,尽可能接近实际错误;如果你不走运(例如,破坏 malloc 簿记信息),直到错误代码执行很久之后,你才会真正出现段错误。
这绝对有可能,但这会以不太稳定的方式复制现有功能。
当程序访问尚未由物理内存支持的地址时,内核将已经收到页面错误异常,然后将根据现有映射分配并可能初始化页面,然后重试违规指令.
一个假设的 SEGV 处理程序会做完全相同的事情:决定应该在这个地址映射什么,创建映射并重试指令——但不同的是如果处理程序会引发另一个 SEGV,我们可以去在这里进入无限循环,并且检测会很困难,因为该决定需要查看代码——所以我们会在这里创建一个停止问题。
内核已经延迟分配内存页面,允许映射文件内容并支持具有写时复制语义的共享映射,因此从该机制中没有太多好处。
虽然您的问题专门询问分段错误,但真正的问题是:
如果软件或硬件组件被命令做一些荒谬甚至不可能的事情,它应该怎么做?什么都不做?猜猜实际需要做什么并做到这一点?或者使用某种机制(例如“抛出异常”)来停止发出无意义命令的更高级别的计算?
许多工程师多年来积累的大量经验一致认为,最好的答案是停止整个计算,并生成诊断信息,这可能有助于找出问题所在.
除了非法访问受保护或不存在的内存外,'nonsensical commands' 的其他示例包括告诉 CPU 将整数除以零或执行不解码为任何有效指令的垃圾字节.如果使用具有 运行 时间类型检查的编程语言,则尝试调用任何未为所涉及的数据类型定义的操作是另一个例子。
但是为什么强制试图除以零的程序崩溃更好?没有人希望他们的程序崩溃。我们不能将被零除定义为等于某个数字,例如零或 73 吗?难道我们不能创建 CPUs 来跳过无效指令而不会出错吗?也许我们的 CPUs 也可以 return 一些特殊值,比如 -1,用于从受保护或未映射的内存地址读取。他们可以忽略对受保护地址的写入。没有更多的段错误!哇!
当然,这些事情都可以做,但实际上并没有什么收获。重点是:虽然没有人希望他们的程序崩溃,但不崩溃并不意味着成功。人们编写 运行 计算机程序是为了 做 一些事情,而不仅仅是为了“不崩溃”。如果一个程序有足够的错误来读取或写入随机内存地址或尝试除以零,那么它执行您真正想要的操作的可能性非常低,即使允许它继续 运行ning 也是如此。另一方面,如果程序在尝试疯狂的事情时没有停止,它可能最终会做一些您不不想做的事情,例如破坏或破坏您的数据。
从历史上看,一些编程语言被设计为始终“只做某事”以响应无意义的命令,而不是引发致命错误。这是为了对新手程序员更友好而被误导的尝试,但它总是以糟糕的方式结束。您关于操作系统不应因段错误而导致程序崩溃的建议也是如此。
当您使用术语 SIGSEGV 时,我相信您使用的是带有操作系统的系统,并且问题出现在您的用户级应用程序中。
当应用程序收到 SIGSEGV 时,这是内存访问之前出现问题的征兆。有时可以准确指出问题出在哪里,但通常不会。所以出了点问题,过了一会儿这个错误是 SIGSEGV 的原因。如果错误发生在“操作系统中”,我的反应是关闭系统。有一个非常具体的例外——当 OS 有一个特定的功能来检查是否安装了存储卡或 IO 卡(或可能已删除)。
在用户领域,我可能会将我的应用程序分成几个进程。一个或多个进程将完成实际工作。另一个进程将监视工作进程并可以发现其中一个进程何时失败。然后监控进程可以发现工作进程中的 SIGSEGV,这可以重新启动工作进程或进行故障转移或在特定情况下认为合适的任何操作。这不会恢复实际的内存访问,但可能会恢复应用程序功能。
您可能会研究“早期失败”的 Erlang 哲学和 OTP 库,以获得有关这种做事方式的更多灵感。虽然它不处理 SIGSEGV,但处理其他几种类型的问题。
取决于你所说的恢复是什么意思。如果 OS 向您发送 SEGV 信号,唯一明智的恢复是清理您的程序并从头开始旋转另一个程序,希望不会遇到同样的陷阱。
你无法知道在 OS 宣布结束混乱之前你的记忆损坏了多少。如果您尝试从下一条指令或某个任意恢复点继续,您的程序可能会进一步出现错误。
很多赞成的回复似乎都忘记了,有些应用程序可能会在生产中发生段错误而不会出现编程错误。在高可用性的地方,预计会有数十年的使用寿命和零维护。在这些环境中,通常所做的是,如果程序因任何原因(包括段错误)崩溃而重新启动。此外,看门狗功能用于确保程序不会陷入计划外的无限循环。
想想您所依赖的所有没有重置按钮的嵌入式设备。他们依赖于不完美的硬件,因为没有硬件是完美的。软件必须处理硬件缺陷。换句话说,软件必须能够抵抗硬件的不当行为。
嵌入式并不是这一点至关重要的唯一领域。想一想仅处理 Whosebug 的服务器数量。如果您查看地面上的任何一项操作,电离辐射导致单个事件混乱的可能性很小,但如果您查看大量计算机 运行 24/7,这种可能性就变得不小了。 ECC 内存有助于防止这种情况,但并非所有内容都可以得到保护。
您的程序无法从分段错误中恢复,因为它不知道 任何东西 处于什么状态。
考虑这个类比。
你在缅因州有一栋漂亮的房子,有一个漂亮的前花园和一条穿过它的垫脚石小路 运行。无论出于何种原因,您选择用丝带将每块石头连接到下一块石头(a.k.a。您已将它们变成一个单链表)。
一天早上,从房子里出来,你踏上第一块石头,然后沿着丝带走到第二块,然后又走到第三块,但是,当你踏上 第四块 石头时,你突然发现自己在阿尔伯克基。
现在告诉我们 - 你 如何从 那个 中恢复过来?
你的程序有同样的困境。
出现了惊人的错误,但您的程序不知道它是什么,或者是什么导致了它,或者如何做 任何有用的东西。
因此:它崩溃并燃烧。
正在关注
我不明白的是为什么分段错误本质上是不可恢复的。
写入受保护内存的时刻 - 否则,SIGSEGV
将不会被发送。
如果可以捕捉到写入受保护内存的时刻,我不明白为什么 - 理论上 - 它不能在某个低级别恢复,并且无法将 SIGSEGV 转换为标准软件异常。
请解释为什么在出现分段错误后程序处于不确定状态,很明显,错误是在 内存实际更改之前抛出的(我可能错了,不要'明白为什么)。如果它在之后被抛出,人们可以创建一个程序来更改受保护的内存,一次一个字节,出现分段错误,并最终重新编程内核 - 一种不存在的安全风险,因为我们可以看到世界仍然存在。
- 分段错误具体何时发生(=何时发送
SIGSEGV
)? - 为什么在该点之后进程处于未定义行为状态?
- 为什么无法恢复?
- 为什么 this solution 避免这种不可恢复的状态?甚至吗?
When exactly does segmentation fault happen (=when is SIGSEGV sent)?
当您尝试访问您无权访问的内存时,例如越界访问数组或取消引用无效指针。信号 SIGSEGV
是标准化的,但不同的 OS 可能会以不同的方式实现它。 “Segmentation fault”主要是*nix系统中使用的一个术语,Windows称之为“access violation”。
Why is the process in undefined behavior state after that point?
因为程序中的一个或多个变量没有按预期运行。假设您有一些应该存储多个值的数组,但您没有为所有这些值分配足够的空间。因此,只有那些你分配了空间的才能正确写入,其余写入数组边界之外的可以容纳任何值。 OS 到底有多重要才能知道这些越界值对您的应用程序的运行有多重要?它对它们的目的一无所知。
此外,在允许的内存之外写入通常会破坏其他不相关的变量,这显然是危险的并且可能导致任何随机行为。此类错误通常很难追踪。例如,堆栈溢出是此类易于覆盖相邻变量的分段错误,除非错误被保护机制捕获。
如果我们查看没有任何 OS 且没有虚拟内存功能,只有原始物理内存的“裸机”微控制器系统的行为 - 它们将完全按照指示默默地执行 - 例如,覆盖不相关的变量并继续前进。如果应用程序是关键任务,这反过来可能会导致灾难性行为。
Why is it not recoverable?
因为 OS 不知道你的程序应该做什么。
虽然在上面的“裸机”场景中,系统可能足够聪明,可以将自身置于安全模式并继续运行。不允许汽车和医疗技术等关键应用程序停止或重置,因为这本身可能很危险。他们宁愿尝试使用有限的功能“跛行回家”。
Why does this solution avoid that unrecoverable state? Does it even?
该解决方案只是忽略错误并继续进行。它不能解决导致它的问题。这是一个非常脏的补丁,setjmp/longjmp 通常是非常危险的函数,无论出于何种目的都应避免使用。
我们必须认识到,分段错误是错误的症状,而不是原因。
这可能不是一个完整的答案,它绝不是完整或准确的,但它不适合发表评论
因此,当您尝试以不应该的方式访问内存时(例如在内存为只读时写入或从未映射的地址范围读取),可能会发生 SIGSEGV
。如果您对环境有足够的了解,这样的错误可能是可以恢复的。
但是您首先要如何确定无效访问发生的原因。
在对另一个答案的评论中,您说:
short-term practice, I think my errors are only access to null and nothing more.
没有一个应用程序是没有错误的,所以为什么你假设如果空指针访问可能发生,你的应用程序不会,例如还有一种情况是,在释放后使用或越界访问“有效”内存位置时,不会立即导致错误或 SIGSEGV
.
释放后使用或越界访问也可能将指针修改为指向无效位置或变为 nullptr,但它也可能同时更改了内存中的其他位置.如果您现在只假设指针未初始化并且您的错误处理仅考虑这一点,那么您将继续使用处于不符合您的预期状态或其中一个编译器在生成代码时所处状态的应用程序。
在这种情况下,应用程序将在最好的情况下在“恢复”后不久崩溃,在最坏的情况下,某些变量具有错误值,但它会继续 运行。这种疏忽对关键应用程序的危害可能比重新启动它更大。
但是,如果您知道某个操作在某些情况下可能会导致 SIGSEGV
,您可以处理该错误,例如您知道内存地址是有效的,但是内存映射到的设备可能不完全可靠并且可能会导致 SIGSEGV
,因此从 SIGSEGV
中恢复可能是一种有效的方法.
到目前为止,答案和评论都是通过更高级别的编程模型来响应的,这从根本上限制了程序员的创造力和潜力,以方便他们使用。所述模型定义了它们自己的语义并且不出于它们自己的原因处理分段错误,无论是简单性、效率还是其他原因。从这个角度来看,段错误是一种不寻常的情况,表明程序员错误,无论是用户空间程序员还是语言实现的程序员。然而,问题不在于它是否是一个好主意,也不是询问您对此事的任何想法。
实际上,您说的是正确的:分段错误是可恢复的。您可以像任何常规信号一样,使用 sigaction
为它附加一个处理程序。而且,是的,您的程序肯定可以以处理分段错误作为正常功能的方式制作。
一个障碍是分段错误是 错误,而不是异常,这与错误发生后控制流 returns 的位置不同被处理了。具体来说,故障处理程序 returns 到同一个故障指令,它将无限期地继续故障。不过,这不是真正的问题,因为它可以手动跳过,您可以 return 到指定位置,您可以尝试修补错误指令使其变得正确,或者您可以将所述内存映射到存在,如果你相信故障代码。只要对机器有适当的了解,就没有什么能阻止你,即使是那些手持规格的骑士。
在机器代码级别,许多平台允许在某些情况下“预期”出现分段错误的程序调整内存配置并恢复执行。这对于实现堆栈监控之类的东西可能很有用。如果需要确定应用程序曾经使用过的最大堆栈量,可以将堆栈段设置为仅允许访问少量堆栈,然后通过调整堆栈段的边界和恢复代码执行。
然而,在 C 语言级别,支持这种语义将极大地阻碍优化。如果要写这样的东西:
void test(float *p, int *q)
{
float temp = *p;
if (*q += 1)
function2(temp);
}
编译器可能会将 *p
的读取和 *q
上的读取-修改-写入序列视为彼此无序,并生成仅读取 *p
的代码在 *q
的初始值不是 -1 的情况下。如果 p
有效,这不会对程序行为产生任何影响,但如果 p
无效,则此更改可能会导致在 *q
之后访问 *p
时出现段错误即使触发错误的访问是在递增之前执行的,也会递增。
对于一种有效且有意义地支持可恢复段错误的语言,它必须比 C 标准更详细地记录允许和不允许的优化范围,我认为没有理由期望C 标准的未来版本将包含此类细节。
它是可以恢复的,但通常是个坏主意。 例如,Microsoft C++ 编译器可以选择将段错误转换为异常。
你可以看看微软的SEH documentation,但是连他们都不建议用
Please explain why after a segmentation fault the program is in an undetermined state
我认为这是您根本性的误解——SEGV 不会导致 未确定状态,它是未确定状态的一个症状。所以问题是(通常)程序在 SIGSEGV 发生之前就处于非法、不可恢复的状态,并且从 SIGSEGV 恢复不会改变这一点。
- When exactly does segmentation fault happen (=when is SIGSEGV sent)?
SIGSEGV 发生的唯一标准方式是调用 raise(SIGSEGV);
。如果这是 SIGSEGV 的来源,那么显然可以使用 longjump 恢复。但这是现实中从未发生过的微不足道的案例。有特定于平台的处理方式可能会导致定义明确的 SEGV(例如,在 POSIX 系统上使用 mprotect),并且这些 SEGV 可能是可恢复的(但可能需要特定于平台的恢复)。然而,与未定义行为相关的 SEGV 的危险通常意味着信号处理程序将非常仔细地检查信号附带的(平台相关的)信息,以确保它是预期的。
- Why is the process in undefined behavior state after that point?
在那之前它(通常)处于未定义的行为状态;它只是没有被注意到。这是 C 和 C++ 中未定义行为的大问题——没有与之关联的特定行为,因此可能不会立即注意到它。
- Why does this solution avoid that unrecoverable state? Does it even?
它不会,它只是返回到较早的某个点,但不会执行任何操作来撤消甚至识别导致问题的未定义行为。
关于分段错误,您必须了解的是它们不是问题。他们是主近乎无限怜悯的一个例子(据我在大学时的一位老教授说)。分段错误是出现严重错误的标志,您的程序认为访问没有内存的内存是个好主意。这种访问本身并不是问题;问题出现在某个不确定的时间之前,当出现问题时,最终导致您的程序认为此访问是一个好主意。访问不存在的内存在这一点上只是一个症状,但是(这就是上帝的怜悯发挥作用的地方)它是一个 容易检测到的 症状。情况可能更糟;它可能正在访问有内存的内存,只是错误的内存。 OS 无法挽救你。
OS 没有办法弄清楚是什么导致你的程序相信如此荒谬的事情,它唯一能做的就是在它以某种方式做其他疯狂的事情之前关闭它。 =16=] 没那么容易检测到。通常,大多数 OSes 还提供核心转储(程序内存的保存副本),理论上可以用来弄清楚程序认为它在做什么。这对于任何重要的程序来说都不是很简单,但这就是 OS 这样做的原因,以防万一。
您的程序处于未确定状态,因为 C 无法定义该状态。导致这些错误的错误是未定义的行为。这是最恶劣的 class 不良行为。
从这些事情中恢复的关键问题是,作为未定义的行为,编译器没有义务以任何方式支持它们。特别是,它可能已经进行了优化,如果仅发生定义的行为,则可证明具有相同的效果。编译器完全有权重新排列行、跳过行和执行各种花哨的技巧来使您的代码 运行 更快。它所要做的就是根据C++虚拟机模型证明效果是一样的。
当发生未定义的行为时,所有这些都会消失 window。您可能会遇到编译器重新排序操作的困难情况,现在 不能 让您进入可以通过执行程序一段时间来达到的状态。请记住,赋值会删除旧值。如果分配在发生段错误的行之前向上移动,则无法恢复旧值以“展开”优化。
只要没有未定义的行为发生,此重新排序的代码的行为确实与原始代码相同,。一旦发生未定义的行为,它就会暴露发生重新排序并可能改变结果的事实。
这里的权衡是速度。因为编译器不是如履薄冰,害怕某些未指定的 OS 行为,所以它可以更好地优化您的代码。
现在,因为未定义的行为总是未定义的行为,无论您多么希望它不是,都没有规范的 C++ 方法来处理这种情况。 C++ 语言永远无法引入解决此问题的方法,至少不能使其定义行为并为此付出代价。在给定的平台和编译器上,您可能能够识别出这种未定义的行为实际上是由您的编译器定义的,通常以扩展的形式。事实上,我之前链接的答案显示了一种将信号转换为异常的方法,它确实适用于至少一个 platform/compiler 对。
但它总是必须像这样处于边缘。 C++ 开发人员重视优化代码的速度而不是定义这种未定义的行为。
老实说,如果我能告诉计算机忽略分段错误。我不会选择这个选项。
通常发生分段错误是因为您正在取消引用空指针或已解除分配的指针。当取消引用 null 时,行为是完全未定义的。引用已释放指针时,您提取的数据可能是旧值、随机垃圾,或者在最坏情况下来自另一个程序的值。在任何一种情况下,我都希望程序出现段错误而不是继续并报告垃圾计算。
多年来,段错误一直困扰着我。我主要在嵌入式平台上工作,因为我们 运行 在裸机上,所以没有文件系统来记录核心转储。系统刚刚锁定并死机,可能从串口输出了一些离别字符。那些年最有启发性的时刻之一是当我意识到分段错误(和类似的致命错误)是一件好事。体验一个是不好的,但是拥有它们是难免的失败点。
这样的故障不是轻易产生的。硬件已经尽了一切努力来恢复,故障是硬件警告您继续操作是危险的方式。这么多,事实上,让整个 process/system 崩溃实际上比继续 更安全 。即使在具有 protected/virtual 内存的系统中,在出现此类故障后继续执行也会破坏系统其余部分的稳定性。
If the moment of writing to protected memory can be caught
除了写入受保护的内存之外,还有更多方法可以进入段错误。您也可以通过例如读取具有无效值的指针来到达那里。这要么是由于之前的内存损坏(损坏已经造成,因此恢复为时已晚)造成的,要么是由于缺少错误检查代码(应该已被静态分析器 and/or 测试捕获)造成的。
Why is it not recoverable?
您不一定知道导致问题的原因或问题的严重程度,因此您无法知道如何从中恢复。如果你的记忆已经损坏,你就不能相信任何东西。可以恢复的情况是您可以提前检测到问题,因此使用异常不是解决问题的正确方法。
请注意,其中一些类型的问题 可以在 C# 等其他语言中恢复。这些语言通常有一个额外的运行时层,它会提前检查指针地址并在硬件产生故障之前抛出异常。但是,对于像 C 这样的低级语言,你没有任何这些。
Why does this solution avoid that unrecoverable state? Does it even?
该技术“有效”,但仅在人为的、简单化的用例中有效。继续执行不等于恢复。有问题的系统仍处于故障状态,内存损坏不明,您只是选择继续前进,而不是听从硬件的建议以认真对待问题。不知道您的程序此时会做什么。在潜在的内存损坏后继续执行的程序将是攻击者的早期圣诞礼物。
即使没有任何内存损坏,该解决方案也会在许多不同的常见用例中失效。您不能在已经在其中的情况下输入第二个受保护的代码块(例如在辅助函数内部)。在受保护的代码块之外发生的任何段错误都将导致跳转到代码中不可预测的点。这意味着每一行代码都需要放在一个保护块中,并且您的代码将令人讨厌。您不能调用外部库代码,因为该代码不使用此技术并且不会设置 setjmp
锚点。您的“处理程序”块不能调用库函数或执行任何涉及指针的操作,否则您可能需要无休止地嵌套块。在 longjmp
.
One thing missing here, about mission critical systems (or any system): In large systems in production, one can't know where, or even if the segfaults are, so the reccomendation to fix the bug and not the symptom does not hold.
我不同意这个想法。我见过的大多数分段错误都是由取消引用指针(直接或间接)引起的,而没有先验证它们。在使用指针之前检查指针会告诉您段错误在哪里。将 my_array[ptr1->offsets[ptr2->index]]
之类的复杂语句拆分为多个语句,以便您也可以检查中间指针。像 Coverity 这样的静态分析器非常适合查找使用指针但未经验证的代码路径。这不会保护您免受完全内存损坏引起的段错误的影响,但在任何情况下都无法从这种情况中恢复。
In short-term practice, I think my errors are only access to null and nothing more.
好消息!整个讨论都没有实际意义。指针和数组索引可以(并且应该!)在使用之前进行验证,并且提前检查比等待问题发生并尝试恢复要少得多。
当您的程序试图取消引用错误指针时会发生段错误。 (请参阅下面的更多技术版本,以及其他可能出现段错误的内容。)到那时,您的程序已经被导致指针错误的错误绊倒了;取消引用它的尝试通常不是真正的错误。
除非你故意做一些可能会导致段错误的事情,并打算捕获和处理这些情况(见下文),否则你不会知道什么在错误的访问实际上出错之前被程序中的错误(或宇宙射线翻转了一下)搞砸了。(这通常需要用 asm 编写,或者 运行ning 你自己 JIT 的代码,不是 C 或 C++。)
C 和 C++ 没有定义导致分段错误的程序行为,因此编译器不会生成预期恢复尝试的机器代码。即使在手写的 asm 程序中,除非您 预期 某些类型的段错误,否则尝试尝试是没有意义的,没有理智的方法可以尝试真正恢复;至多你应该在退出前打印一条错误消息。
如果您在尝试访问的访问方式的任何地址映射一些新内存,或者将其从只读保护为读+写(在 SIGSEGV 处理程序中),这可以让错误指令执行,但这非常不太可能让执行恢复。大多数只读存储器出于某种原因是只读的,让某些东西写入它不会有帮助。并且尝试通过指针读取某些内容可能需要获取一些实际上位于其他地方的特定数据(或者根本不读取,因为没有什么可读取的)。因此,将一个新的零页映射到该地址将使执行继续,但不会有用正确执行。与在 SIGSEGV 处理程序中修改主线程的指令指针相同,因此它会在错误指令之后恢复。然后任何加载或存储都不会发生,使用以前在寄存器中的任何垃圾(用于加载),或 CISC add reg, [mem]
或其他类似的其他结果。
(您链接的捕获 SIGSEGV 的示例取决于编译器以明显的方式生成机器代码,而 setjump/longjump 取决于知道哪个代码将发生段错误,并且它发生时没有先覆盖一些有效内存,例如 printf 所依赖的 stdout
数据结构,在到达未映射的页面之前,就像循环或 memcpy 可能发生的那样。)
预期的 SIGSEGV,例如 JIT 沙箱
像 Java 或 Javascript 这样的语言(没有未定义的行为)的 JIT 需要以明确定义的方式处理空指针取消引用,通过 (Java) 在来宾机器中抛出 NullPointerException。
实现 Java 程序逻辑的机器代码(由 JIT 编译器创建,作为 JVM 的一部分)需要在使用前至少检查一次每个引用,在任何情况下都不能在 JIT 编译时证明它是非空的,如果它想避免 JIT 代码错误的话。
但这很昂贵,因此 JIT 可以通过允许在它生成的来宾 asm 中发生错误来消除一些空指针检查,即使这样的错误首先会陷入 OS,并且只有在那时到 JVM 的 SIGSEGV 处理程序。
如果 JVM 在布局其生成的 asm 指令时小心翼翼,那么任何可能的空指针 deref 都会在正确的时间发生。对其他数据产生副作用,并且仅对应该发生的执行路径产生副作用(请参阅@supercat 的回答示例),那么这是有效的。 JVM 必须从信号处理程序中捕获 SIGSEGV 和 longjmp 或任何其他内容,以编写向来宾传递 NullPointerException 的代码。
但这里的关键部分是 JVM 假设它自己的代码没有错误,因此唯一可能“损坏”的状态是来宾实际状态,而不是 JVM 关于来宾的数据。这意味着 JVM 能够处理来宾中发生的异常,而不依赖于可能已损坏的数据。
来宾本身可能无能为力,但是,如果 它 没有预料到 NullPointerException,因此不知道如何修复这种情况。它可能不应该做更多的事情,只是打印一条错误消息并自行退出或重新启动。 (几乎是普通的提前编译 C++ 程序的限制。)
当然JVM需要检查SIGSEGV的故障地址,准确找出它在哪个guest代码中,才能知道在哪里传递NullPointerException。 (哪个 catch 块,如果有的话。)如果错误地址根本不在 JIT 客户代码中,那么 JVM 就像任何其他发生段错误的提前编译 C/C++ 程序一样,并且除了打印错误消息并退出之外,不应该做更多的事情。 (或 raise(SIGABRT)
触发核心转储。)
作为一个 JIT JVM 并不能更容易地从 意外 段错误中恢复,因为你自己的逻辑中有错误。关键是有一个沙盒来宾,您已经确保它不会弄乱主程序,并且它的错误对于主机 JVM 来说并不意外。 (您不能允许来宾中的“托管”代码具有可以指向任何地方的完全野指针,例如指向来宾代码。但这通常很好。但是您仍然可以使用空指针,使用实际上确实如此的表示如果硬件试图取消引用它,则会出错。这不会让它写入或读取主机的状态。)
有关此的更多信息,请参阅
Effective Null Pointer Check Elimination Utilizing Hardware Trap 三位 IBM 科学家为 Java 撰写的关于此的研究论文。
SableVM: 6.2.4 Hardware Support on Various Architectures 关于 NULL 指针检查
另一个技巧是将数组的末尾放在页面的末尾(后面是足够大的未映射区域),因此对每次访问进行边界检查由硬件免费完成。如果你能静态地证明索引总是正的,并且它不能大于 32 位,那么你已经准备好了。
- 隐式 Java 64 位数组边界检查 架构。他们讨论了当数组大小不是页面大小的倍数时该怎么办,以及其他注意事项。
背景:什么是段错误
OS 传递 SIGSEGV 的通常原因是在您的进程触发页面错误之后 OS 发现它是“无效的”。 (也就是说,这是你的错,而不是 OS 的问题,所以它无法通过分页换出到磁盘的数据(硬页面错误)或写时复制或将新的匿名归零来修复它首次访问页面(软页面错误),并更新该虚拟页面的硬件页面 tables 以匹配您的进程逻辑映射的内容。)。
页面错误处理程序无法修复这种情况,因为 user-space 线程正常,因为 user-space 没有向 OS 请求任何内存映射到该虚拟地址。如果它只是尝试恢复 user-space 而没有对页面 table 做任何事情,相同的指令将再次出错,因此内核会发送一个 SIGSEGV。该信号的默认操作是终止进程,但如果 user-space 安装了信号处理程序,它可以捕获它。
其他原因包括(在 Linux 上)尝试 运行 user-space 中的特权指令(例如 x86 #GP
“一般保护错误”硬件异常),或在 x86 Linux 上未对齐的 16 字节 SSE 加载或存储(同样是 #GP 异常)。这可能发生在使用 _mm_load_si128
而不是 loadu
的手动矢量化代码中,甚至是在具有未定义行为的程序中自动矢量化的结果:
段错误通常只发生在之后你的程序遇到了错误
所以你的程序状态已经一团糟,这就是为什么有一个 NULL 指针,你希望它是非 NULL 或其他无效的。 (例如,某些形式的释放后使用,或者用一些不代表有效指针的位覆盖的指针。)
如果幸运的话,它会尽早发生段错误并大声失败,尽可能接近实际错误;如果你不走运(例如,破坏 malloc 簿记信息),直到错误代码执行很久之后,你才会真正出现段错误。
这绝对有可能,但这会以不太稳定的方式复制现有功能。
当程序访问尚未由物理内存支持的地址时,内核将已经收到页面错误异常,然后将根据现有映射分配并可能初始化页面,然后重试违规指令.
一个假设的 SEGV 处理程序会做完全相同的事情:决定应该在这个地址映射什么,创建映射并重试指令——但不同的是如果处理程序会引发另一个 SEGV,我们可以去在这里进入无限循环,并且检测会很困难,因为该决定需要查看代码——所以我们会在这里创建一个停止问题。
内核已经延迟分配内存页面,允许映射文件内容并支持具有写时复制语义的共享映射,因此从该机制中没有太多好处。
虽然您的问题专门询问分段错误,但真正的问题是:
如果软件或硬件组件被命令做一些荒谬甚至不可能的事情,它应该怎么做?什么都不做?猜猜实际需要做什么并做到这一点?或者使用某种机制(例如“抛出异常”)来停止发出无意义命令的更高级别的计算?
许多工程师多年来积累的大量经验一致认为,最好的答案是停止整个计算,并生成诊断信息,这可能有助于找出问题所在.
除了非法访问受保护或不存在的内存外,'nonsensical commands' 的其他示例包括告诉 CPU 将整数除以零或执行不解码为任何有效指令的垃圾字节.如果使用具有 运行 时间类型检查的编程语言,则尝试调用任何未为所涉及的数据类型定义的操作是另一个例子。
但是为什么强制试图除以零的程序崩溃更好?没有人希望他们的程序崩溃。我们不能将被零除定义为等于某个数字,例如零或 73 吗?难道我们不能创建 CPUs 来跳过无效指令而不会出错吗?也许我们的 CPUs 也可以 return 一些特殊值,比如 -1,用于从受保护或未映射的内存地址读取。他们可以忽略对受保护地址的写入。没有更多的段错误!哇!
当然,这些事情都可以做,但实际上并没有什么收获。重点是:虽然没有人希望他们的程序崩溃,但不崩溃并不意味着成功。人们编写 运行 计算机程序是为了 做 一些事情,而不仅仅是为了“不崩溃”。如果一个程序有足够的错误来读取或写入随机内存地址或尝试除以零,那么它执行您真正想要的操作的可能性非常低,即使允许它继续 运行ning 也是如此。另一方面,如果程序在尝试疯狂的事情时没有停止,它可能最终会做一些您不不想做的事情,例如破坏或破坏您的数据。
从历史上看,一些编程语言被设计为始终“只做某事”以响应无意义的命令,而不是引发致命错误。这是为了对新手程序员更友好而被误导的尝试,但它总是以糟糕的方式结束。您关于操作系统不应因段错误而导致程序崩溃的建议也是如此。
当您使用术语 SIGSEGV 时,我相信您使用的是带有操作系统的系统,并且问题出现在您的用户级应用程序中。
当应用程序收到 SIGSEGV 时,这是内存访问之前出现问题的征兆。有时可以准确指出问题出在哪里,但通常不会。所以出了点问题,过了一会儿这个错误是 SIGSEGV 的原因。如果错误发生在“操作系统中”,我的反应是关闭系统。有一个非常具体的例外——当 OS 有一个特定的功能来检查是否安装了存储卡或 IO 卡(或可能已删除)。
在用户领域,我可能会将我的应用程序分成几个进程。一个或多个进程将完成实际工作。另一个进程将监视工作进程并可以发现其中一个进程何时失败。然后监控进程可以发现工作进程中的 SIGSEGV,这可以重新启动工作进程或进行故障转移或在特定情况下认为合适的任何操作。这不会恢复实际的内存访问,但可能会恢复应用程序功能。
您可能会研究“早期失败”的 Erlang 哲学和 OTP 库,以获得有关这种做事方式的更多灵感。虽然它不处理 SIGSEGV,但处理其他几种类型的问题。
取决于你所说的恢复是什么意思。如果 OS 向您发送 SEGV 信号,唯一明智的恢复是清理您的程序并从头开始旋转另一个程序,希望不会遇到同样的陷阱。
你无法知道在 OS 宣布结束混乱之前你的记忆损坏了多少。如果您尝试从下一条指令或某个任意恢复点继续,您的程序可能会进一步出现错误。
很多赞成的回复似乎都忘记了,有些应用程序可能会在生产中发生段错误而不会出现编程错误。在高可用性的地方,预计会有数十年的使用寿命和零维护。在这些环境中,通常所做的是,如果程序因任何原因(包括段错误)崩溃而重新启动。此外,看门狗功能用于确保程序不会陷入计划外的无限循环。
想想您所依赖的所有没有重置按钮的嵌入式设备。他们依赖于不完美的硬件,因为没有硬件是完美的。软件必须处理硬件缺陷。换句话说,软件必须能够抵抗硬件的不当行为。
嵌入式并不是这一点至关重要的唯一领域。想一想仅处理 Whosebug 的服务器数量。如果您查看地面上的任何一项操作,电离辐射导致单个事件混乱的可能性很小,但如果您查看大量计算机 运行 24/7,这种可能性就变得不小了。 ECC 内存有助于防止这种情况,但并非所有内容都可以得到保护。
您的程序无法从分段错误中恢复,因为它不知道 任何东西 处于什么状态。
考虑这个类比。
你在缅因州有一栋漂亮的房子,有一个漂亮的前花园和一条穿过它的垫脚石小路 运行。无论出于何种原因,您选择用丝带将每块石头连接到下一块石头(a.k.a。您已将它们变成一个单链表)。
一天早上,从房子里出来,你踏上第一块石头,然后沿着丝带走到第二块,然后又走到第三块,但是,当你踏上 第四块 石头时,你突然发现自己在阿尔伯克基。
现在告诉我们 - 你 如何从 那个 中恢复过来?
你的程序有同样的困境。
出现了惊人的错误,但您的程序不知道它是什么,或者是什么导致了它,或者如何做 任何有用的东西。
因此:它崩溃并燃烧。