记一次SIGSEGV引发的悲剧

在C/C++的项目线上环境部署的时候,会使用minidump的机制,来避免进程宕掉的时候生成巨大的core文件。比如,注册各种信号处理函数,记录发生宕机的backtrace到文件,然后把进程尽快的拉起。类似如下这样:

void minidump_handle(int sig) 
{
    // do-something 
    // 记录 backtrace
    // 记录现场
    exit(-1);
}

int install_minidump_handle()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act));

    act.sa_handler = minidump_handle;

    int ret = 0;

    ret = sigaction(SIGBUS, &act, NULL);
    if (ret != 0)
    {
        printf("failed to install minidump handle for SIGBUS. error: %d", errno);
        return ret;
    }
    
    ret = sigaction(SIGSEGV, &act, NULL);
    if (ret != 0)
    {
        printf("failed to install minidump handle for SIGSEGV. error: %d", errno);
        return ret;
    }

    ret = sigaction(SIGABRT, &act, NULL);
    if (ret != 0)
    {
        printf("failed to install minidump handle for SIGABRT. error: %d", errno);
        return ret;
    }

    ret = sigaction(SIGFPE, &act, NULL);
    if (ret != 0)
    {
        printf("failed to install minidump handle for SIGFPE. error: %d", errno);
        return ret;
    }
    return 0;
}

一般会注册四个信号处理函数,这几个信号一般涵盖了新手写C++会掉的常见的坑。

SIGBUS 
产生这个信号的原因,一般是访问直接对齐的地址导致的;

SIGSEGV
段错误,访问空指针,栈溢出等问题会产生这个信号;

SIGABRT
assert的时候,会产生这个信号 ;

SIGFPE
fpe : float 计算异常,比如除以0等会产生这个信号;

一切都看着很正常,似乎看起来很完美。但是,对于如下的情况,上面的注册的信号处理函数就不管用了。在函数栈上面开辟了10mb的空间,函数栈的大小一般是8MB,这样会导致栈溢出。会产生sigsegv信号,但是上面的信号处理函数是无法正常工作的。程序会直接coredump。

void test_stackoverflow()
{
     // 在函数栈上面开辟一个巨大的空间 10mb
    char data[10*1024*1024];
    printf("data size %s", sizeof(data));
}

测试程序如下,sigsegv信号没有被捕捉到,程序直接core掉。

#include 
#include 
#include 
#include 
#include 

void minidump_handle(int sig)
{
    printf("catch stack overflow sig : %d\n", sig);
    exit(-1);
}

int install_minidump_handle()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act));

    act.sa_handler = minidump_handle;

    int ret = 0;

    ret = sigaction(SIGSEGV, &act, NULL);
    if (ret != 0)
    {
        return ret;
    }
}

void test_illegal_pointer()
{
    int *a = (int *)0x1;
    int b = *a;
}

int main()
{
     install_minidump_handle();  // 在栈上开辟巨大的空间的段错误不能被捕捉到

     // test_illegal_pointer(); // 访问空指针的段错误 可以被捕捉到
     test_stackoverflow();
}

那么,问题来了,为什么会出现这样的情况? 查下来发现,如果是在栈上开辟巨大的空间导致的段错误的时候,信号注册的处理函数已经无法在栈上进行调用信号处理的函数了。

问题定位了,接下来就是如何去解决?实验了一下,只要不要信号处理的函数放在栈上,声明为static 或者是new 出来的空间都是可以的。

可以写成如下的方式, 这样就可以正常的捕捉由于声明大的栈变量导致的段错误了。

int install_minidump_handle_in_static()
{
    static char stack[SIGSTKSZ];
    stack_t ss = {
        .ss_size = SIGSTKSZ,
        .ss_sp = stack,
    };
    struct sigaction sa = {
        .sa_handler = minidump_handle,
        .sa_flags = SA_ONSTACK
    };
    sigaltstack(&ss, 0);
    sigfillset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, 0);
}

完成的测试程序如下:

#include 
#include 
#include 
#include 
#include 
#include "minidump.h"

//#define _XOPEN_SOURCE 700

void minidump_handle(int sig)
{
    printf("catch stack overflow sig : %d\n", sig);
    exit(-1);
}

int install_minidump_handle()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act));

    act.sa_handler = minidump_handle;

    int ret = 0;

    ret = sigaction(SIGSEGV, &act, NULL);
    if (ret != 0)
    {
        return ret;
    }
}

int install_minidump_handle_in_static()
{
    static char stack[SIGSTKSZ];
    stack_t ss = {
        .ss_size = SIGSTKSZ,
        .ss_sp = stack,
    };
    struct sigaction sa = {
        .sa_handler = minidump_handle,
        .sa_flags = SA_ONSTACK
    };
    sigaltstack(&ss, 0);
    sigfillset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, 0);
}

void test_stackoverflow()
{
    test_stackoverflow();
    //char data[10*1024*1024];
    //printf("data size %s", sizeof(data));
}

void test_illegal_pointer()
{
    int *a = (int *)0x1;
    int b = *a;
}

int test()
{
    test_illegal_pointer();
}

int main()
{
    // if(1)
    // {
    //     install_minidump_handle();
    //     test_stackoverflow();
    //     //test_illegal_pointer();
    // }
    // else
    // {
    //     install_minidump_handle_in_static();
    //     test_stackoverflow();
    //     //test_illegal_pointer();
    // }
    
    install_minidump_handle();
    //test_illegal_pointer();
    test();
    
    return 0;
}

那找到了问题的所在,就比较容易解决了。

  1. 修改linux 信号捕捉的方式
  2. 对于在栈上开辟这样比较巨大结构体变量,比较好的做法是声明为static

声明为static 的时,把栈上的变量放到的static区,可能导致程序的二进制变大。这可能是附带来的一个小问题。


补充:

  • 如何获得backtrace
void debug_dump_backtrace()
{
    printf("[backtrace]\n");
    void *array[16];
    size_t size = backtrace(array, 16);
    char **strings = backtrace_symbols(array, static_cast(size));
    if (strings == NULL)
    {
        perror("backtrace_symbols ret null");
        return ;
    }
    for (size_t i=0; i

这样既可以记录到宕机现场的函数调用栈,有避免了生成巨大的core-dump文件的负担。

你可能感兴趣的:(记一次SIGSEGV引发的悲剧)