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

rus*_*enz 4 c++ linux recursion stack redhat

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

(一位客户正面临这个问题,并且想要针对此类用例的解决方案。通过“限制堆栈大小 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);  
}
Run Code Online (Sandbox Code Playgroud)

Fra*_*kH. 5

这是一个需要解决的极其复杂的问题。在这一点上,我不会提供工作代码,而是关注您遇到的一些“漂亮”问题 - 或者,当您继续为此编码时 - 将遇到的问题。

首先,为什么要递归?

原因是虽然信号处理程序是“执行上下文传输”,但默认情况下它们没有自己的堆栈。这意味着,如果您因堆栈溢出而收到信号,信号处理程序将尝试为可能传递给它的上下文分配堆栈空间 - 并且只会再次重新抛出相同的信号。

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

其次,根据堆栈溢出的“严重程度”(您的测试程序可能不会触发此问题,但现实世界的程序可能会),作为“溢出影响操作”的内存访问(尝试)可能最终会产生其他信号SIGSEGV.
您的示例“非特定”捕获了所有信号,但这在实践中可能相当不足/相当令人困惑 - 您发送应用程序 a或 shell/终端在后台SIGUSR1发送它绝对不表示堆栈溢出。 这意味着还有另一个问题 - 当由于堆栈溢出而进行“堆栈外”内存访问时,会出现哪些信号?您如何知道您收到的特定信号是由于堆栈访问引起的?这个问题的答案比乍一看更复杂:SIGTTOU

  • 如果堆栈溢出“足够小”,则可以想象它位于保护页内(有效映射,但故意不可读)并且会触发SIGSEGV.
  • 如果(没有使用保护页并且)访问的是未映射的内存区域,您将收到一个SIGBUS
  • 即使某些 CPU 指令也可能会产生影响,无论访问“无效内存地址 X”是否会导致SIGSEGVSIGBUS(例如,在 x86 上,某些指令会引发,#GP而其他指令#PF- 对于相同的内存地址读/写 - 并且 Linux 内核可能将其转换为SIGBUS另一个到SIGSEGV
  • 如果发生此访问的地方碰巧有其他内存映射(例如,您已经得到了char local_to_blow_stack[1ULL << 40]; memset(&local_to_blow_stack, 0, 1);)并且恰好发生了其他有效的事情,则“无论您的堆栈减去 1 TB”),该访问将在事实上只是工作。如果没有编译器创建“辅助”代码来识别此类访问,实际上您可能已经破坏了堆栈,并且在最终到达触发信号的内存区域之前仍然进行了许多成功/非信号内存访问。
  • 除了堆栈访问之外,您可能会收到其他无效操作的这些信号。堆访问、内存映射文件/设备访问也可能导致相同的结果。

因此,“仅捕获信号”,甚至“捕获由于堆栈溢出而可能发生的所有信号”是不够的。您需要在信号处理程序中解码内存访问位置,可能解码操作/CPU 指令,以验证尝试的内存访问实际上是“堆栈访问越界”。线程可以检索自己的堆栈边界 - https://man7.org/linux/man-pages/man3/pthread_getattr_np.3.html可用于此目的,至少在 Linux 上(_np意味着“不可移植”) - 不保证在所有系统上可用,其他系统可能有不同的接口来检索此信息) - 但是......找到被访问的内存位置取决于信号和再次访问指令。通常(但并非总是)它位于siginfosi_addr)字段中。

据我所知,哪些信号si_addr在什么情况下填充,以及其中的地址是否是发出内存访问的指令或尝试访问的内存位置,在某种程度上取决于系统和硬件(Linux 的行为可能有所不同)来自 Windows 或 MacOSX,并且在 ARM 上与 x86 上不同)
因此您还需要验证“其中si_addrsiginfo_t某个位置靠近有信号的线程堆栈”,但也可能验证导致它实际上是内存访问的指令/si_addr可以“追溯到”出错的指令。(找到错误指令的地址/程序计数器)...需要解码信号处理程序的另一个ucontext_t参数, ...并且您在硬件/操作系统细节中很深很深[这里递归无穷大]。

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