0%

实用工具之 backtrace

这段时间,发现了一个 Linux 系统下 Debug 时很好用的库函数——backtrace。

简介

backtrace是 Linux 系统提供的一个库函数,使用时需要引入头文件exeinfo.h,这个函数不属于 C/C++ 标准且是 Linux 独有的 API,其主要功能是追踪程序的执行路径,并返回函数调用栈中的信息,借助这些信息可以帮助我们快速分析程序的执行顺序。

函数原型

在 Linux 系统下,使用man命令可以很快捷的查询该 API 的用法:

1
$ man backtrace

我们会得到三个对应的 API:

1
2
3
4
5
#include <execinfo.h>

int backtrace(void **buffer, int size);
char **backtrace_symbols(void *const *buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);

这里我们只介绍前两个。

backtrace

1
int backtrace(void **buffer, int size);

函数功能:得到函数的调用栈信息,在那个函数体内就得到那个函数的调用栈信息。
参数含义:

  • buffer 是一个二级指针,用于接收返回的函数调用栈信息。
  • size 用于设置 buffer 所接收的调用栈信息的最大深度。
    说明:
  • 为了避免丢失一些信息,这个值一般需要大于等于函数调用栈的深度。
  • 函数返回值表示 buffer 所接收的调用栈信息的深度,这个值小于等于 size。

backtrace_symbols

1
char **backtrace_symbols(void *const *buffer, int size);

函数功能:将 backtrace 得到的函数调用栈信息转义为字符串数组。
函数参数:

  • buffer 指向调用 backtrace 得到的函数调用栈信息。
  • size 表示需要转化的函数调用栈深度。

示例

这里有一个简单的示例程序,来帮助我们熟悉它的用法:

egg.c
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
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>

void myBacktrace() {
int size = 64;
void** array = malloc(sizeof(void*) * size);
size_t s = backtrace(array, size);

char** strings = backtrace_symbols(array, s);
for(size_t i = 0; i < s; ++i) {
printf("%s\n", strings[i]);
}
free(strings);
}

void func1() {
myBacktrace();
}

void func2() {
func1();
}

int main() {
func2();
return 0;
}

/* output:
./a.out(myBacktrace+0x35) [0x4008c7]
./a.out(func1+0xe) [0x40092e]
./a.out(func2+0xe) [0x40093f]
./a.out(main+0xe) [0x400950]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f3c6fb62840]
./a.out(_start+0x29) [0x4007e9]
*/

上述这代码在编译时,需要添加链接选项:

1
$ gcc -rdynamic egg.c

另外关于输出,我们还需要说明一下:

  1. ./a.out是我们执行程序的指令。
  2. (...)中的信息以函数名+地址偏移量的形式输出,这个地址偏移量是相对于函数起始地址而言的。
  3. [...]中的信息是实际的执行地址。
  4. 我们还可以发现,输出顺序与函数调用的过程相反的,这也是因为函数调用是基于栈的缘故。
  5. 不要忘记最后得释放资源。

如何使用

一般来讲,用到backtrace的地方一定是可能会出错或将来会出错的地方,所以我们可以配合assert来帮助我们找出潜在的 bug。同时借助 C++ 提供的容器,也能更方便的得到结果。

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
#include <iostream>
#include <sstream>
#include <vector>
#include <string>

#include <stdlib.h>
#include <execinfo.h>
#include <assert.h>

#define ASSERT(x) \
if(!(x)) { \
std::cout << "ASSERTION: " #x \
<< "\nbacktrace:\n" \
<< BacktraceToString(64, 2); \
assert(x); \
}

void Backtrace(std::vector<std::string>& bt, int size = 128, int skip = 2) {
void** array = (void**)malloc(sizeof(void*) * size);
size_t s = backtrace(array, size);

char** strings = backtrace_symbols(array, s);
if(strings == NULL) {
// 自定义异常处理
return;
}
for(size_t i = 0; i < s - skip; ++i) {
bt.push_back(strings[i]);
}
// 释放内存
free(strings);
}

std::string BacktraceToString(int size = 64, int skip = 2) {
std::vector<std::string> bt;
Backtrace(bt, size, skip);
std::stringstream ss;
for(size_t i = 0; i < bt.size(); ++i) {
ss << bt[i] << std::endl;
}
return ss.str();
}

void func1() {
ASSERT(0);
}

void func2() {
func1();
}

int main() {
func2();

return 0;
}

/* output:
ASSERTION: 0
backtrace:
./a.out(_Z9BacktraceRSt6vectorINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESaIS5_EEii+0x48) [0x403d7a]
./a.out(_Z17BacktraceToStringB5cxx11ii+0x5a) [0x403ed6]
./a.out(_Z5func1v+0x4d) [0x404034]
./a.out(_Z5func2v+0x9) [0x40408b]
./a.out(main+0x9) [0x404097]
a.out: egg.cpp:45: void func1(): Assertion `0' failed.
[1] 8490 abort (core dumped) ./a.out
*/

上述代码中,主要做了以下几个动作:

  1. 定义Backtrace函数,目的是为了封装backtrace函数,同时提供了三个参数,其中skip这个参数表示需要跳过的调用栈信息。
  2. 定义BacktraceToString来讲得到的字符串数组转换为string,并输出。
  3. 定义ASSERT宏,配合assert用来断言表达式的同时,输出函数调用栈信息。

总结

好了,有关backtrace的基本用法就讲完了。