Linux下使用backtrace打印函数调用栈信息
Java和Python等语言都有比较简便的方法可以打印函数调用栈,那么在Linux下使用C语言有没有办法呢?
据说有多种方法。本文介绍最基本的方法,即使用 glibc 的 backtrace() 和 backtrace_symbols() 等 API.
在 Linux 下,运行 man 命令可以查看到帮助文档。
man 3 backtrace
文档并不长。下面翻译主要部分如下:
首先,需要包含头文件,并看一下相关的3个API的声明:
#include <exeinfo.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
backtrace 有 2 个参数,第1个参数实际上是一个 void * 类型的数组,将来所有的 frame 信息都会存在这个数组里;第2个参数 size 的含义是指明该数组的大小。比如,数组大小为5,而实际frames有10层,则只存储最新的 5 个 frames;
backtrace 的返回值的含义是究竟返回了多少层的 frames,比如,size指定为10,而frames只有5层,则返回5; 若size指定为5,而frames有10层,则也返回5. -
backtrace_symbols
这个函数用来解析每个frame中的函数地址代表的函数名称是什么。它必须要在运行完了上面的 backtrace 函数之后才能用。为什么呢?因为它的第1个参数就是被上面的 backtrace 函数填充了的那个 void * 数组。而它的第2个参数指的是要解析该buffer数组中的几个元素。
该函数的使用有3个注意点:
一、它返回的是一个char *数组,代表函数名数组,但是返回的这个 char ** 必须要被调用者 free 掉,而该char * 数组内的每个 char * 是不能被free的;
二、出错情况下返回 NULL
三、一般情况下,该函数解析不出函数名;只有在编译的时候加了 “-rdynamic” 选项,将来运行时才能解析出函数名。 -
backtrace_symbols_fd
该函数和 backtrace_symbols 类似,都是把函数地址解析成函数名称。不同点在于,它不会返回任何东西,而是把解析出的函数名称写进第3个参数,即一个文件描述符。 -
其他
1> 这3个函数只有 backtrace() 是线程安全函数
2> 编译器优化,比如 “-O3”, “-O2” 等,可能会导致调用层次丢失(笔者注:后面给一个例子)
3> inline 函数没有 stack frame ,因此不会被打印出来
4> Tail-call 优化也会导致函数名称的打印丢失
以下给出一个实例程序:
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_FRAMES 10
int myfunc1(int);
int myfunc2(int);
int myfunc3(int);
void printCallers()
{
int layers = 0, i = 0;
char ** symbols = NULL;
void * frames[MAX_FRAMES];
memset(frames, 0, sizeof(frames));
layers = backtrace(frames, MAX_FRAMES);
for (i=0; i<layers; i++) {
printf("Layer %d: %p\n", i, frames[i]);
}
printf("------------------\n");
symbols = backtrace_symbols(frames, layers);
if (symbols) {
for (i=0; i<layers; i++) {
printf("SYMBOL layer %d: %s\n", i, symbols[i]);
}
free(symbols);
}
else {
printf("Failed to parse function names\n");
}
}
int myfunc1(int a)
{
int b = a + 5;
int result = myfunc2(b);
return result;
}
int myfunc2(int b)
{
int c = b * 2;
int result = c + myfunc3(c);
return result;
}
int myfunc3(int c)
{
int d = c << 2;
printCallers();
d = d/0;
return d;
}
int main()
{
int result = 0;
result = myfunc1(1);
printf("result = %d\n", result);
return 0;
}
这个程序的调用栈是: _start -> _start_main -> main -> myfunc1 -> myfunc2 -> myfunc3 -> printCallers
所以总共是7层.
另外,因为在 myfunc3 调用 printCallers 之后,执行了一句除以0的操作,会导致引起 coredump.
首先,不加任何优化编译:
gcc -rdynamic test.c
运行结果如下:
./a.out
Layer 0: 0x556a4408ab5f
Layer 1: 0x556a4408ac79
Layer 2: 0x556a4408ac4c
Layer 3: 0x556a4408ac27
Layer 4: 0x556a4408aca5
Layer 5: 0x7f4ecbabf34a
Layer 6: 0x556a4408aa3a
------------------
SYMBOL layer 0: ./a.out(printCallers+0x45) [0x556a4408ab5f]
SYMBOL layer 1: ./a.out(myfunc3+0x1e) [0x556a4408ac79]
SYMBOL layer 2: ./a.out(myfunc2+0x1d) [0x556a4408ac4c]
SYMBOL layer 3: ./a.out(myfunc1+0x1e) [0x556a4408ac27]
SYMBOL layer 4: ./a.out(main+0x19) [0x556a4408aca5]
SYMBOL layer 5: /lib64/libc.so.6(__libc_start_main+0xea) [0x7f4ecbabf34a]
SYMBOL layer 6: ./a.out(_start+0x2a) [0x556a4408aa3a]
Floating point exception (core dumped)
由上可见,函数名以及各层函数名都是可以被打印出来的。
运行 gdb ,打印所有的 bt, 然后和上面运行结果中的各个函数地址做对比,就会发现: 基本上上面打印出的几个函数地址都是在 bt 里的。
但 printCallers 的地址不在,这是因为,打印的上述信息的时候,printCallers 正在运行中,而执行除以0语句时,printCallers 已经执行完了,所以core dump文件中不会有这个函数的栈帧。
最后,加一点优化试试:
gcc -O3 -rdynamic test.c
运行结果如下:
./a.out
Layer 0: 0x5593d28eab18
Layer 1: 0x5593d28ea9cb
Layer 2: 0x7f6e0a67034a
Layer 3: 0x5593d28ea9fa
------------------
SYMBOL layer 0: ./a.out(printCallers+0x38) [0x5593d28eab18]
SYMBOL layer 1: ./a.out(main+0xb) [0x5593d28ea9cb]
SYMBOL layer 2: /lib64/libc.so.6(__libc_start_main+0xea) [0x7f6e0a67034a]
SYMBOL layer 3: ./a.out(_start+0x2a) [0x5593d28ea9fa]
Illegal instruction (core dumped)
由以上可见,在编译器做了优化之后,在实际运行中所出现的函数调用栈可能比原先想象中少了几层,更加简单。
最后, 想要用 C 语言在 Linux 下打印调用栈信息,除了本文介绍的方法以外,还可以使用 libunwind.就不在本文介绍范围内了。
此外,如果不是C而是C++程序,则需要做 demangling 处理,也会略有麻烦。
(完)