堆栈溢出是否会导致分段错误以外的结果?
Can a stack overflow result in something other than a segmentation fault?
在编译程序中(假设是 C 或 C++,但我想这个问题可能会扩展到任何具有调用堆栈的非 VM-ish 语言)- 通常当您溢出堆栈时,you get a segmentation fault :
Stack overflow is [a] cause, segmentation fault is the result.
但情况总是如此吗?堆栈溢出会导致其他类型的 program/OS 行为吗?
我也在询问非Linux、非Windows OSes 和非X86 硬件。 (当然,如果你没有硬件内存保护或 OS 支持它(例如 MS-DOS)那么就没有分段错误这样的东西;我问的是你的情况可能 出现分段错误,但会发生其他情况)。
注意:假设除了堆栈溢出之外,程序是有效的并且不会尝试越界访问数组、取消引用无效指针等
是的,即使在标准 OS (Linux) 和标准硬件 (x86) 上也是如此。
void f(void) {
char arr[BIG_NUMBER];
arr[0] = 0; // stack overflow
}
请注意,在 x86 上,堆栈向下增长,因此我们分配到数组的开头以触发溢出。通常的免责声明适用......确切的行为取决于比这个答案中讨论的更多的因素,包括你的 C 编译器的细节。
如果 BIG_NUMBER 刚好大到溢出,您将 运行 进入堆栈保护并出现分段错误。这就是堆栈保护的用途,它可以小到单个 4 KiB 页面(但不能更小,并且在 Linux 4.12 之前使用这个 4 KiB 大小)或者它可以更大(1 MiB默认为 Linux 4.12,请参见 mm: large stack guard gap),但它始终具有特定大小。
如果 BIG_NUMBER 足够大,溢出可以跳过堆栈保护并落在其他一些内存上,可能是有效的内存。这可能会导致您的程序行为不正确但不会崩溃,这基本上是最坏的情况:我们希望我们的程序在错误时崩溃,而不是做一些意外的事情。
Whosebug 是其中一个 many reasons for undefined behavior 的程序。在这种情况下,您可以获得预期的结果或分段错误,或者您的硬盘可能会被擦除等。不要期望任何已定义的行为,因为它是未定义的行为。
一件事是在运行溢出堆栈时发生的事情,这可能是很多事情。包括但不仅限于;分段错误,覆盖任何溢出后的变量,导致非法指令,什么都没有,还有更多。 "old" 经典论文 Smashing The Stack For Fun And Profit 描述了人们可以 "fun" 使用这些东西的很多方法。
另一件事是编译时可能发生的事情。在 C 和 C++ 中,超出数组或超出堆栈大小的写入是未定义的行为,当程序包含 UB anywhere 时,编译器基本上可以自由地做 whatever它需要 到 任何 程序的一部分。现代编译器在利用 UB 进行优化方面变得非常积极——通常假设 UB 永远不会发生,导致它们简单地删除包含 UB 的代码或导致分支总是或永远不会被采用,因为替代方案会导致 UB。有时编译器会引入 time travel or call a function that was never called in the source code 和许多其他可能导致真正令人困惑的 运行 时间行为的东西。
另请参阅:
What Every C Programmer Should Know About Undefined Behavior #1/3
What Every C Programmer Should Know About Undefined Behavior #2/3
What Every C Programmer Should Know About Undefined Behavior #3/3
A Guide to Undefined Behavior in C and C++, Part 1
其他答案已经很好地涵盖了PC端。我将谈谈嵌入式世界中的一些问题。
嵌入式代码确实有类似于段错误的东西。代码存储在某种非易失性存储器中(现在通常是闪存,但过去是某种 ROM 或 PROM)。写入此需要特殊操作来设置它;正常的内存访问可以从中读取但不能写入。此外,嵌入式处理器通常在它们的内存映射中有很大的间隙。如果处理器收到对只读内存的写请求,或者收到对物理上不存在的地址的读或写请求,处理器通常会抛出硬件异常。如果连接了调试器,则可以检查系统状态以找出问题所在,就像核心转储一样。
但不能保证堆栈溢出会发生这种情况。堆栈可以放在 RAM 中的任何位置,并且通常与其他变量放在一起。堆栈溢出的结果通常是破坏这些变量。
如果您的应用程序也使用堆(动态分配),那么通常分配一段内存,其中堆栈从该部分的底部开始向上扩展,堆从该部分的顶部开始,并且向下扩展。显然,这意味着动态分配的数据将成为第一个牺牲品。
如果你不走运,你甚至可能没有注意到它何时发生,然后你需要弄清楚为什么你的代码行为不正确。在最讽刺的情况下,如果被覆盖的数据是一个指针,那么当该指针试图访问无效内存时,您仍然可能会遇到硬件异常——但这将在堆栈溢出之后的某个时间发生,并且自然的假设通常是它是您的代码中的错误。
嵌入式代码有一个通用模式来处理这个问题,即通过将每个字节初始化为已知值来"watermark"堆栈。有时编译器可以做到这一点;或者有时您可能需要在 main() 之前的启动代码中自己实现它。你可以从栈尾往回看,找到不再设置这个值的地方,此时你就知道栈使用的高水位线;或者,如果一切都不正确,那么您就知道溢出了。嵌入式应用程序将其作为后台操作连续轮询并能够报告它以用于诊断目的是很常见的(也是良好的做法)。
在跟踪堆栈使用情况后,大多数公司会设置可接受的最坏情况余量以避免溢出。这通常在 75% 到 90% 之间,但总会有一些空闲。这不仅允许出现您尚未看到的更糟糕的最坏情况,而且在需要添加使用更多堆栈的新代码时,它还使未来的开发更容易。
在编译程序中(假设是 C 或 C++,但我想这个问题可能会扩展到任何具有调用堆栈的非 VM-ish 语言)- 通常当您溢出堆栈时,you get a segmentation fault :
Stack overflow is [a] cause, segmentation fault is the result.
但情况总是如此吗?堆栈溢出会导致其他类型的 program/OS 行为吗?
我也在询问非Linux、非Windows OSes 和非X86 硬件。 (当然,如果你没有硬件内存保护或 OS 支持它(例如 MS-DOS)那么就没有分段错误这样的东西;我问的是你的情况可能 出现分段错误,但会发生其他情况)。
注意:假设除了堆栈溢出之外,程序是有效的并且不会尝试越界访问数组、取消引用无效指针等
是的,即使在标准 OS (Linux) 和标准硬件 (x86) 上也是如此。
void f(void) {
char arr[BIG_NUMBER];
arr[0] = 0; // stack overflow
}
请注意,在 x86 上,堆栈向下增长,因此我们分配到数组的开头以触发溢出。通常的免责声明适用......确切的行为取决于比这个答案中讨论的更多的因素,包括你的 C 编译器的细节。
如果 BIG_NUMBER 刚好大到溢出,您将 运行 进入堆栈保护并出现分段错误。这就是堆栈保护的用途,它可以小到单个 4 KiB 页面(但不能更小,并且在 Linux 4.12 之前使用这个 4 KiB 大小)或者它可以更大(1 MiB默认为 Linux 4.12,请参见 mm: large stack guard gap),但它始终具有特定大小。
如果 BIG_NUMBER 足够大,溢出可以跳过堆栈保护并落在其他一些内存上,可能是有效的内存。这可能会导致您的程序行为不正确但不会崩溃,这基本上是最坏的情况:我们希望我们的程序在错误时崩溃,而不是做一些意外的事情。
Whosebug 是其中一个 many reasons for undefined behavior 的程序。在这种情况下,您可以获得预期的结果或分段错误,或者您的硬盘可能会被擦除等。不要期望任何已定义的行为,因为它是未定义的行为。
一件事是在运行溢出堆栈时发生的事情,这可能是很多事情。包括但不仅限于;分段错误,覆盖任何溢出后的变量,导致非法指令,什么都没有,还有更多。 "old" 经典论文 Smashing The Stack For Fun And Profit 描述了人们可以 "fun" 使用这些东西的很多方法。
另一件事是编译时可能发生的事情。在 C 和 C++ 中,超出数组或超出堆栈大小的写入是未定义的行为,当程序包含 UB anywhere 时,编译器基本上可以自由地做 whatever它需要 到 任何 程序的一部分。现代编译器在利用 UB 进行优化方面变得非常积极——通常假设 UB 永远不会发生,导致它们简单地删除包含 UB 的代码或导致分支总是或永远不会被采用,因为替代方案会导致 UB。有时编译器会引入 time travel or call a function that was never called in the source code 和许多其他可能导致真正令人困惑的 运行 时间行为的东西。
另请参阅:
What Every C Programmer Should Know About Undefined Behavior #1/3
What Every C Programmer Should Know About Undefined Behavior #2/3
What Every C Programmer Should Know About Undefined Behavior #3/3
A Guide to Undefined Behavior in C and C++, Part 1
其他答案已经很好地涵盖了PC端。我将谈谈嵌入式世界中的一些问题。
嵌入式代码确实有类似于段错误的东西。代码存储在某种非易失性存储器中(现在通常是闪存,但过去是某种 ROM 或 PROM)。写入此需要特殊操作来设置它;正常的内存访问可以从中读取但不能写入。此外,嵌入式处理器通常在它们的内存映射中有很大的间隙。如果处理器收到对只读内存的写请求,或者收到对物理上不存在的地址的读或写请求,处理器通常会抛出硬件异常。如果连接了调试器,则可以检查系统状态以找出问题所在,就像核心转储一样。
但不能保证堆栈溢出会发生这种情况。堆栈可以放在 RAM 中的任何位置,并且通常与其他变量放在一起。堆栈溢出的结果通常是破坏这些变量。
如果您的应用程序也使用堆(动态分配),那么通常分配一段内存,其中堆栈从该部分的底部开始向上扩展,堆从该部分的顶部开始,并且向下扩展。显然,这意味着动态分配的数据将成为第一个牺牲品。
如果你不走运,你甚至可能没有注意到它何时发生,然后你需要弄清楚为什么你的代码行为不正确。在最讽刺的情况下,如果被覆盖的数据是一个指针,那么当该指针试图访问无效内存时,您仍然可能会遇到硬件异常——但这将在堆栈溢出之后的某个时间发生,并且自然的假设通常是它是您的代码中的错误。
嵌入式代码有一个通用模式来处理这个问题,即通过将每个字节初始化为已知值来"watermark"堆栈。有时编译器可以做到这一点;或者有时您可能需要在 main() 之前的启动代码中自己实现它。你可以从栈尾往回看,找到不再设置这个值的地方,此时你就知道栈使用的高水位线;或者,如果一切都不正确,那么您就知道溢出了。嵌入式应用程序将其作为后台操作连续轮询并能够报告它以用于诊断目的是很常见的(也是良好的做法)。
在跟踪堆栈使用情况后,大多数公司会设置可接受的最坏情况余量以避免溢出。这通常在 75% 到 90% 之间,但总会有一些空闲。这不仅允许出现您尚未看到的更糟糕的最坏情况,而且在需要添加使用更多堆栈的新代码时,它还使未来的开发更容易。