预先在运行时检测堆栈溢出
Posted
技术标签:
【中文标题】预先在运行时检测堆栈溢出【英文标题】:Detecting stack overflows during runtime beforehand 【发布时间】:2016-05-15 08:51:29 【问题描述】:我有一个相当大的递归函数(另外,我用 C 编写),虽然我毫不怀疑堆栈溢出发生的情况极不可能,但它仍然是可能的。我想知道的是您是否可以检测堆栈是否会在几次迭代内溢出,因此您可以在不使程序崩溃的情况下进行紧急停止。
【问题讨论】:
我不认为有一个可移植的解决方案,但如果你不关心可移植性,你可以使用内联汇编检查堆栈指针的值,如果堆栈指针出现紧急退出小于某个值。 您也可以简单地将递归深度限制为最大深度。 不完全重复,但相关:***.com/questions/199747/… 最好的解决方案总是尽可能摆脱递归。递归只能作为解决问题的最后手段。 【参考方案1】:这是一个适用于 win-32 的简单解决方案。实际上类似于 Wossname 已经发布的内容,但不那么恶心:)
unsigned int get_stack_address( void )
unsigned int r = 0;
__asm mov dword ptr [r], esp;
return r;
void rec( int x, const unsigned int begin_address )
// here just put 100 000 bytes of memory
if ( begin_address - get_stack_address() > 100000 )
//std::cout << "Recursion level " << x << " stack too high" << std::endl;
return;
rec( x + 1, begin_address );
int main( void )
int x = 0;
rec(x,get_stack_address());
【讨论】:
啊,内联 asm 为那种温暖、模糊、工作安全的感觉:D 不错。【参考方案2】:另一种方法是在程序开始时了解堆栈限制,并且每次在您的递归函数中检查是否已接近此限制(在一定的安全范围内,例如 64 kb)。如果是,则中止;如果没有,请继续。
POSIX 系统的堆栈限制可以通过getrlimit
系统调用来了解。
线程安全的示例代码:(注意:它的代码假定堆栈向后增长,如x86
!)
#include <stdio.h>
#include <sys/time.h>
#include <sys/resource.h>
void *stack_limit;
#define SAFETY_MARGIN (64 * 1024) // 64 kb
void recurse(int level)
void *stack_top = &stack_top;
if (stack_top <= stack_limit)
printf("stack limit reached at recursion level %d\n", level);
return;
recurse(level + 1);
int get_max_stack_size(void)
struct rlimit rl;
int ret = getrlimit(RLIMIT_STACK, &rl);
if (ret != 0)
return 1024 * 1024 * 8; // 8 MB is the default on many platforms
printf("max stack size: %d\n", (int)rl.rlim_cur);
return rl.rlim_cur;
int main (int argc, char *argv[])
int x;
stack_limit = (char *)&x - get_max_stack_size() + SAFETY_MARGIN;
recurse(0);
return 0;
输出:
max stack size: 8388608
stack limit reached at recursion level 174549
【讨论】:
考虑到在main()
之前在堆栈上发现了多少垃圾,10 kB 还远远不够。使用更长的参数列表再试一次,看看 10 kB 是不够的。不过我想知道,肯定有可能以某种方式找到堆栈的开头。
10 kB 是一种适合我的启发式算法。在答案中将其更改为 64klb。学习堆栈内存区域地址的一种方法是读取 /proc/<pid>/maps
文件,但这是非常特定于平台的。【参考方案3】:
这是一个幼稚的方法,但它有点恶心......
当您第一次进入函数时,您可以存储在该函数中声明的变量的地址。将该值存储在您的函数之外(例如,在全局中)。在随后的调用中,将该变量的当前地址与缓存副本进行比较。递归越深,这两个值之间的距离就越远。
这很可能会导致编译器警告(存储临时变量的地址),但它确实有利于为您提供一种相当准确的方式来准确了解您正在使用多少堆栈。
不能说我真的推荐这个,但它会起作用。
#include <stdio.h>
char* start = NULL;
void recurse()
char marker = '@';
if(start == NULL)
start = ▮
printf("depth: %d\n", abs(&marker - start));
if(abs(&marker - start) < 1000)
recurse();
else
start = NULL;
int main()
recurse();
return 0;
【讨论】:
@MartinJames 可以使用线程局部变量或简单地将指针作为额外参数传递给工作函数。但我不明白这比仅仅计算递归的深度更好。 @MartinJames,非常正确,但这不是先决条件:)【参考方案4】:在 C 编程语言本身中,这是不可能的。一般来说,你不能轻易知道在用完之前你已经用完了堆栈。我建议您改为在实现中对递归深度设置可配置的硬限制,这样您就可以在超出深度时简单地中止。您还可以重写您的算法以使用辅助数据结构而不是通过递归使用堆栈,这为您提供了更大的灵活性来检测内存不足的情况; malloc()
会在失败时告诉你。
但是,您可以通过类似 UNIX 的系统上的类似过程获得类似的结果:
-
使用
setrlimit
将软堆栈限制设置为低于硬堆栈限制
为SIGSEGV
和SIGBUS
建立信号处理程序以获得堆栈溢出通知。一些操作系统为这些生成SIGSEGV
,而另一些则生成SIGBUS
。
如果您收到这样的信号并确定它来自堆栈溢出,请使用setrlimit
提高软堆栈限制并设置一个全局变量以识别发生这种情况。设置变量volatile
,这样优化器就不会破坏你的平原。
在您的代码中,在每个递归步骤中,检查是否设置了此变量。如果是,请中止。
这可能并非在任何地方都有效,并且需要特定于平台的代码来找出信号来自堆栈溢出。并非所有系统(尤其是早期的 68000 系统)在获得 SIGSEGV
或 SIGBUS
后都可以继续正常处理。
Bourne shell 使用类似的方法进行内存分配。
【讨论】:
是的,类似的黑客攻击在 Windows 中是可能的,但如果您使用支持异常并可以捕获 Windows 结构化堆栈溢出异常的语言,它会容易得多。 @MartinJames 该解决方案无论如何都是不可移植的,所以在 Windows 上只需使用任何 Windows API 来解决问题。以上是关于预先在运行时检测堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章
如何在 c / c++ 程序中检测可能/潜在的堆栈溢出问题?