捕获堆栈溢出

发布于 2024-11-29 19:20:47 字数 677 浏览 4 评论 0原文

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

更具体地说:

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

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

)捕获堆栈溢出的形式将涉及 longjmp 返回主循环。 (完全可以丢弃主循环下方堆栈帧中保存的所有数据。)

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

有更好的方法吗?具体来说,我并不期望有更好的便携式解决方案,但如果我有一个针对 Linux 的系统特定解决方案和另一个针对 Windows 的系统特定解决方案,那就没问题了。

我在 Windows 上看到过一些称为结构化异常处理的参考资料,尽管我看到的参考资料是关于将其转换为 C++ 异常处理机制的;可以从 C 访问它吗?如果可以的话,它对于这种情况有用吗?

我了解 Linux 可以让你捕获分段错误信号;是否可以可靠地将其转换为 longjmp 返回主循环?

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

What's the best way to catch stack overflow in C?

More specifically:

A C program contains an interpreter for a scripting language.

Scripts are not trusted, and may contain infinite recursion bugs. The interpreter has to be able to catch these and smoothly continue. (Obviously this can partly be handled by using a software stack, but performance is greatly improved if substantial chunks of library code can be written in C; at a minimum, this entails C functions running over recursive data structures created by scripts.)

The preferred form of catching a stack overflow would involve longjmp back to the main loop. (It's perfectly okay to discard all data that was held in stack frames below the main loop.)

The fallback portable solution is to use addresses of local variables to monitor the current stack depth, and for every recursive function to contain a call to a stack checking function that uses this method. Of course, this incurs some runtime overhead in the normal case; it also means if I forget to put the stack check call in one place, the interpreter will have a latent bug.

Is there a better way of doing it? Specifically, I'm not expecting a better portable solution, but if I had a system specific solution for Linux and another one for Windows, that would be okay.

I've seen references to something called structured exception handling on Windows, though the references I've seen have been about translating this into the C++ exception handling mechanism; can it be accessed from C, and if so is it useful for this scenario?

I understand Linux lets you catch a segmentation fault signal; is it possible to reliably turn this into a longjmp back to your main loop?

Java seems to support catching stack overflow exceptions on all platforms; how does it implement this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

女皇必胜 2024-12-06 19:20:47

在我的脑海中,捕捉堆栈过度增长的一种方法是检查堆栈帧地址的相对差异:

#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 ,则会写入一条消息。

当然,这假设您所在的架构使用线性始终向下增长或始终向上增长堆栈。

您不必在每个函数中都进行此检查,但通常会在达到您选择的限制之前捕获过大的堆栈增长。

Off the top of my head, one way to catch excessive stack growth is to check the relative difference in addresses of stack frames:

#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()...
}

This compares the address of the first stack frame for foo() to the current stack frame address, and if the difference exceeds MAX_ROOM it writes a message.

This assumes that you're on an architecture that uses a linear always-grows-down or always-grows-up stack, of course.

You don't have to do this check in every function, but often enough that excessively large stack growth is caught before you hit the limit you've chosen.

风铃鹿 2024-12-06 19:20:47

AFAIK,所有检测堆栈溢出的机制都会产生一些运行时成本。你可以让 CPU 检测段错误,但已经太晚了;你可能已经把一些重要的东西写满了。

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

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

AFAIK, all mechanisms for detecting stack overflow will incur some runtime cost. You could let the CPU detect seg-faults, but that's already too late; you've probably already scribbled all over something important.

You say that you want your interpreter to call precompiled library code as much as possible. That's fine, but to maintain the notion of a sandbox, your interpreter engine should always be responsible for e.g. stack transitions and memory allocation (from the interpreted language's point of view); your library routines should probably be implemented as callbacks. The reason being that you need to be handling this sort of thing at a single point, for reasons that you've already pointed out (latent bugs).

Things like Java deal with this by generating machine code, so it's simply a case of generating code to check this at every stack transition.

可是我不能没有你 2024-12-06 19:20:47

(我不会根据特定平台来打扰这些方法以获得“更好”的解决方案。它们通过限制语言设计和可用性来制造麻烦,但收效甚微。对于在 Linux 上“正常工作”的答案Windows,见上文。)

首先,从 C 的意义上来说,你不能以可移植的方式做到这一点。事实上,ISO C 根本不强制要求“堆栈”。迂腐地说,甚至当自动对象的分配失败时,行为实际上是未定义的,根据第 4p2 条 - 根本无法保证当调用嵌套太深时会发生什么。您必须依赖一些额外的实现假设(ISAOS ABI)才能做到这一点,因此您最终会得到 C + 别的东西,而不仅仅是 C。机器代码生成在 C 级别也不可移植。

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

除了限制调用深度之外,所有方法都有一些额外的运行时成本。成本很容易观察到,除非有一些硬件辅助的方法来摊销它(例如页表遍历)。可悲的是,现在情况并非如此。

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

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

另一种方法是在实现中编写复杂的异步样式代码(例如 trampolinesCPS 转换),依赖较少的本机调用框架可能的。通常很难做到正确,但它确实有效。通过这种方式实现的附加功能包括更容易的尾调用优化和更容易的一流连续捕获。

(I won't bother those methods depending on particular platforms for "better" solutions. They make troubles, by limiting the language design and usability, with little gain. For answers "just work" on Linux and Windows, see above.)

First of all, in the sense of C, you can't do it in a portable way. In fact, ISO C mandates no "stack" at all. Pedantically, it even seems when allocation of automatic objects failed, the behavior is literally undefined, as per Clause 4p2 - there is simply no guarantee what would happen when the calls nested too deep. You have to rely on some additional assumptions of implementation (of ISA or OS ABI) to do that, so you end up with C + something else, not only C. Runtime machine code generation is also not portable in C level.

(BTW, ISO C++ has a notion of stack unwinding, but only in the context of exception handling. And there is still no guarantee of portable behavior on stack overflow; though it seems to be unspecified, not undefined.)

Besides to limit the call depth, all ways have some extra runtime cost. The cost would be quite easily observable unless there are some hardware-assisted means to amortize it down (like page table walking). Sadly, this is not the case now.

The only portable way I find is to not rely on the native stack of underlying machine architecture. This in general means you have to allocate the activation record frames as part of the free store (on the heap), rather than the native stack provided by ISA. This does not only work for interpreted language implementations, but also for compiled ones, e.g. SML/NJ. Such software stack approach does not always incur worse performance because they allow providing higher level abstraction in the object language so the programs may have more opportunities to be optimized, though it is not likely in a naive interpreter.

You have several options to achieve this. One way is to write a virtual machine. You can allocate memory and build the stack in it.

Another way is to write sophisticated asynchronous style code (e.g. trampolines, or CPS transformation) in your implementation instead, relying on less native call frames as possible. It is generally difficult to get right, but it works. Additional capabilities enabled by such way are easier tail call optimization and easier first-class continuation capture.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文