一、导读
在 backtrace 中,通常只有一堆看不太懂的十六进制地址。然而利用 addr2line 这个工具,可以定位到对应文件的代码行。但该工具使用的前提条件是可执行程序或者动态链接库编译的时候带-g 选项。
具体来说,addr2line 的使用分两种情况:
如果关注的一行 backtrace 位于一个可执行文件中,那么直接
addr2line -e <executable> <address>
如果关注的 backtrace 位于一个动态链接库中,那么麻烦一些,因为动态链接库的基地址不是固定的。这个时候,首先要把进程的 memory map 找来。在 Linux 下,进程的 memory map 可以在
/proc/<pid>/maps
文件中得到。然后在这个文件中找到动态链接库的基地址,然后将 backtrace 中的地址 – 动态链接库的基地址,得到偏移地址 offset address, 最后addr2line -e <shared library> <offset address>
。
当然,用 GDB 也可以找出地址对应的代码行。不过相比 addr2line,GDB 需要将 BUG 现象重现一遍,所以对于不好重现的 BUG,或是随机重现的 BUG 来说,使用 addr2line 就可以直接从 backtrace 找到对应的代码行,不需要重现现象,比 GDB 使用起来更简单。
二、实践部分
1、获取程序的调用栈
在 Linux 上的 C/C++编程环境下,我们可以通过如下三个函数来获取程序的调用栈信息。
#include <execinfo.h>
/*
该函数获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针数组,
参数size用来指定buffer中可以保存多少个void*元素。函数的返回值是实际返回的void*元素
个数。buffer中的void*元素实际是从堆栈中获取的返回地址。
*/
int backtrace(void **buffer, int size);
/*
该函数将backtrace函数获取的信息转化为一个字符串数组,参数buffer是backtrace获取
的堆栈指针,size是backtrace返回值。函数返回值是一个指向字符串数组的指针,它包含
char*元素个数为size。每个字符串包含了一个相对于buffer中对应元素的可打印信息,包括
函数名、函数偏移地址和实际返回地址。backtrace_symbols生成的字符串占用的内存是
malloc出来的,但是是一次性malloc出来的,释放是只需要一次性释放返回的二级指针即可。
*/
char **backtrace_symbols(void *const *buffer, int size);
/*
该函数与backtrace_symbols函数功能相同,只是它不会malloc内存,而是将结果写入
文件描述符为fd的文件中,每个函数对应一行。该函数可重入。
*/
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
使用它们的时候有几点需要我们注意的地方:
1.backtrace 的实现依赖于栈指针(fp 寄存器),在 gcc 编译过程中任何非零的优化等级(-On 参数)或加入了栈指针优化参数-fomit-frame-pointer 后多将不能正确得到程序栈信息;
2.backtrace_symbols 的实现需要符号名称的支持,在 gcc 编译过程中需要加入-rdynamic 参数;
3.内联函数没有栈帧,它在编译过程中被展开在调用的位置;
4.尾调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。
2、捕获系统异常信号输出调用栈
当程序出现异常时通常伴随着会收到一个由内核发过来的异常信号,如当对内存出现非法访问时将收到段错误信号 SIGSEGV,然后才退出。利用这一点,当我们在收到异常信号后将程序的调用栈进行输出,它通常是利用 signal()函数,关于系统信号的。
三、从 backtrace 信息分析定位问题
1、测试程序
为了更好的说明和分析问题,我这里将举例一个小程序,它有三个文件组成分别是 backtrace.c、dump.c、add.c,其中 add.c 提供了对一个数值进行加一的方法,我们在它的执行过程中故意使用了一个空指针并为其赋值,这样人为的造成段错误的发生;dump.c 中主要用于输出 backtrace 信息,backtrace.c 则包含了我们的 main 函数,它会先注册段错误信号的处理函数然后去调用 add.c 提供的接口从而导致发生段错误退出。它们的源程序分别如下:
/*
* 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 addressesn", nptrs);
strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (j = 0; j < nptrs; j++)
printf(" [%02d] %sn", 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;
}
2、静态链接情况下的错误信息分析定位
我们首先将用最基本的编译方式将他们编译成一个可执行文件并执行,如下:
[root@localhost backtrace]# gcc -g -rdynamic backtrace.c add.c dump.c -o backtrace
[root@localhost backtrace]# ./backtrace
=========>>>catch signal 11 <<<=========
Dump stack start...
backtrace() returned 8 addresses
[00] ./backtrace(dump+0x1f) [0x400aa0]
[01] ./backtrace(signal_handler+0x2e) [0x400b66]
[02] /lib64/libc.so.6(+0x36400) [0x7f8e9479a400]
[03] ./backtrace(add1+0x1a) [0x400a44]
[04] ./backtrace(add+0x1c) [0x400a79]
[05] ./backtrace(main+0x2f) [0x400a0c]
[06] /lib64/libc.so.6(__libc_start_main+0xf5) [0x7f8e94786555]
[07] ./backtrace() [0x400919]
Dump stack end...
段错误(吐核)
由此可见在调用完函数 add1 后就开始调用段错误信号处理函数了,所以问题是出在函数 add1 中。这似乎还不够,更准确的位置应该是在地址 0x400a44 处,但这到底是哪一行呢,我们使用 addr2line 命令来得到,执行如下:
[root@localhost backtrace]# addr2line -e backtrace 0x400a44
/home/lxy/pipe/backtrace/add.c:132
2、动态链接情况下的错误信息分析定位
然而我们通常调试的程序往往没有这么简单,通常会加载用到各种各样的动态链接库。如果错误是发生在动态链接库中那么处理将变得困难一些。下面我们将上述程序中的 add.c 编译成动态链接库 libadd.so,然后再编译执行 backtrace 看会得到什么结果呢。
/* 编译生成libadd.so */
gcc -g -rdynamic add.c -fPIC -shared -o libadd.so
/* 编译生成backtrace可执行文件 */
gcc -g -rdynamic backtrace.c dump.c -L. -ladd -Wl,-rpath=. -o backtrace
其中参数 -L. -ladd 为编译时链接当前目录的 libadd.so;参数-Wl,-rpath=.为指定程序执行时动态链接库搜索路径为当前目录,否则会出现执行找不到 libadd.so 的错误。然后执行 backtrace 程序结果如下:
[root@localhost backtrace]# ./backtrace
=========>>>catch signal 11 <<<=========
Dump stack start...
backtrace() returned 8 addresses
[00] ./backtrace(dump+0x1f) [0x400a59]
[01] ./backtrace(signal_handler+0x2e) [0x400b1f]
[02] /lib64/libc.so.6(+0x36400) [0x7f00f096b400]
[03] ./libadd.so(add1+0x1a) [0x7f00f0d036bf]
[04] ./libadd.so(add+0x1c) [0x7f00f0d036f4]
[05] ./backtrace(main+0x2f) [0x400a1c]
[06] /lib64/libc.so.6(__libc_start_main+0xf5) [0x7f00f0957555]
[07] ./backtrace() [0x400929]
Dump stack end...
段错误(吐核)
此时我们再用前面的方法将得不到任何信息,如下:
[root@localhost backtrace]# addr2line -e libadd.so 0x7f00f0d036bf
??:0
这是为什么呢?
出现这种情况是由于动态链接库是程序运行时动态加载的而其加载地址也是每次可能多不一样的,可见 0x7f00f0d036bf 是一个非常大的地址,和能得到正常信息的地址如 0x400a44 相差甚远,其也不是一个实际的物理地址(用户空间的程序无法直接访问物理地址),而是经过 MMU(内存管理单元)映射过的。
有上面的认识后那我们就只需要得到此次 libadd.so 的加载地址然后用 0x7f00f0d036bf 这个地址减去 libadd.so 的加载地址得到的结果再利用 addr2line 命令就可以正确的得到出错的地方;另外我们注意到(add1+0x1a)其实也是在描述出错的地方,这里表示的是发生在符号 add1 偏移 0x1a 处的地方,也就是说如果我们能得到符号 add1 也就是函数 add1 在程序中的入口地址再加上偏移量 0x1a 也能得到正常的出错地址。
我们先利用第一种方法即试图得到 libadd.so 的加载地址来解决这个问题。我们可以通过查看进程的 maps 文件来了解进程的内存使用情况和动态链接库的加载情况,所以我们在打印栈信息前再把进程的 maps 文件也打印出来,加入如下代码:
char buff[64] = {0x00};
sprintf(buff,"cat /proc/%d/maps", getpid());
system((const char*) buff);
然后编译执行得到如下结果(打印比较多这里摘取关键部分):
[root@localhost backtrace]# ./backtrace
00400000-00401000 r-xp 00000000 08:03 1214128 /home/lxy/pipe/backtrace/backtrace
00601000-00602000 r--p 00001000 08:03 1214128 /home/lxy/pipe/backtrace/backtrace
00602000-00603000 rw-p 00002000 08:03 1214128 /home/lxy/pipe/backtrace/backtrace
7f625d06d000-7f625d231000 r-xp 00000000 08:03 104856 /usr/lib64/libc-2.17.so
7f625d231000-7f625d430000 ---p 001c4000 08:03 104856 /usr/lib64/libc-2.17.so
7f625d430000-7f625d434000 r--p 001c3000 08:03 104856 /usr/lib64/libc-2.17.so
7f625d434000-7f625d436000 rw-p 001c7000 08:03 104856 /usr/lib64/libc-2.17.so
7f625d436000-7f625d43b000 rw-p 00000000 00:00 0
7f625d43b000-7f625d43c000 r-xp 00000000 08:03 1214129 /home/lxy/pipe/backtrace/libadd.so
7f625d43c000-7f625d63b000 ---p 00001000 08:03 1214129 /home/lxy/pipe/backtrace/libadd.so
7f625d63b000-7f625d63c000 r--p 00000000 08:03 1214129 /home/lxy/pipe/backtrace/libadd.so
7f625d63c000-7f625d63d000 rw-p 00001000 08:03 1214129 /home/lxy/pipe/backtrace/libadd.so
7f625d63d000-7f625d65f000 r-xp 00000000 08:03 104849 /usr/lib64/ld-2.17.so
7f625d845000-7f625d848000 rw-p 00000000 00:00 0
7f625d85d000-7f625d85e000 rw-p 00000000 00:00 0
7f625d85e000-7f625d85f000 r--p 00021000 08:03 104849 /usr/lib64/ld-2.17.so
7f625d85f000-7f625d860000 rw-p 00022000 08:03 104849 /usr/lib64/ld-2.17.so
7f625d860000-7f625d861000 rw-p 00000000 00:00 0
7ffe18677000-7ffe18698000 rw-p 00000000 00:00 0 [stack]
7ffe187e2000-7ffe187e4000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
=========>>>catch signal 11 <<<=========
Dump stack start...
backtrace() returned 8 addresses
[00] ./backtrace(dump+0x1f) [0x400b39]
[01] ./backtrace(signal_handler+0x6e) [0x400c3f]
[02] /lib64/libc.so.6(+0x36400) [0x7f625d0a3400]
[03] ./libadd.so(add1+0x1a) [0x7f625d43b6bf]
[04] ./libadd.so(add+0x1c) [0x7f625d43b6f4]
[05] ./backtrace(main+0x2f) [0x400afc]
[06] /lib64/libc.so.6(__libc_start_main+0xf5) [0x7f625d08f555]
[07] ./backtrace() [0x400a09]
Dump stack end...
段错误(吐核)
Maps 信息 libadd.so 第一项表示的为地址范围如第一条记录中的 7f625d43b000-7f625d43c000,第二项 r-xp 分别表示只读、可执行、私有的,由此可知这里存放的为 libadd.so 的.text 段即代码段,后面的栈信息 0x7f625d43b6bf 也正好是落在了这个区间。所有我们正确的地址应为 0x7f625d43b6bf – 7f625d43b000 = 0x6bf,将这个地址利用 addr2line 命令得到如下结果:
[root@localhost backtrace]# addr2line -e libadd.so 0x6bf
/home/lxy/pipe/backtrace/add.c:13
可见也得到了正确的出错行号。
接下来我们再用提到的第二种方法即想办法得到函数 add 的入口地址再上偏移量来得到正确的地址。要得到一个函数的入口地址我们多种途径和方法,比如生成查看程序的 map 文件;使用 gcc 的 nm、readelif 等命令直接对 libadd.so 分析等。在这里我们只介绍生成查看程序的 map 文件的方法,其他方法可通过查看 gcc 手册和 baidu 找到。
1)利用 gcc 编译生成的 map 文件,用如下命令我们将编译生成 libadd.so 对应的 map 文件如下:
gcc -g -rdynamic add.c -fPIC -shared -o libadd.so -Wl,-Map,add.map
Map 文件中将包含关于 libadd.so 的丰富信息,我们搜索函数名 add1 就可以找到其在.text 段的地址如下:
[root@localhost backtrace]# more add.map | grep add1
0x00000000000006a5 add1
由此可知我们的 add1 的地址为 0x6a5,然后加上偏移地址 0x1a 即 0x6a5 + 0x1a = 0x6bf,由前面可知这个地址是正确的。
扩展:
通过在编译过程中生成 so 包的 map 文件,其中存放的是堆栈的各种信息,实际上除了生成 map 之外,还可以通过 readelf -s 和 nm 两个命令来查询逻辑地址,实现对问题的定位。
例如:
[root@localhost backtrace]# nm libadd.so | grep add1
00000000000006a5 T add1
[root@localhost backtrace]# readelf -s libadd.so | grep add1
7: 00000000000006a5 51 FUNC GLOBAL DEFAULT 11 add1
52: 00000000000006a5 51 FUNC GLOBAL DEFAULT 11 add1
由上可看出地址是一样的。
原文始发于微信公众号(Linux二进制):Linux 程序堆栈dump和问题定位
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/98443.html