1、backtrace的用处
一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的。主要用于程序异常退出时寻找错误原因。
通常情况下,程序发生段错误时系统会发送SIGSEGV信号给程序,缺省处理是退出函数。我们可以使用 signal(SIGSEGV, &your_function);函数来接管SIGSEGV信号的处理,程序在发生段错误后,自动调用我们准备好的函数,从而在那个函数里来获取当前函数调用栈。
2、提供的接口
int backtrace(void **buffer, int size);
backtrace()函数用来获取程序中当前函数的回溯信息,即一系列的函数调用关系,获取到的信息被放在参数buffer中。buffer是一个数组指针,数组的每个元素保存着每一级被调用函数的返回地址。参数size指定了buffer中可存放的返回地址的数量。如果函数实际的回溯层级数大于size,则buffer中只能存放最近的函数调用关系,所以,想要得到完整的回溯信息,就要确保size参数足够大。
backtrace()函数的返回值为buffer中的条目数量,这个值不一定等于size,因为如果为得到完整回溯信息而将size设置的足够大,则该函数的返回值为buffer中实际得到的返回地址数量。
char **backtrace_symbols(void *const *buffer, int size);
通过backtrace()函数得到buffer之后,backtrace_symbols()可以将其中的返回地址都对应到具体的函数名,参数size为buffer中的条目数。backtrace_symbols()函数可以将每一个返回值都翻译成“函数名+函数内偏移量+函数返回值”,这样就可以更直观的获得函数的调用关系。
经过翻译后的函数回溯信息放到backtrace_symbols()的返回值中,如果失败则返回NULL。需要注意,返回值本身是在backtrace_symbols()函数内部进行malloc的,所以必须在后续显式地free掉。
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
backtrace_symbols_fd()的buffer和size参数和backtrace_symbols()函数相同,只是它翻译后的函数回溯信息不是放到返回值中,而是一行一行的放到文件描述符fd对应的文件中。
注意:
- backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后多将不能正确得到程序栈信息;加上 -g 选项以后创建符号表,关闭所有的优化机制。
- 在编译的时候需要加上-rdynamic选项让链接器将所有符号添加到动态符号表中,这样才能将函数地址翻译成函数名。另外,这个选项不会处理static函数,所以,static函数的符号无法得到。
- 内联函数没有栈帧,它在编译过程中被展开在调用的位置;
- 尾调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。
所以编译方法应该这样: $ gcc -g *.c -rdynamic -rdynamic需要放后面,不知道为什么?
3、示例:生成backtrace文件
/** add.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int add1(int num)
{
int ret = 0x00;
int *pTemp = NULL;
*pTemp = 0x01; /* 这将导致一个段错误,致使程序崩溃退出 */
ret = num + *pTemp;
return ret;
}
int add(int num)
{
int ret = 0x00;
ret = add1(num);
return ret;
}
/** dump.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> /* for signal */
#include <execinfo.h> /* for backtrace() */
#define BACKTRACE_SIZE 16
void dump(void)
{
int j, nptrs;
void *buffer[BACKTRACE_SIZE];
char **strings;
nptrs = backtrace(buffer, BACKTRACE_SIZE);
printf("backtrace() returned %d addresses\n", nptrs);
strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (j = 0; j < nptrs; j++)
printf(" [%02d] %s\n", j, strings[j]);
free(strings);
}
void signal_handler(int signo)
{
#if 0
char buff[64] = {0x00};
sprintf(buff,"cat /proc/%d/maps", getpid());
system((const char*) buff);
#endif
printf("\n=========>>>catch signal %d <<<=========\n", signo);
printf("Dump stack start...\n");
dump();
printf("Dump stack end...\n");
signal(signo, SIG_DFL); /* 恢复信号默认处理 */
raise(signo); /* 重新发送信号 */
}
/** backtrace.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> /* for signal */
#include <execinfo.h> /* for backtrace() */
extern void dump(void);
extern void signal_handler(int signo);
extern int add(int num);
int main(int argc, char *argv[])
{
int sum = 0x00;
signal(SIGSEGV, signal_handler); /* 为SIGSEGV信号安装新的处理函数 */
sum = add(sum);
printf(" sum = %d \n", sum);
return 0x00;
}
输出如下:
gm@gm-pc:~/test/backtrace
$ gcc -g -rdynamic backtrace.c add.c dump.c -o backtrace
gm@gm-pc:~/test/backtrace
$ ./backtrace
563ef7e06000-563ef7e07000 r--p 00000000 08:02 5389091 /home/gm/test/backtrace/backtrace
563ef7e07000-563ef7e08000 r-xp 00001000 08:02 5389091 /home/gm/test/backtrace/backtrace
563ef7e08000-563ef7e09000 r--p 00002000 08:02 5389091 /home/gm/test/backtrace/backtrace
563ef7e09000-563ef7e0a000 r--p 00002000 08:02 5389091 /home/gm/test/backtrace/backtrace
563ef7e0a000-563ef7e0b000 rw-p 00003000 08:02 5389091 /home/gm/test/backtrace/backtrace
7f3b68fff000-7f3b69021000 r--p 00000000 08:04 659962 /usr/lib/libc-2.28.so
7f3b69021000-7f3b6916c000 r-xp 00022000 08:04 659962 /usr/lib/libc-2.28.so
7f3b6916c000-7f3b691b8000 r--p 0016d000 08:04 659962 /usr/lib/libc-2.28.so
7f3b691b8000-7f3b691b9000 ---p 001b9000 08:04 659962 /usr/lib/libc-2.28.so
7f3b691b9000-7f3b691bd000 r--p 001b9000 08:04 659962 /usr/lib/libc-2.28.so
7f3b691bd000-7f3b691bf000 rw-p 001bd000 08:04 659962 /usr/lib/libc-2.28.so
7f3b691bf000-7f3b691c5000 rw-p 00000000 00:00 0
7f3b691f9000-7f3b691fb000 r--p 00000000 08:04 659917 /usr/lib/ld-2.28.so
7f3b691fb000-7f3b6921a000 r-xp 00002000 08:04 659917 /usr/lib/ld-2.28.so
7f3b6921a000-7f3b69222000 r--p 00021000 08:04 659917 /usr/lib/ld-2.28.so
7f3b69222000-7f3b69223000 r--p 00028000 08:04 659917 /usr/lib/ld-2.28.so
7f3b69223000-7f3b69224000 rw-p 00029000 08:04 659917 /usr/lib/ld-2.28.so
7f3b69224000-7f3b69225000 rw-p 00000000 00:00 0
7fffb3e86000-7fffb3ea7000 rw-p 00000000 00:00 0 [stack]
7fffb3f07000-7fffb3f0a000 r--p 00000000 00:00 0 [vvar]
7fffb3f0a000-7fffb3f0c000 r-xp 00000000 00:00 0 [vdso]=========>>>catch signal 11 <<<=========
Dump stack start...
backtrace() returned 8 addresses
[00] ./backtrace(dump+0x2e) [0x563ef7e072d2]
[01] ./backtrace(signal_handler+0xac) [0x563ef7e07458]
[02] /usr/lib/libc.so.6(+0x37e00) [0x7f3b69036e00]
[03] ./backtrace(add1+0x1a) [0x563ef7e07267]
[04] ./backtrace(add+0x1c) [0x563ef7e0729c]
[05] ./backtrace(main+0x34) [0x563ef7e0722d]
[06] /usr/lib/libc.so.6(__libc_start_main+0xf3) [0x7f3b69023223]
[07] ./backtrace(_start+0x2e) [0x563ef7e0712e]
Dump stack end...
[1] 11205 segmentation fault (core dumped) ./backtrace
gm@gm-pc:~/test/backtrace
$
4、backtrace文件分析方法
由此可见在调用完函数add1后就开始调用段错误信号处理函数了,所以问题是出在函数add1中。这似乎还不够,更准确的位置应该是在地址0x563ef7e07267处,这好像是一个虚拟地址,addr2line需要的估计是错误信息在elf文件中的地址,所以还需要减去文件的加载地址
但这到底是哪一行呢,我们使用addr2line命令来得到,执行如下:
我们可以通过 /proc 观察到一个正在运行着的Linux系统的内核数据信息以及各进程相关的信息。所以我们如果要查看某一个进程的内存空间情况,也可以通过它来进行。
也可以用gcc编译生成map文件分析错误的位置。立个flag方便以后查找,不写了。