用封装的栈回溯类捕获段错误

Posted 李迟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用封装的栈回溯类捕获段错误相关的知识,希望对你有一定的参考价值。

本文介绍使用自封装的 backtrace 类对段错误进行捕获,以方便分析运行错误的方法。并给出实现和测试代码。

背景

我们写程序难免会运行出错,常在河边,哪能不湿鞋。出错不可怕,怕的是无法定位问题,像段错误,在服务端、嵌入式等领域,很多时候都无迹可寻,我们可以用 coredump 进行事后分析,但还是略显麻烦。

设计思路

本节介绍CBackTracer类的设计。

  • 利用backtracebacktrace_symbols可以获取函数符号和地址。
  • 可以指定获取的函数数量,本文暂定为10,如果回溯的函数数量少于指定的,则按实际数量显示。
  • 得到地址后,使用addr2line命令解析出对应的文件行号。由于该命令需要程序名称,因此需要调用者提供程序名称,与信号值一并传递。
  • 由于sigaction的回调函数不能使用类内的函数,因为单独编写之。

实现代码

实现代码如下:

// 头文件 backtraceplus.h
#ifndef BACKTRACEPLUS_H
#define BACKTRACEPLUS_H

class CBackTracer {
public:
    CBackTracer() {}
    CBackTracer(const char* name, int sig); // argv[0] SIGSEGV
    ~CBackTracer() {}

    void Setup(const char* name, int sig);
private:
};

#endif


// 实现文件 backtraceplus.cpp
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <signal.h>

#include "backtraceplus.h"

// 程序名,用于输出函数行号
static char g_exeName[256] = {0};

// 不能作为了类成员
void fault_trap(int sig, siginfo_t * siginfo, void *myact)
{
    printf("Catch SegmentFault!!\\n");

    void *array[10] = { 0 };
    int num = backtrace(array, 10);
    char **calls = backtrace_symbols(array, num);

    for (int i = 0; i < num; i++)
    {
        char *symbol = calls[i];

        char addr[64] = { 0 };
        char *p = strstr(symbol, "[0x");
        snprintf(addr, sizeof(addr), p + 1);
        *(addr + strlen(addr) - 1) = 0;

        char cmd[64] = { 0 };
        snprintf(cmd, sizeof(cmd), "addr2line %s -s -e %s", addr, g_exeName);

        FILE *fp = popen(cmd, "r");

        char buf[256] = { 0 };
        fread(buf, sizeof(buf), sizeof(char), fp);

        printf("%s %s", symbol, buf);

        pclose(fp);
        fp = NULL;
    }

    exit(0);
}

CBackTracer::CBackTracer(const char* name, int sig)
{
    Setup(name, sig);   
}

void CBackTracer::Setup(const char* name, int sig)
{
    strncpy(g_exeName, name, sizeof(g_exeName));

    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;
    act.sa_sigaction = fault_trap;
    sigaction(sig, &act, NULL);
}

测试代码

测试代码如下:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <execinfo.h>

#include "backtraceplus.h"

void badcall(void)
{
    int a = 250;
    printf("fault: %s\\n", a);
}

void foobar(void)
{
    printf("in %s, call bad\\n", __func__);
    
    badcall();
}

void myfunc(void)
{
    printf("in %s\\n", __func__);

    foobar();
}

int main(int argc, char* argv[])
{
    printf("test of backtrace...\\n");
    
    //CBackTracer mybt(argv[0], SIGSEGV);
    
    CBackTracer mybt;
    mybt.Setup(argv[0], SIGSEGV);

    myfunc();
    return 0;
}

输出结果示例如下:

$ ./a.out 
test of backtrace...
in myfunc
in foobar, call bad
fault: Catch SegmentFault!!
./a.out(_Z10fault_trapiP9siginfo_tPv+0x57) [0x40159f] backtraceplus.cpp:23
/usr/lib64/libc.so.6(+0x363b0) [0x7f9baac973b0] ??:0
/usr/lib64/libc.so.6(_IO_vfprintf+0x4a79) [0x7f9baacae029] ??:0
/usr/lib64/libc.so.6(_IO_printf+0x99) [0x7f9baacb4459] ??:0
./a.out(_Z7badcallv+0x23) [0x401800] main.cpp:23
./a.out(_Z6foobarv+0x21) [0x401823] main.cpp:31
./a.out(_Z6myfuncv+0x1d) [0x401882] main.cpp:46
./a.out(main+0x50) [0x4018d4] main.cpp:59
/usr/lib64/libc.so.6(__libc_start_main+0xf5) [0x7f9baac83505] ??:0
./a.out() [0x401169] ??:?

注:已经能捕获到段错误,由于系统库没有源码,因此libc.so.6文件最后显示的是??:0,但我们的测试程序a.out可以显示行号。根据行号,可以逐步排查问题。

小结

本文的示例有几个依赖条件:系统需安装有addr2line命令,程序需使用调试版本,不能strip,否则无法分析出程序函数位置。

以上是关于用封装的栈回溯类捕获段错误的主要内容,如果未能解决你的问题,请参考以下文章

java知识28 Java封装多测师

“未捕获的类型错误:无法在 Websocket Angular JS 上读取未定义的属性‘延迟’”

常用Javascript代码片段集锦

(重磅原创)冬之焱: 谈谈Linux内核的栈回溯与妙用

栈回溯技术

kmp算法的个人理解