在Linux中如何利用backtrace信息解决问题
一、导读
在程序调试过程中如果遇到程序崩溃死机的情况下我们通常多是通过出问题时的栈信息来找到出错的地方,这一点我们在调试一些高级编程语言程序的时候会深有体会,它们通常在出问题时会主动把出问题时的调用栈信息打印出来,比如我们在eclipse中调试java程序时。
当这些换到Linux上的C/C++环境时情况将变的稍微复杂一些,通常在这种情况下是通过拿到出问题时产生的core文件然后再利用gdb调试来看到出错时的程序栈信息,这是再好不过的了,但当某些特殊的情况如不正确的系统设置或文件系统出现问题时导致我们没有拿到core文件那我们还有补救的办法吗?本文将介绍在程序中安排当出现崩溃退出时把当前调用栈通过终端打印出来并定位问题的方法。
二、输出程序的调用栈
1、获取程序的调用栈
在Linux上的C/C++编程环境下,我们可以通过如下三个函数来获取程序的调用栈信息。
#include /* Store up to SIZE return address of the current program state inARRAY and return the exact number of values stored. */
int backtrace(void **array, int size);/* Return names of functions from the backtrace list in ARRAY in a newlymalloc()ed memory block. */
char **backtrace_symbols(void *const *array, int size);/* This function is similar to backtrace_symbols() but it writes the resultimmediately to a file. */
void backtrace_symbols_fd(void *const *array, int size, int fd);
它们由GNU C Library提供,关于它们更详细的介绍可参考Linux Programmer’s Manual中关于backtrack相关函数的介绍。
使用它们的时候有一下几点需要我们注意的地方:
- backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后多将不能正确得到程序栈信息;
- backtrace_symbols的实现需要符号名称的支持,在gcc编译过程中需要加入-rdynamic参数;
- 内联函数没有栈帧,它在编译过程中被展开在调用的位置;
- 尾调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。
2、捕获系统异常信号输出调用栈
当程序出现异常时通常伴随着会收到一个由内核发过来的异常信号,如当对内存出现非法访问时将收到段错误信号SIGSEGV,然后才退出。利用这一点,当我们在收到异常信号后将程序的调用栈进行输出,它通常是利用signal()函数,关于系统信号的
三、从backtrace信息分析定位问题
1、测试程序
为了更好的说明和分析问题,我这里将举例一个小程序,它有三个文件组成分别是backtrace.c、dump.c、add.c,其中add.c提供了对一个数值进行加一的方法,我们在它的执行过程中故意使用了一个空指针并为其赋值,这样人为的造成段错误的发生;dump.c中主要用于输出backtrace信息,backtrace.c则包含了我们的man函数,它会先注册段错误信号的处理函数然后去调用add.c提供的接口从而导致发生段错误退出。它们的源程序分别如下:
/** add.c*/
#include
#include
#include 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
#include
#include
#include /* for signal */
#include /* for backtrace() */#define BACKTRACE_SIZE 16void 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
#include
#include
#include /* for signal */
#include /* 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、静态链接情况下的错误信息分析定位
我们首先将用最基本的编译方式将他们编译成一个可执行文件并执行,如下:
char buff[64] = {0x00};sprintf(buff,"cat /proc/%d/maps", getpid());system((const char*) buff);
然后编译执行得到如下结果(打印比较多这里摘取关键部分):
....................................................
7f0962fb3000-7f0962fb4000 r-xp 00000000 08:01 2895572 /home/share/work/backtrace/libadd.so
7f0962fb4000-7f09631b3000 ---p 00001000 08:01 2895572 /home/share/work/backtrace/libadd.so
7f09631b3000-7f09631b4000 r--p 00000000 08:01 2895572 /home/share/work/backtrace/libadd.so
7f09631b4000-7f09631b5000 rw-p 00001000 08:01 2895572 /home/share/work/backtrace/libadd.so
.....................................................
=========>>>catch signal 11 <<<=========
Dump stack start...
backtrace() returned 8 addresses[00] ./backtrace(dump+0x1f) [0x400b7f][01] ./backtrace(signal_handler+0x83) [0x400c99][02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f0962c2b150][03] ./libadd.so(add1+0x1a) [0x7f0962fb35c6][04] ./libadd.so(add+0x1c) [0x7f0962fb35f9][05] ./backtrace(main+0x2f) [0x400b53][06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f0962c1676d][07] ./backtrace() [0x400a69]
Dump stack end...
段错误 (核心已转储)
Maps信息第一项表示的为地址范围如第一条记录中的7f0962fb3000-7f0962fb4000,第二项r-xp分别表示只读、可执行、私有的,由此可知这里存放的为libadd.so的.text段即代码段,后面的栈信息0x7f0962fb35c6也正好是落在了这个区间。所有我们正确的地址应为0x7f0962fb35c6 - 7f0962fb3000 = 0x5c6,将这个地址利用addr2line命令得到如下结果:
zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e libadd.so 0x5c6
/home/share/work/backtrace/add.c:13
可见也得到了正确的出错行号。
接下来我们再用提到的第二种方法即想办法得到函数add的入口地址再上偏移量来得到正确的地址。要得到一个函数的入口地址我们多种途径和方法,比如生成查看程序的map文件;使用gcc的nm、readelif等命令直接对libadd.so分析等。在这里我们只介绍生成查看程序的map文件的方法,其他方法可通过查看gcc手册和google找到。
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段的地址如下:
...................................
.text 0x00000000000005ac 0x55 /tmp/ccCP0hNf.o0x00000000000005ac add10x00000000000005dd add
...................................
由此可知我们的add1的地址为0x5ac,然后加上偏移地址0x1a即0x5ac + 0x1a = 0x5c6,由前面可知这个地址是正确的。
四、最后再说几句
- 通过addr2line命令,我们只需要想办法找出程序出错时的地址我们即可定位错误,这也就是加了调试信息的程序运行地址和源程序有着对应关系(gdb调试时可体会到);
- 通过前面的叙述我们发现不管是定位发生在可执行程序中或动态链接库中的错误我们多可以利用找出符号的入口地址加上偏移量的方法来正确定位出错的地址(注意在C++中为了支持函数重载函数名通常多是做了混淆);
- 以上实验全部是在x86的ubuntu平台下进行的,当转换到嵌入式Linux平台时只需将所有的gcc命令多要使用对应的交叉编译器的gcc命令,通常是在命令前多了个前缀,如arm-none-linux-gnueabi-addr2line,其他命令以此类推;
- 利用程序运行时地址定位源程序位置的思想不管是在调试windows下或其他操作系统下的程序多适用,在MCU下无操作系统的情况下也同样适用,只是会因为平台和编译器的不同所使用的方法和手段会有所不同。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
