如何在 C++ 中处理或避免堆栈溢出
Posted
技术标签:
【中文标题】如何在 C++ 中处理或避免堆栈溢出【英文标题】:How to handle or avoid a stack overflow in C++ 【发布时间】:2012-08-22 04:33:53 【问题描述】:在 C++ 中,堆栈溢出通常会导致程序不可恢复的崩溃。对于需要真正健壮的程序,这是一种不可接受的行为,特别是因为堆栈大小是有限的。关于如何处理问题的几个问题。
有没有办法通过通用技术来防止堆栈溢出。 (一种可扩展、强大的解决方案,包括处理消耗大量堆栈的外部库等)
如果发生堆栈溢出,有没有办法处理它们?最好,堆栈会展开,直到有处理程序来处理这种问题。
有些语言的线程具有可扩展堆栈。类似的东西在 C++ 中可能吗?
任何其他有关 C++ 行为解决方案的有用 cmets 将不胜感激。
【问题讨论】:
标准甚至没有提到堆栈,您应该指定您的目标平台;一些平台提供了拦截堆栈溢出的方法,甚至在堆栈几乎耗尽时获得“堆栈溢出警报”。 就个人而言,我发现堆栈溢出不是要避免的,而是要拥抱的。看看这里的伟大社区! Moing Duck 链接的现代版本? msdn.microsoft.com/en-us/library/89f73td2.aspx 使用智能编译器:gcc -fsplit-stack
,您很可能会出现堆栈溢出以及内存不足的情况。
从未发现堆栈溢出是个问题,(无论如何,在桌面操作系统上)。它确实发生了,但这只是因为我的严重错误,并且很容易调试。与绝大多数真正令人讨厌的错误相比,SO 不是问题。
【参考方案1】:
处理堆栈溢出不是正确的解决方案,相反,您必须确保您的程序不会溢出堆栈。
不要在堆栈上分配大变量(其中“大”取决于程序)。确保任何递归算法在已知最大深度后终止。如果递归算法可能会递归未知次数或大量次数,要么自己管理递归(通过维护自己的动态分配的堆栈),要么将递归算法转换为等效的迭代算法
必须“真正健壮”的程序不会使用“吃掉大量堆栈”的第三方或外部库。
请注意,某些平台会在发生堆栈溢出时通知程序并允许程序处理错误。例如,在 Windows 上,会引发异常。此异常不是 C++ 异常,但它是异步异常。而 C++ 异常只能由 throw
语句引发,而异步异常可能在程序执行期间的任何时候引发。不过这是意料之中的,因为堆栈溢出随时可能发生:任何函数调用或堆栈分配都可能溢出堆栈。
问题是堆栈溢出可能会导致异步异常被抛出,即使是从预计不会抛出任何异常的代码(例如,从 C++ 中标记为 noexcept
或 throw()
的函数)。因此,即使您确实以某种方式处理了此异常,您也无法知道您的程序处于安全状态。因此,处理异步异常的最好方法是根本不处理它(*)。如果抛出一个,则意味着程序包含错误。
其他平台可能有类似的方法来“处理”堆栈溢出错误,但任何此类方法都可能遇到相同的问题:预期不会导致错误的代码可能会导致错误。
(*) 有一些非常罕见的例外情况。
【讨论】:
使用第三方库通常是一个成本问题。如果您在某个时候在程序中使用 Windows,则必须直接或间接使用 WinApi。其他系统也一样。您将根本无法没有第三方库。 (你可能会认为 C++ 标准库是第三方。)但我的观点是,如果你想拥有一个超级健壮的程序并使用第三方库来降低成本,那么你可能需要一种方法来保证程序不会由于其他库中的堆栈溢出而完全崩溃。 你能告诉我们更多关于那些“非常罕见的例外”的信息吗?【参考方案2】:C++ 是一门功能强大的语言,而这种强大的力量带来了自取其辱的能力。我不知道发生堆栈溢出时有任何可移植机制来检测和纠正/中止。当然,任何此类检测都是特定于实现的。例如 g++ 提供-fstack-protector
来帮助监控您的堆栈使用情况。
一般来说,最好的办法是主动避免基于堆栈的大型变量,并小心递归调用。
【讨论】:
-fstack-protector
无助于监控过多的堆栈分配。它用于检测堆栈分配的变量何时超出其界限。【参考方案3】:
您可以使用良好的编程实践来防止堆栈溢出,例如:
-
对递归要非常小心,我最近看到一个 SO 是由于递归 CreateDirectory 函数编写不当而导致的,如果您不确定您的代码是否 100% 正常,则添加将在 N 次递归调用后停止执行的保护变量。或者最好不要编写递归函数。
不要在堆栈上创建巨大的数组,这可能是隐藏的数组,例如一个非常大的数组作为类字段。使用矢量总是更好。
使用 alloca 时要非常小心,尤其是当它被放入一些宏定义中时。我已经看到很多 SO 是由于将字符串转换宏放入使用 alloca 进行快速内存分配的 for 循环中。
确保您的堆栈大小是最佳的,这在嵌入式平台中更为重要。如果你的线程做的不多,那么给它小的堆栈,否则使用更大的。我知道预留应该只占用一些地址范围 - 而不是物理内存。
这些是我在过去几年看到的最 SO 的原因。
对于自动 SO 查找,您应该能够找到一些静态代码分析工具。
【讨论】:
【参考方案4】:Re:可扩展堆栈。您可以通过以下方式给自己更多的堆栈空间:
#include <iostream>
int main()
int sp=0;
// you probably want this a lot larger
int *mystack = new int[64*1024];
int *top = (mystack + 64*1024);
// Save SP and set SP to our newly created
// stack frame
__asm__ (
"mov %%esp,%%eax; mov %%ebx,%%esp":
"=a"(sp)
:"b"(top)
:
);
std::cout << "sp=" << sp << std::endl;
// call bad code here
// restore old SP so we can return to OS
__asm__(
"mov %%eax,%%esp":
:
"a"(sp)
:);
std::cout << "Done." << std::endl;
delete [] mystack;
return 0;
这是 gcc 的汇编语法。
【讨论】:
糟糕,让int *top = (mystack + 64*1024 - 1);
可能还回复:足够的力量射中自己的脚。
我认为那行不通。推送/弹出 esp 比移动到寄存器更好,因为您不知道编译器是否会决定将 eax 用于某些事情。【参考方案5】:
我认为那行不通。推送/弹出 esp 比移动到寄存器更好,因为您不知道编译器是否会决定使用 eax 来做某事。
【讨论】:
你能提一下在这种情况下“那个”指的是什么吗?因为 OP 没有提及/建议任何可能有效的具体内容。【参考方案6】:给你: https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/resetstkoflw?view=msvc-160
-
您自己不会捕获 EXCEPTION_STACK_OVERFLOW 结构化异常,因为操作系统会捕获它(在 Windows 的情况下)。
是的,您可以安全地从结构化异常(上面称为“异步”)中恢复,这与上面所指出的不同。如果您不能,Windows 将根本无法工作。 PAGE_FAULT 是可从中恢复的结构化异常。
我不太熟悉 Linux 和其他平台下的工作原理。
【讨论】:
以上是关于如何在 C++ 中处理或避免堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章
如何在 c / c++ 程序中检测可能/潜在的堆栈溢出问题?