堆栈溢出是否会导致除分段错误之外的其他内容?

ein*_*ica 36 c c++ stack-overflow segmentation-fault

在一个已编译的程序中(假设是C或C++,但我想这个问题可以扩展到任何带有调用堆栈的非VM语言) - 通常当你溢出堆栈时,会出现分段错误:

堆栈溢出是[a]原因,分段错误就是结果.

但这总是如此吗?堆栈溢出是否会导致其他类型的程序/操作系统行为?

我也问过非Linux,非Windows操作系统和非X86硬件.(当然,如果你没有硬件内存保护或操作系统支持(例如MS-DOS)那么就没有分段错误;我问的是你可能会遇到分段错误的情况但是还会发生其他事情).

注意:假设以外的堆栈溢出,该方案是有效的,而不是试图访问数组超出边界,取消引用无效指针等

Die*_*Epp 31

是的,即使在标准OS(Linux)和标准硬件(x86)上也是如此.

void f(void) {
    char arr[BIG_NUMBER];
    arr[0] = 0; // stack overflow
}
Run Code Online (Sandbox Code Playgroud)

请注意,在x86上,堆栈会增长,因此我们将分配到数组的开头以触发溢出.通常的免责声明适用......确切的行为取决于比本答案中讨论的更多因素,包括C编译器的细节.

如果BIG_NUMBER刚刚大到足以溢出,您将遇到堆栈保护并获得分段错误.这就是堆栈保护的用途,它可以小到单个4 KiB页面(但不小,在Linux 4.12之前使用这个4 KiB大小)或者它可以更大(Linux 4.12上默认为1 MiB) ,见mm:大堆保护间隙),但总是有一些特殊的尺寸.

如果BIG_NUMBER足够大,溢出可以跳过堆栈保护并落在其他一块内存上,可能是有效的内存.这可能会导致您的程序行为不正确但不会崩溃,这基本上是最糟糕的情况:我们希望我们的程序在错误时崩溃,而不是做一些无意识的事情.

  • @Linuxios MSVC在帧大于单个页面的函数中插入`_chkstk`调用.它的目的是避免在堆栈扩展期间出现段错误(Windows特定的事情),但是它还可以检测到堆栈溢出并引发结构化异常.这应该保证堆栈溢出总是导致异常并避免静默数据损坏. (7认同)
  • @Stefan是的,请参阅https://www.qualys.com/2017/06/19/stack-clash/stack-clash.txt. (6认同)
  • 出于好奇:任何标准编译器都会对这样的代码发出警告吗?一些"堆栈分配的局部变量......可能溢出......"? (5认同)
  • 由于堆栈分配/大小是链接器/加载器的责任,因此编译器不能轻易/可靠地发出警告/错误. (3认同)
  • 不会(不是)那是一个很大的潜在利用? (2认同)

Jes*_*uhl 7

有一件事是当你溢出堆栈时在运行时会发生什么,这可能是很多事情.包括但不仅限于; 分段错误,覆盖任何溢出的变量,导致非法指令,什么都没有,等等."旧的"经典论文Smashing The Stack For Fun和Profit描述了很多人可以用这些东西"玩得开心"的方式.

另一件事是在编译时会发生什么.在C和C++,编写超出数组或超过堆栈的大小是未定义行为,当一个程序包含UB 任何地方编译器基本上是免费做为所欲为,以任何你计划的一部分.现代编译器在利用UB进行优化时变得非常积极 - 通常假设UB从未发生过,导致他们只是删除包含UB的代码或导致分支始终或永远不会被占用,因为替代方案会导致UB.有时,编译器会引入时间旅行拨打这是从来没有在源代码中调用的函数和许多其他的东西,可能会导致真正令人困惑的运行时行为.

也可以看看:

每个C程序员应该知道什么是未定义的行为#1/3

每个C程序员应该知道什么是未定义的行为#2/3

每个C程序员应该知道什么是未定义的行为#3/3

C和C++中未定义行为指南,第1部分

C和C++中未定义行为指南,第2部分

C和C++中未定义行为指南,第3部分

  • 溢出堆栈*的方法之一(也是最常见的方法之一)是*从其边界访问堆栈分配的数组. (2认同)
  • @einpoklum您是指为堆栈分配的空间还是使用情况?前一种你肯定知道。后者可以是有界的或估计的。 (2认同)

Gra*_*ham 6

其他答案已经很好地涵盖了PC方面.我将谈谈嵌入式世界中的一些问题.

嵌入式代码确实有类似于段错误的东西.代码存储在某种非易失性存储器中(这些天通常是闪存,但过去是某种ROM或PROM).写这个需要特殊的操作来设置它; 正常的内存访问可以从中读取但不能写入.此外,嵌入式处理器通常在其存储器映射中存在较大间隙.如果处理器获得对只读存储器的写请求,或者如果它获得对物理上不存在的地址的读或写请求,则处理器通常会抛出硬件异常.如果连接了调试器,则可以检查系统状态以查找出现问题的情况,如核心转储.

但是不能保证堆栈溢出会发生这种情况.堆栈可以放在RAM中的任何位置,这通常与其他变量并列.堆栈溢出的结果通常是破坏这些变量.

如果您的应用程序也使用堆(动态分配),那么通常会分配一段内存,其中堆栈从该部分的底部开始向上扩展,堆从该部分的顶部开始并向下扩展.显然,这意味着动态分配的数据将成为第一个受害者.

如果你运气不好,你甚至可能没有注意到它何时发生,然后你需要找出你的代码行为不正确的原因.在最具讽刺意味的情况下,如果被覆盖的数据是一个指针,那么当指针试图访问无效内存时,你仍然可能会得到一个硬件异常 - 但这将是堆栈溢出后的一段时间,并且自然的假设通常是它的代码中的错误.

嵌入式代码有一个共同的模式来处理这个问题,即通过将每个字节初始化为已知值来对堆栈进行"水印".有时编译器可以这样做; 或者有时您可能需要在main()之前在启动代码中自己实现它.您可以从堆栈的末尾回头查找它不再设置为此值的位置,此时您知道堆栈使用的高水位标记; 或者如果它都是不正确的,那么你知道你有溢出.嵌入式应用程序通常(并且是良好实践)将其作为后台操作连续轮询,并且能够将其报告以用于诊断目的.

由于可以跟踪堆栈使用情况,大多数公司将设置可接受的最坏情况余量以避免溢出.这通常在75%到90%之间,但总会有一些备用.这不仅有可能存在您还没有看到的更糟糕的最坏情况,而且当需要添加使用更多堆栈的新代码时,它还使未来开发的生活更轻松.