Gul*_*zar 112 c c++ exception segmentation-fault
继我之前的问题之后,大多数评论都说“不要这样做,你处于一种不确定的状态,你必须杀死一切并重新开始”。还有一个“安全”的解决方法。
我不明白的是为什么分段错误本质上是不可恢复的。
写入受保护内存的时刻被捕获 - 否则,将SIGSEGV不会被发送。
如果可以捕获写入受保护内存的时刻,我不明白为什么 - 理论上 - 它不能在某个低级别上恢复,并且不能将 SIGSEGV 转换为标准软件异常。
请解释为什么在分段错误之后程序处于不确定状态,因为很明显,在内存实际更改之前抛出了错误(我可能是错的,不明白为什么)。如果它被抛出,人们可以创建一个程序来更改受保护的内存,一次一个字节,出现分段错误,并最终重新编程内核 - 这种安全风险并不存在,因为我们可以看到世界仍然存在。
SIGSEGV)?Lun*_*din 99
\n\n分段错误到底是什么时候发生的(=SIGSEGV 何时发送)?
\n
当您尝试访问您无权访问的内存时,例如访问越界数组或取消引用无效指针。该信号SIGSEGV是标准化的,但不同的操作系统可能会以不同的方式实现它。“分段错误”主要是*nix系统中使用的术语,Windows称之为“访问冲突”。
\n\n为什么该进程在该点之后处于未定义的行为状态?
\n
因为程序中的一个或多个变量\xe2\x80\x99没有按预期运行。假设\xe2\x80\x99s 假设你有一些数组应该存储多个值,但你没有\xe2\x80\x99s 为所有这些值分配足够的空间。因此,只有那些分配了空间的内容才能正确写入,而其余超出数组范围的内容可以保存任何值。操作系统到底如何知道这些越界值对于应用程序的运行有多重要?它对他们的目的一无所知。
\n此外,在允许的内存之外写入通常会破坏其他不相关的变量,这显然是危险的并且可能导致任何随机行为。此类错误通常很难追踪。例如,堆栈溢出是一种易于覆盖相邻变量的分段错误,除非该错误被保护机制捕获。
\n如果我们观察没有任何操作系统、没有虚拟内存功能、只有原始物理内存的“裸机”微控制器系统的行为 - 它们只会默默地完全按照指示执行 - 例如,覆盖不相关的变量并继续运行。如果应用程序是关键任务,这反过来可能会导致灾难性的行为。
\n\n\n为什么无法恢复?
\n
因为操作系统不知道你的程序应该做什么。
\n尽管在上面的“裸机”场景中,系统可能足够智能,可以将自身置于安全模式并继续运行。汽车和医疗技术等关键应用不允许停止或重置,因为这本身可能很危险。他们宁愿尝试以有限的功能“跛行回家”。
\n\n\n为什么这个解决方案可以避免这种不可恢复的状态?甚至吗?
\n
该解决方案只是忽略错误并继续下去。它不能解决导致它的问题。它\xe2\x80\x99是一个非常脏的补丁,并且setjmp/longjmp通常是非常危险的函数,出于任何目的都应该避免。
\n我们必须认识到,分段错误是错误的症状,而不是原因。
\nChr*_*odd 55
请解释为什么在出现分段错误后程序处于未确定状态
我认为这是你的根本误解——SEGV 不会导致不确定状态,它只是它的一个症状。因此,问题(通常)是在 SIGSEGV 发生之前,程序就处于非法的、不可恢复的状态,并且从 SIGSEGV 中恢复不会改变这一点。
- 分段错误到底是什么时候发生的(=SIGSEGV 何时发送)?
SIGSEGV 发生的唯一标准方式是通过调用raise(SIGSEGV);。如果这是 SIGSEGV 的来源,那么显然可以通过使用 longjump 来恢复。但这只是一个微不足道的案例,现实中从未发生过。有一些特定于平台的方法可能会产生明确定义的 SEGV(例如,在 POSIX 系统上使用 mprotect),并且这些 SEGV 可能是可恢复的(但可能需要特定于平台的恢复)。然而,与 SEGV 相关的未定义行为的危险通常意味着信号处理程序将非常仔细地检查信号附带的(平台相关的)信息,以确保它是预期的内容。
- 为什么该进程在该点之后处于未定义的行为状态?
在那之前它(通常)处于未定义的行为状态;只是没有注意到。这是 C 和 C++ 中未定义行为的大问题——没有与之相关的特定行为,因此可能不会立即注意到。
- 为什么这个解决方案可以避免这种不可恢复的状态?甚至吗?
它不会,它只是回到某个较早的点,但不会做任何事情来撤消甚至识别导致问题的未定义行为。
Pet*_*des 25
当您的程序尝试取消引用错误指针时,就会发生段错误。(请参阅下面的更多技术版本,以及其他可能出现段错误的内容。)此时,您的程序已经因错误而导致指针损坏;尝试解除引用通常不是真正的错误。
除非你故意做一些可能会出现段错误的事情,并打算捕获并处理这些情况(请参阅下面的部分),否则你不会知道程序中的错误(或宇宙射线翻转一点)之前弄乱了什么。错误的访问实际上是错误的。 (这通常需要用 asm 编写,或者运行您自己 JIT 的代码,而不是 C 或 C++。)
C 和 C++ 没有定义导致分段错误的程序行为,因此编译器不会生成预期尝试恢复的机器代码。即使在手写的 asm 程序中,除非您预期会出现某种段错误,否则尝试尝试也是没有意义的,没有明智的方法来尝试真正恢复;最多你应该在退出之前打印一条错误消息。
如果您将一些新内存映射到尝试访问的访问方式的任何地址,或者将其从只读保护为读+写(在 SIGSEGV 处理程序中),则可以让错误指令执行,但这不太可能让执行恢复。大多数只读内存都是只读的,这是有原因的,让某些内容写入其中不会有任何帮助。尝试通过指针读取某些内容可能需要获取实际上在其他地方的某些特定数据(或者根本不读取,因为没有任何内容可读取)。因此,将新的零页映射到该地址将使执行继续,但对正确执行没有用处。与在 SIGSEGV 处理程序中修改主线程的指令指针相同,因此它会在错误指令后恢复。然后,使用寄存器中先前存在的任何垃圾(用于加载)或 CISC 或其他类似的其他结果,任何加载或存储都不会发生add reg, [mem]。
(您链接的捕获 SIGSEGV 的示例取决于编译器以明显的方式生成机器代码,而 setjump/longjump 取决于知道哪个代码将出现段错误,并且它发生时没有首先覆盖一些有效的内存,例如数据stdout结构printf 取决于,在到达未映射的页面之前,就像循环或 memcpy 可能发生的情况一样。)
Java 或 Javascript(没有未定义行为)等语言的 JIT 需要以明确定义的方式处理空指针取消引用,方法是 (Java) 在来宾计算机中抛出 NullPointerException。
实现 Java 程序逻辑(由 JIT 编译器作为 JVM 的一部分创建)的机器代码在使用之前需要至少检查一次每个引用,以防在 JIT 编译时无法证明它是非空,如果它想避免出现 JIT 代码错误。
但这是昂贵的,因此 JIT 可以通过允许它生成的 guest asm 中发生错误来消除一些空指针检查,即使此类错误将首先捕获到操作系统,然后才捕获到 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关于 NULL 指针检查的各种架构的硬件支持
另一个技巧是将数组的末尾放在页面的末尾(后面是足够大的未映射区域),因此每次访问的边界检查都是由硬件免费完成的。如果您可以静态地证明索引始终为正,并且它不能大于 32 位,那么您就一切就绪了。
操作系统传递 SIGSEGV 的常见原因是您的进程触发了操作系统发现“无效”的页面错误。(即,这是您的错误,而不是操作系统的问题,因此它无法通过分页换出到磁盘的数据(硬页面错误)或写入时复制或在首次访问时将新的匿名页面清零(软页面错误)来修复它。页面错误),并更新该虚拟页面的硬件页表以匹配您的进程在逻辑上映射的内容。)。
页面错误处理程序无法修复这种情况,因为用户空间线程通常不会向操作系统请求将任何内存映射到该虚拟地址。如果它只是尝试恢复用户空间而不对页表执行任何操作,则相同的指令将再次出错,因此内核会传递 SIGSEGV。该信号的默认操作是终止进程,但如果用户空间安装了信号处理程序,它可以捕获它。
其他原因包括(在 Linux 上)尝试在用户空间中运行特权指令(例如 x86 #GP“一般保护故障”硬件异常),或在 x86 Linux 上未对齐的 16 字节 SSE 加载或存储(又是 #GP 异常) 。_mm_load_si128这种情况可能发生在使用而不是手动矢量化的代码中loadu,甚至是在具有未定义行为的程序中进行自动矢量化的结果:Why does unaligned access to mmap'ed memory 有时在 AMD64 上出现段错误? (其他一些操作系统,例如 MacOS / Darwin,为未对齐的 SSE 提供 SIGBUS。)
所以你的程序状态已经混乱了,这就是为什么有一个 NULL 指针,而你期望它是非 NULL 的,或者是无效的。(例如,某些形式的释放后使用,或者用一些不代表有效指针的位覆盖的指针。)
如果你幸运的话,它会出现段错误并尽早失败,并且尽可能接近实际的错误;如果你运气不好(例如损坏了 malloc 簿记信息),那么直到有问题的代码执行很长时间后,你才会真正出现段错误。
Pau*_*l Z 21
关于分段错误,您必须了解的是,它们不是问题。他们是主近乎无限仁慈的一个例子(根据我大学里的一位老教授的说法)。分段错误表明存在严重错误,并且您的程序认为访问没有内存的内存是个好主意。这种访问本身并不是问题,而是问题所在。问题出现在某个不确定的时间之前,当出现问题时,最终导致您的程序认为此访问是一个好主意。访问不存在的记忆只是此时的一个症状,但是(这就是主的仁慈发挥作用的地方)这是一个很容易检测到的症状。情况可能会更糟;它可能正在访问有内存的内存,只是访问了错误的内存。操作系统无法帮助您避免这种情况。
操作系统无法弄清楚是什么导致你的程序相信如此荒谬的事情,它唯一能做的就是关闭程序,然后再以操作系统无法轻易检测到的方式做出其他疯狂的事情。通常,大多数操作系统还提供核心转储(程序内存的保存副本),理论上可以用来确定程序认为它在做什么。对于任何重要的程序来说,这都不是很简单,但这就是操作系统这样做的原因,以防万一。
Ale*_*x D 13
虽然您的问题专门询问分段错误,但真正的问题是:
如果软件或硬件组件被命令做一些无意义甚至不可能的事情,它应该做什么?什么也不做?猜猜实际上需要做什么并做到这一点?或者使用某种机制(例如“抛出异常”)来停止发出无意义命令的高级计算?
许多工程师多年来积累的大量经验一致认为,最好的答案是停止整体计算,并生成可以帮助某人找出问题所在的诊断信息。
除了非法访问受保护或不存在的内存之外,“无意义命令”的其他示例包括告诉 CPU 将整数除以零或执行无法解码为任何有效指令的垃圾字节。如果使用具有运行时类型检查的编程语言,则尝试调用未针对所涉及的数据类型定义的任何操作是另一个示例。
但为什么最好强制一个试图除以零的程序崩溃呢?没有人希望他们的程序崩溃。难道我们不能将除零定义为等于某个数字,例如零或 73 吗?难道我们就不能创造出能够跳过无效指令而不出错的 CPU 吗?对于从受保护或未映射的内存地址进行的任何读取,也许我们的 CPU 还可以返回一些特殊值,例如 -1。他们可以忽略对受保护地址的写入。不再有段错误!哎哟!
当然,这些事情都可以做,但是并没有什么真正的收获。重点是:虽然没有人希望他们的程序崩溃,但不崩溃并不意味着成功。人们编写和运行计算机程序是为了做某事,而不仅仅是为了“不崩溃”。如果一个程序有足够的错误来读取或写入随机内存地址或尝试除以零,那么即使允许它继续运行,它执行您实际想要的操作的可能性也非常低。另一方面,如果程序在尝试疯狂的事情时没有停止,它最终可能会做一些您不想要的事情,例如损坏或破坏您的数据。
从历史上看,一些编程语言被设计为总是“只做某事”来响应无意义的命令,而不是引发致命错误。这样做的目的是为了对新手程序员更加友好,但结果总是很糟糕。您的建议也是如此,即操作系统不应因段错误而导致程序崩溃。
sup*_*cat 10
在机器代码级别,许多平台允许在某些情况下“预期”分段错误的程序调整内存配置并恢复执行。这对于实现堆栈监控等功能可能很有用。如果需要确定应用程序曾经使用的最大堆栈量,可以将堆栈段设置为仅允许访问少量堆栈,然后通过调整堆栈段的边界和恢复代码执行。
然而,在 C 语言级别,支持此类语义将极大地阻碍优化。如果有人写这样的东西:
void test(float *p, int *q)
{
float temp = *p;
if (*q += 1)
function2(temp);
}
Run Code Online (Sandbox Code Playgroud)
编译器可能会将 的读取*p和读取-修改-写入序列*q视为相对于彼此无序,并生成仅*p在 的初始值*q不是 -1 的情况下读取的代码。p如果有效,则不会影响任何程序行为,但如果p无效,则此更改可能会导致从访问到增量*p之后发生的段错误*q,即使触发故障的访问是在增量之前执行的。
对于一种有效且有意义地支持可恢复段错误的语言,它必须比 C 标准更详细地记录允许和不允许的优化范围,而且我认为没有理由期待 C 标准的未来版本包含此类细节的标准。
老实说,如果我可以告诉计算机忽略分段错误。我不会接受这个选择。
通常,出现分段错误是因为您取消引用空指针或已释放的指针。当取消引用 null 时,行为完全未定义。当引用已释放的指针时,您提取的数据可能是旧值、随机垃圾,或者在最坏的情况下是来自另一个程序的值。在任何一种情况下,我都希望程序出现段错误,而不是继续并报告垃圾计算。
多年来,分段错误一直是我的眼中钉。我主要在嵌入式平台上工作,由于我们在裸机上运行,因此没有可记录核心转储的文件系统。系统刚刚锁定并死机,可能会从串行端口输出一些分离字符。这些年中最具启发性的时刻之一是我意识到分段错误(以及类似的致命错误)是一件好事。经历一个失败点并不好,但是将它们作为难以避免的失败点就位了。
诸如此类的故障并不是轻易产生的。硬件已经尝试了一切可以恢复的方法,而故障是硬件警告您继续操作是危险的。事实上,让整个进程/系统崩溃实际上比继续更安全。即使在具有受保护/虚拟内存的系统中,发生此类故障后继续执行也会破坏系统其余部分的稳定性。
如果可以捕捉到写入受保护内存的时刻
产生段错误的方法有很多,而不仅仅是写入受保护的内存。您还可以通过例如从具有无效值的指针读取来到达那里。这要么是由于之前的内存损坏(损坏已经造成,因此恢复为时已晚),要么是由于缺乏错误检查代码(应该由静态分析器和/或测试捕获)引起的。
为什么无法恢复?
您不一定知道问题的原因或问题的严重程度,因此您无法知道如何从中恢复。如果你的记忆被破坏了,你就无法相信任何事情。在可以恢复的情况下,您可以提前检测到问题,因此使用异常并不是解决问题的正确方法。
请注意,其中一些类型的问题可以在其他语言(例如 C#)中恢复。这些语言通常有一个额外的运行时层,可以提前检查指针地址并在硬件生成故障之前抛出异常。然而,对于像 C 这样的低级语言来说,你没有这些。
为什么这个解决方案可以避免这种不可恢复的状态?甚至吗?
该技术“有效”,但仅适用于人为的、简单化的用例。继续执行并不等同于恢复。有问题的系统仍然处于故障状态,存在未知的内存损坏,您只是选择继续前进,而不是听从硬件的建议来认真对待问题。没有人知道你的程序此时会做什么。在潜在的内存损坏后继续执行的程序对于攻击者来说将是一份提前的圣诞礼物。
即使没有任何内存损坏,该解决方案也会在许多不同的常见用例中出现问题。当您已经在一个受保护的代码块中时,您无法输入第二个受保护的代码块(例如在辅助函数中)。在受保护的代码块之外发生的任何段错误都将导致跳转到代码中不可预测的点。这意味着每一行代码都需要位于保护块中,并且您的代码将很难遵循。您无法调用外部库代码,因为该代码不使用此技术并且不会设置锚点setjmp。您的“处理程序”块无法调用库函数或执行任何涉及指针的操作,否则您可能需要无限嵌套的块。有些东西(例如自动变量)在longjmp.
关于关键任务系统(或任何系统),这里缺少一件事:在生产中的大型系统中,人们无法知道段错误在哪里,甚至不知道段错误是否存在,因此修复错误而不是症状的建议不成立。
我不同意这个想法。我见过的大多数分段错误都是由于取消引用指针(直接或间接)而没有首先验证它们而引起的。在使用指针之前检查它们会告诉您段错误在哪里。将复杂的语句拆分my_array[ptr1->offsets[ptr2->index]]为多个语句,以便您也可以检查中间指针。像 Coverity 这样的静态分析器擅长查找使用指针而无需验证的代码路径。这并不能保护您免受直接内存损坏引起的段错误的影响,但在任何情况下都无法从这种情况中恢复。
在短期实践中,我认为我的错误只是访问 null,仅此而已。
好消息!整个讨论毫无意义。指针和数组索引可以(而且应该!)在使用之前进行验证,提前检查的代码量比等待问题发生并尝试恢复要少得多。
这可能不是一个完整的答案,也绝不完整或准确,但它不适合评论
因此,SIGSEGV当您尝试以不应该的方式访问内存时(例如在只读状态下写入内存或从未映射的地址范围读取内存),可能会发生这种情况。如果您对环境足够了解,那么此类错误本身就可能是可以恢复的。
但是您想如何确定为什么会发生无效访问呢?
在对另一个答案的评论中,您说:
短期实践中,我认为我的错误只是访问null而已。
没有一个应用程序是没有错误的,所以为什么您假设如果可能发生空指针访问,您的应用程序不会发生例如释放后使用或对“有效”内存位置进行越界访问的情况,这不会发生立即导致错误或SIGSEGV.
释放后使用或越界访问还可以将指针修改为指向无效位置或成为 nullptr,但它也可能同时更改内存中的其他位置。如果您现在仅假设指针尚未初始化,并且您的错误处理仅考虑这一点,则您将继续使用处于与您的期望或其中一个编译器在生成代码时的期望不匹配的状态的应用程序。
在这种情况下,在最好的情况下,应用程序将在“恢复”后不久崩溃,在最坏的情况下,某些变量具有错误值,但它将继续以这些值运行。对于关键应用程序来说,这种疏忽可能比重新启动它造成的危害更大。
但是,如果您知道某个操作在某些情况下可能会导致SIGSEGV错误,您可以处理该错误,例如,您知道内存地址有效,但内存映射到的设备可能不完全可靠,并且可能会导致错误SIGSEGV因此,从 a 中恢复SIGSEGV可能是一种有效的方法。
取决于你所说的恢复是什么意思。如果操作系统向您发送 SEGV 信号,唯一明智的恢复是清理您的程序并从头开始运行另一个程序,希望不会遇到同样的陷阱。
在操作系统结束混乱之前,您无法知道自己的内存损坏了多少。如果您尝试从下一条指令或某个任意恢复点继续,您的程序可能会进一步出现错误。
似乎许多赞成的回复都忘记了,在某些应用程序中,生产中可能会发生段错误,而不会出现编程错误。期望高可用性、数十年的使用寿命和零维护。在这些环境中,通常所做的事情是,如果程序因任何原因(包括段错误)崩溃,则重新启动程序。此外,还使用看门狗功能来确保程序不会陷入计划外的无限循环。
想想您所依赖的所有没有重置按钮的嵌入式设备。他们依赖于不完美的硬件,因为没有硬件是完美的。软件必须处理硬件缺陷。换句话说,软件必须能够抵御硬件的不当行为。
嵌入式并不是这一点至关重要的唯一领域。想想处理 StackOverflow 的服务器数量。如果您观察地面上的任何一项操作,电离辐射导致单个事件扰乱的可能性很小,但如果您观察大量 24/7 运行的计算机,这种可能性就变得非常重要。ECC 内存有助于解决此问题,但并非所有内容都可以得到保护。
| 归档时间: |
|
| 查看次数: |
14425 次 |
| 最近记录: |