捕获堆栈溢出

时间:2011-08-15 15:59:17

标签: c stack-overflow

在C中捕获堆栈溢出的最佳方法是什么?

更具体地说:

C程序包含脚本语言的解释器。

脚本不受信任,可能包含无限的递归错误。口译员必须能够抓住这些并顺利地继续下去。 (显然,这部分可以通过使用软件堆栈来处理,但是如果可以用C语言编写大量的库代码块,性能会大大提高;至少,这需要在脚本创建的递归数据结构上运行C函数。)

捕获堆栈溢出的首选形式是将longjmp返回到主循环。 (丢弃在主循环下面的堆栈帧中保存的所有数据是完全可以的。)

回退可移植解决方案是使用局部变量的地址来监视当前堆栈深度,并使用每个递归函数来包含对使用此方法的堆栈检查函数的调用。当然,这会在正常情况下产生一些运行时开销;这也意味着如果我忘记将堆栈检查调用放在一个地方,那么解释器就会有潜在的错误。

有更好的方法吗?具体来说,我不期待更好的可移植解决方案,但如果我有一个针对Linux的系统特定解决方案和另一个针对Windows的解决方案,那就没关系。

我在Windows上看到了对结构化异常处理的引用,尽管我看过的引用一直是将它转换为C ++异常处理机制;可以从C访问它,如果是这样,它对这种情况有用吗?

我了解Linux可以捕获分段故障信号;是否可以将其可靠地转换为longjmp回到主循环?

Java似乎支持在所有平台上捕获堆栈溢出异常;它是如何实现的?

3 个答案:

答案 0 :(得分:3)

脱离我的头脑,捕获过多堆栈增长的一种方法是检查堆栈帧地址的相对差异:

#define MAX_ROOM    (64*1024*1024UL)    // 64 MB

static char *   first_stack = NULL;

void foo(...args...)
{
    char    stack;

    // Compare addresses of stack frames
    if (first_stack == NULL)
        first_stack = &stack;
    if (first_stack > &stack  &&  first_stack - &stack > MAX_ROOM  ||
        &stack > first_stack  &&  &stack - first_stack > MAX_ROOM)
        printf("Stack is larger than %lu\n", (unsigned long)MAX_ROOM);

    ...code that recursively calls foo()...
}

foo()的第一个堆栈帧的地址与当前堆栈帧地址进行比较,如果差值超过MAX_ROOM,则会写入一条消息。

这假设您使用的是一种使用线性始终向下扩展或始终在成长的堆栈的架构。

您无需在每个功能中执行此检查,但通常会在达到您选择的限制之前捕获过大的堆栈增长。

答案 1 :(得分:2)

AFAIK,所有检测堆栈溢出的机制都会产生一些运行时成本。您可以让CPU检测到seg-faults,但这已经太晚了;你可能已经在一些重要的东西上乱涂乱画了。

您说您希望您的解释器尽可能多地调用预编译的库代码。这很好,但是为了保持沙盒的概念,你的解释器引擎应该始终对例如沙箱负责。堆栈转换和内存分配(从解释语言的角度来看);您的库例程应该可以实现为回调。原因是你需要在一个点上处理这类事情,原因是你已经指出过(潜在的错误)。

像p这样的东西通过生成机器代码来解决这个问题,所以它只是生成代码以在每次堆栈转换时检查它的情况。

答案 2 :(得分:1)

(我不会因为特定的平台而烦恼这些方法,因为#34;更好的"解决方案。他们制造麻烦,通过限制语言设计和可用性,收益甚微。对于答案"只需在Linux和Windows上工作,请参见上文。)

首先,在C意义上的,您无法以便携方式进行。事实上,ISO C强制要求没有#34;堆栈"一点都不讽刺的是,甚至在自动对象的分配失败时,根据第4p2条,行为实际上是未定义的 - 根本不能保证当嵌套过深时调用会发生什么。你必须依赖一些额外的实现假设( ISA OS ABI )才能做到这一点,所以你最终得到C +别的东西,而不仅仅是C.运行时机器码生成在C级也不可移植。

(顺便说一句,ISO C ++有堆栈展开的概念,但仅在异常处理的上下文中。并且仍然无法保证堆栈溢出的可移植行为;尽管它似乎未指定,不是未定义的。)

除限制调用深度外,所有方式都有一些额外的运行时成本。除非有一些硬件辅助手段将其摊销(如页表行走),否则成本将很容易被观察到。可悲的是,现在情况并非如此。

我找到的唯一可移植方式是不依赖底层机器架构的本机堆栈。这通常意味着您必须将激活记录帧分配为免费存储(在堆上)的一部分,而不是ISA提供的本机堆栈。这不仅适用于解释型语言实现,也适用于已编译的语言实现,例如: SML / NJ。这种软件堆栈方法并不总是会导致性能下降,因为它们允许在对象语言中提供更高级别的抽象,因此程序可能有更多机会进行优化,尽管它不太可能是天真的解释器。 / p>

您有多种方法可以实现这一目标。一种方法是编写虚拟机。您可以分配内存并在其中构建堆栈。

另一种方法是在您的实现中编写复杂的异步样式代码(例如 trampolines CPS转换),依赖于较少的原生调用框架尽可能。通常很难做到正确,但它确实有效。通过这种方式启用的其他功能更容易尾部调用优化,并且更容易一流继续捕获。