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

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

背景

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

设计思路

本节介绍CBackTracer类的设计。

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

实现代码

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 头文件 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);
}

测试代码

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#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;
}

输出结果示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./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,否则无法分析出程序函数位置。