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 处理,也会略有麻烦。
(完)