有没有办法在进程中捕获堆栈溢出? C++ Linux

Is there a way to catch stack overflow in a process? C++ Linux

我有以下代码进入无限递归并在用尽分配给它的堆栈限制时触发段错误。我正在尝试捕获此分段错误并优雅地退出。但是,我无法在任何信号编号中捕获此分段错误。

(一位客户正面临此问题,并希望为此类用例提供解决方案。将堆栈大小增加 "limit stacksize 128M" 之类的值可以使他的测试通过。但是,他要求的是优雅退出,而不是而不是段错误。以下代码只是重现了实际问题,而不是实际算法的作用)。

感谢任何帮助。如果我尝试捕获信号的方式有问题,请也告诉我。编译:g++ test.cc -std=c++0x

#include <iostream>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <string.h>

int recurse_and_crash (int val)
{
    // Print rough call stack depth at intervals.
    if ((val %1000) == 0)
    {
        std::cout << "\nval: " << val;
    }
    return val + recurse_and_crash (val+1);
}

void signal_handler(int signal, siginfo_t * si, void * arg)
{
    std::cout << "Caught segfault\n";
    exit(0);
}


int main(int argc, char ** argv)
{
    int signal = 11; // SIGSEGV
    if (argc == 2)
    {
        signal = std::stoi(std::string(argv[1]));
    }

    struct sigaction sa;
    memset(&sa, 0, sizeof(struct sigaction));
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = signal_handler;
    sa.sa_flags   = SA_SIGINFO;

    sigaction(signal, &sa, NULL);
    recurse_and_crash (1);  
}

这是一个非常复杂的问题。在这一点上,我不会提供工作代码,而是专注于您遇到的一些 "nifty" 问题 - 或者,当您继续为此编码时 - 会遇到。

首先,你为什么要递归?

原因是虽然信号处理程序是 "execution context transfers",但默认情况下 它们没有自己的堆栈 。这意味着如果由于堆栈溢出而收到信号,信号处理程序将尝试为可能传递给它的上下文分配 space-on-the-stack - 并且只是再次重新抛出相同的信号.

要确保信号处理程序 运行 在它们自己的单独/预分配堆栈上,请使用 sigaltstack()sigaction()SA_ONSTACK 标志。

其次,取决于 "how badly" 堆栈超过 运行s(您的测试程序可能不会触发此但真实世界的程序可能),内存访问(尝试)是 "the overflow-effecting action" 可能以 other 信号结束,但 SIGSEGV
您的示例 "unspecifically" 捕获所有信号,但在实践中这可能相当不足/相当混乱 - 您向您的应用发送 SIGUSR1 或 shell/terminal 向其发送 SIGTTOU 处于后台状态绝对不表示 Whosebug。
这意味着还有另一个问题——当由于堆栈溢出而进行 "out of stack" 内存访问时,预期会出现哪些信号?你怎么知道你得到的特定信号是由于堆栈访问
答案又是更多比第一眼更复杂:

  • 如果堆栈溢出是"small enough",可以想象它在保护页内(一个有效的映射,但故意不可读)并且它会触发SIGSEGV
  • 如果(未使用保护页并且)访问的是未映射的内存区域,您将收到 SIGBUS
  • 甚至某些 CPU 指令可能会导致访问 "invalid memory address X" 导致 SIGSEGVSIGBUS(例如,在 x86 上,某些指令引发 #GP 而其他 #PF - 对于相同的内存地址 read/write - 并且 Linux 内核可能将一个翻译成 SIGBUS 另一个翻译成 SIGSEGV)
  • 如果恰好有 其他内存 映射到此访问发生的位置(例如,您有 char local_to_blow_stack[1ULL << 40]; memset(&local_to_blow_stack, 0, 1);)并且就这样发生了"whatever your stack is minus a terabyte") 上还有其他有效的东西,访问实际上是可行的。如果没有编译器为您创建 "assist" 代码来识别此类访问,实际上您可能已经炸毁了堆栈,并且在最终到达触发信号的内存区域之前仍然进行了许多成功/非信号内存访问。
  • 对于 其他无效 操作但 堆栈 访问,您可能会收到这些信号。堆访问、内存映射 file/device 访问也可能导致相同的结果。

所以"just catching signals",连"catching all signals that may possibly occur as a consequence of a stack overflow"都不够。您需要在信号处理程序中解码内存访问位置,可能是操作/cpu指令,以验证内存访问尝试实际上是 "stack access out of bounds"。线程有可能检索自己的堆栈边界 - https://man7.org/linux/man-pages/man3/pthread_getattr_np.3.html 可用于此,至少在 Linux 上(_np 暗示 'non portable' - 这不能保证要在所有系统上可用,其他人可能有不同的接口来检索此信息) - 但是......找到被访问的内存位置再次取决于信号和访问指令。 经常(但不是总是)它在siginfosi_addr)字段中。

据我所知,究竟是什么信号在什么情况下填充si_addr,以及那里的地址是否是例如发出内存访问的 指令 或尝试访问的 内存位置 在某种程度上取决于系统和硬件(Linux 可能与 Windows 或 MacOSX 的行为不同,并且在 ARM 上与在 x86 上不同)
因此您还需要验证 "the si_addr in this siginfo_t is somewhere-near the signaled thread's stack",但也可能验证导致它的指令实际上是内存访问/si_addr 可以 "traced back" 到 指令 出错。那(找到错误指令的地址/程序计数器)...需要解码信号处理程序的other参数,ucontext_t ...在 HW / OS 细节中,你有很深的 [递归无穷大]。

此时我想终止; "simple" 但不是完美的解决方案只需要一个备用信号堆栈,以及通过 pthread_getattr_np() 检索当前堆栈边界的处理程序,以与 si_addr 进行比较。如果您或他人的生命取决于正确答案,请记住以上内容。