有没有一种方法可以捕获进程中的堆栈溢出?C++Linux

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

本文关键字:进程 堆栈 C++Linux 栈溢出 一种 方法 有没有      更新时间:2023-10-16

我有以下代码,它进入无限递归,并在耗尽分配给它的堆栈限制时触发seg错误。我正试图捕获这个分段错误并优雅地退出。然而,我无法在任何信号数字中捕捉到这种分割错误。

(一位客户正面临这个问题,并希望为这样的用例找到一个解决方案。将堆栈大小增加"极限堆栈大小128M"可以使他的测试通过。然而,他要求的是一个优雅的退出,而不是seg错误。下面的代码只是再现了实际问题,而不是实际算法的作用)。

感谢您的帮助。如果我试图捕捉信号的方式不正确,请也告诉我。编译: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 segfaultn";
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);  
}

这是一个需要解决的复杂问题。在这一点上,我不会给出工作代码,而是关注您已经遇到的一些"漂亮"问题,或者,当您继续为此进行编码时,将遇到的一些问题。

首先,你为什么反复出现?

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

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

其次,根据堆栈溢出的"严重程度"(您的测试程序可能不会触发,但现实世界中的程序可能会触发),作为"溢出影响动作"的内存访问(尝试)可能会以其他信号而非SIGSEGV结束。
你的例子"不具体地"捕捉到了所有信号,但在实践中这可能是不够的/相当令人困惑的——你向你的应用程序发送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);),并且恰好发生了其他有效的事情,即"无论您的堆栈是多少,减去一TB"),则该访问实际上只会起作用。如果没有编译器来创建你的"辅助"代码来识别这种访问,实际上你可能已经破坏了堆栈,并且在最终到达触发信号的mem区域之前仍然进行了多次成功/非信号内存访问
  • 对于其他无效操作,但堆栈访问,您可能会收到这些信号。堆访问、内存映射文件/设备访问也可能导致同样的结果

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

根据我的记忆,确切地说,在什么情况下,哪些信号填充了si_addr,以及其中的地址是发出内存访问的指令还是尝试访问的内存位置,有点依赖于系统和硬件(Linux的行为可能与Windows或MacOSX不同,在ARM上的行为也与x86不同)
因此,您还需要验证"此siginfo_t中的si_addr在信号线程堆栈附近",但也可能验证导致它的指令实际上是内存访问/si_addr,可以"追溯"到出现故障的指令。(查找出错指令的地址/程序计数器)。。。需要解码信号处理程序ucontext_t其他参数。。。在硬件/操作系统的细节中,你是深深的。

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