在调试代码时,难免会遇到段错误或各种异常导致进程退出,尤其处理大型工程代码时如果没有一个好的调试工具,定位问题很麻烦和费时间。下面简单介绍如何用backtrace定位程序退出位置。
Linux c/c++开发环境下,可以使用以下函数API来获取进程调用的堆栈信息
#include
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);
int backtrace(void **buffer, int size);
该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数 size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小,为了取得全部的函数调用列表,应保证buffer和size足够大。
- 不同编译器(eg:gcc/arm-xxx-gcc)的优化选项对获取调用堆栈信息完整性有干扰,backtrace的实现依赖于栈指针,例如在gcc编译过程中加入任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后都将不能正确得到程序栈信息;
- 内联函数没有堆栈框架,因为它在编译过程中是直接被展开在调用的位置;
char **backtrace_symbols(void *const *buffer, int size);
backtrace_symbols函数将从backtrace函数获取的信息转化为一个字符串数组.。参数buffer是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值),函数返回值是一个指向字符串数组的指针,它的大小同buffer相同,每个字符串包含buffer中每个元素对应地址的信息:它包括函数名、函数的偏移地址、和实际的返回地址。
- 当前只有使用ELF二进制格式的程序才能获取函数名称和偏移地址;
- 函数返回值如果需要支持展示函数名功能需要在编译链接时添加-rdynamic选项;(eg:在GNU ld链接器的系统中-rdynamic可用来通知链接器将所有符号添加到动态符号表中);
- 该函数的返回值是通过malloc函数申请的空间,因此调用结束必须使用free函数来释放指针,如果不能为字符串malloc足够的空间函数的返回值将会为NULL;
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
该函数与backtrace_symbols 函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行,不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况。
程序异常退出往往都是内核发出某个异常信号导致进程终止,比如内存非法访问内核将发出SIGSEGV信号,错误的运算符如除以零内核将发出SIGFPE信号。利用signal机制,就可以在进程里捕获这些异常信号并输出程序调用栈再退出。从而可以判断程序在哪个位置发生异常。
//backtrace.cpp
#include
#include
#include
#include
#include
#include
void signal_handle(int signal)
{
void *l_buffer[512];
char **l_ptrace;
printf("\r\n=========>>>catch signal %d <<<=========\r\n", signal);
printf("Dump stack start...\n");
int size = backtrace(l_buffer, 512);
l_ptrace = backtrace_symbols(l_buffer, size);
if(NULL == l_ptrace)
{
perror("backtrace_symbols");
exit(1);
}
for(int i = 0; i < size; i++)
{
fprintf(stdout," [%02d] %s\n", i, l_ptrace[i]);
}
printf("Dump stack end...\n");
free(l_ptrace);
exit(1);
}
int Test_SIGSEGV_signal(char *s8ptr)
{
char l_s8Num = 10;
*s8ptr += l_s8Num; //s8ptr==NULL这行将产生一个段错误
printf("this Num:%d\r\n", *s8ptr);
return *s8ptr;
}
int Test_SIGFPE_signal(int s32Num)
{
int l_s32Num1 = 0;
s32Num /= l_s32Num1; //这行将产生一个错误的运算符SIGFPE
printf("this Num:%d\r\n", s32Num);
return s32Num;
}
//main.cpp
#include
#include
#include
#include
#include
#include "backtrace.h"
int main(int argc, char *argv[])
{
signal(SIGSEGV, signal_handle);
signal(SIGFPE, signal_handle);
//signal(SIGTERM, signal_handle);
if (argc >= 2)
{
if (0 == strcmp(argv[1], "SIGSEGV")){
Test_SIGSEGV_signal(NULL);
}
else if (0 == strcmp(argv[1], "SIGFPE")){
Test_SIGFPE_signal(20);
}
}
while (1)
{
;
}
return 0;
}
qiuhui@ubuntu:~/work/share/backtrace$ g++ backtrace.cpp main.cpp -I./ -g -rdynamic
qiuhui@ubuntu:~/work/share/backtrace$ ./a.out SIGFPE
=>>>catch signal 8 <<<=
Dump stack start…
[00] ./a.out(_Z13signal_handlei+0x55) [0x400b4b]
[01] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7f0ff12af4b0]
[02]./a.out(_Z18Test_SIGFPE_signali+0x16) [0x400c71]
[03] ./a.out(main+0x81) [0x400d11]
[04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f0ff129a830]
[05] ./a.out(_start+0x29) [0x400a29]
Dump stack end…
以上输出信息可得到信号处理函数执行完的上一个函数是Test_SIGFPE_signal函数,由此可以定位到出现问题的函数是在Test_SIGFPE_signal,借助addr2line命令根据实际的返回地址[0x400c71]就可以定位到具体哪一行。
qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e a.out 0x400c71
/home/qiuhui/work/share/backtrace/backtrace.cpp:47
qiuhui@ubuntu:~/work/share/backtrace$
【1】先编译动态库,编译动态库时“-g -rdynamic”记得带上。
qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic backtrace.cpp -fPIC -shared -o libbacktrace.so
qiuhui@ubuntu:~/work/share/backtrace$ ls -l libbacktrace.so
-rwxrwxr-x 1 qiuhui qiuhui 11424 Jan 12 22:07 libbacktrace.so
qiuhui@ubuntu:~/work/share/backtrace$
【2】编译执行程序,“-Wl,-rpath=.”参数表示程序运行时优先去rpath指定路径寻找库文件,可加多个包含路径(-Wl,-rpath,/home/:/lib/:.),程序运行时的寻找顺序为添加的顺序。
qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic main.cpp -I ./ -L ./ -lbacktrace -Wl,-rpath=.
【3】运行程序
qiuhui@ubuntu:~/work/share/backtrace$ ./a.out SIGFPE
=>>>catch signal 8 <<<=
Dump stack start…
[00] ./libbacktrace.so(_Z13signal_handlei+0x59) [0x7fe1b2cfe9a9]
[01] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7fe1b29694b0]
[02]./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7fe1b2cfeada]
[03] ./a.out(main+0xc1) [0x400a47]
[04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fe1b2954830]
[05] ./a.out(_start+0x29) [0x4008b9]
Dump stack end…
qiuhui@ubuntu:~/work/share/backtrace$
由以上输出信息可得到实际结束地址是[0x7fe1b2cfeada],用addr2line命令来分析可以看到并不能定位到哪一行。
qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0x7fe1b2cfeada
??:0
因为动态库是动态链接,每次链接的内存都是不一样,所以刚刚得到的结束地址[0x7fe1b2cfeada]实际是不准确的,它是一个内存地址,并不是库文件里面的偏移地址,如果将刚刚的地址减去libbacktrace.so库加载的开始地址,其实就可以定位正确了。在main函数中加入输出进程的maps文件的信息,如下:
char l_s8aBuff[128] = {0x00};
snprintf(l_s8aBuff, sizeof(l_s8aBuff), "cat /proc/%d/maps", getpid());
system(l_s8aBuff);
再看下输出信息,输出信息太多,只把相关的贴上来
7f9eddc84000-7f9eddc85000 r-xp 00000000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9eddc85000-7f9edde84000 —p 00001000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9edde84000-7f9edde85000 r–p 00000000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9edde85000-7f9edde86000 rw-p 00001000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
=>>>catch signal 8 <<<=
Dump stack start…
[00] ./libbacktrace.so(_Z13signal_handlei+0x59) [0x7f9eddc849a9]
[01] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7f9edd8ef4b0]
[02] ./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7f9eddc84ada]
[03] ./a.out(main+0xd4) [0x400a9a]
[04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f9edd8da830]
[05] ./a.out(_start+0x29) [0x4008f9]
Dump stack end…
咱把0x7f9eddc84ada减去0x7f9eddc84000得到0xada再分析就可以了
qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0xada
/home/qiuhui/work/share/backtrace/backtrace.cpp:47
【4】通过反汇编代码段分析函数入口地址来定位行号,由上面信息可知实际返回地址是在函数([02] ./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7f9eddc84ada]) 偏移量0x16处。执行反汇编指令将其信息保存到log文件
qiuhui@ubuntu:~/work/share/backtrace$ objdump -d libbacktrace.so > log
在保存的log文件里搜索函数名_Z18Test_SIGFPE_signali就可以得到函数的入口地址(0xac4 ),信息如下:
0000000000000ac4 <_Z18Test_SIGFPE_signali>:
ac4: 55 push %rbp
ac5: 48 89 e5 mov %rsp,%rbp
ac8: 48 83 ec 20 sub $0x20,%rsp
acc: 89 7d ec mov %edi,-0x14(%rbp)
acf: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
ad6: 8b 45 ec mov -0x14(%rbp),%eax
ad9: 99 cltd
ada: f7 7d fc idivl -0x4(%rbp)
add: 89 45 ec mov %eax,-0x14(%rbp)
ae0: 8b 45 ec mov -0x14(%rbp),%eax
ae3: 89 c6 mov %eax,%esi
ae5: 48 8d 3d 8e 00 00 00 lea 0x8e(%rip),%rdi # b7a <_fini+0x7e>
aec: b8 00 00 00 00 mov $0x0,%eax
af1: e8 0a fd ff ff callq 800 printf@plt
af6: 8b 45 ec mov -0x14(%rbp),%eax
af9: c9 leaveq
afa: c3 retq
Disassembly of section .fini:
将(函数入口地址)0xac4 +(偏移量)0x16 = 0xada,再分析此地址就可以和前面得出的行号一致了。
qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0xada
/home/qiuhui/work/share/backtrace/backtrace.cpp:47
【5】用g++编译生成map文件搜索函数名Test_SIGFPE_signal也可以获取函数入口地址,计算方式和上面提到的反汇编代码段一样,具体操作如下:
qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic backtrace.cpp -fPIC -shared -o libbacktrace.so -Wl,-Map,map.log
打开map.log得到如下信息:
.text 0x0000000000000950 0x1ab /tmp/ccjH9RPy.o
0x0000000000000950 signal_handle(int)
0x0000000000000a74 Test_SIGSEGV_signal(char*)
0x0000000000000ac4 Test_SIGFPE_signal(int)
Linux中mprotect()函数详解 请点击!!!
Linux程序内存越界定位分析总结 请点击!!!