在Linux中如何利用backtrace信息解决问题

一、导读

在程序调试过程中如果遇到程序崩溃死机的情况下我们通常多是通过出问题时的栈信息来找到出错的地方,这一点我们在调试一些高级编程语言程序的时候会深有体会,它们通常在出问题时会主动把出问题时的调用栈信息打印出来,比如我们在eclipse中调试java程序时。

当这些换到Linux上的C/C++环境时情况将变的稍微复杂一些,通常在这种情况下是通过拿到出问题时产生的core文件然后再利用gdb调试来看到出错时的程序栈信息,这是再好不过的了,但当某些特殊的情况如不正确的系统设置或文件系统出现问题时导致我们没有拿到core文件那我们还有补救的办法吗?本文将介绍在程序中安排当出现崩溃退出时把当前调用栈通过终端打印出来并定位问题的方法。

二、输出程序的调用栈

1、获取程序的调用栈

在Linux上的C/C++编程环境下,我们可以通过如下三个函数来获取程序的调用栈信息。

  1. #include <execinfo.h>
  2. /* Store up to SIZE return address of the current program state in
  3. ARRAY and return the exact number of values stored. */
  4. int backtrace(void **array, int size);
  5. /* Return names of functions from the backtrace list in ARRAY in a newly
  6. malloc()ed memory block. */
  7. char **backtrace_symbols(void *const *array, int size);
  8. /* This function is similar to backtrace_symbols() but it writes the result
  9. immediately to a file. */
  10. 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提供的接口从而导致发生段错误退出。它们的源程序分别如下:

  1. /*
  2. *   add.c
  3. */
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <unistd.h>
  7. int add1(int num)
  8. {
  9. int ret = 0x00;
  10. int *pTemp = NULL;
  11. *pTemp = 0x01;  /* 这将导致一个段错误,致使程序崩溃退出 */
  12. ret = num + *pTemp;
  13. return ret;
  14. }
  15. int add(int num)
  16. {
  17. int ret = 0x00;
  18. ret = add1(num);
  19. return ret;
  20. }
  1. /*
  2. *   dump.c
  3. */
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <unistd.h>
  7. #include <signal.h>       /* for signal */
  8. #include <execinfo.h>     /* for backtrace() */
  9. #define BACKTRACE_SIZE   16
  10. void dump(void)
  11. {
  12. int j, nptrs;
  13. void *buffer[BACKTRACE_SIZE];
  14. char **strings;
  15. nptrs = backtrace(buffer, BACKTRACE_SIZE);
  16. printf("backtrace() returned %d addresses\n", nptrs);
  17. strings = backtrace_symbols(buffer, nptrs);
  18. if (strings == NULL) {
  19. perror("backtrace_symbols");
  20. exit(EXIT_FAILURE);
  21. }
  22. for (j = 0; j < nptrs; j++)
  23. printf("  [%02d] %s\n", j, strings[j]);
  24. free(strings);
  25. }
  26. void signal_handler(int signo)
  27. {
  28. #if 0
  29. char buff[64] = {0x00};
  30. sprintf(buff,"cat /proc/%d/maps", getpid());
  31. system((const char*) buff);
  32. #endif
  33. printf("\n=========>>>catch signal %d <<<=========\n", signo);
  34. printf("Dump stack start...\n");
  35. dump();
  36. printf("Dump stack end...\n");
  37. signal(signo, SIG_DFL); /* 恢复信号默认处理 */
  38. raise(signo);           /* 重新发送信号 */
  39. }
  1. /*
  2. *   backtrace.c
  3. */
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <unistd.h>
  7. #include <signal.h>       /* for signal */
  8. #include <execinfo.h>     /* for backtrace() */
  9. extern void dump(void);
  10. extern void signal_handler(int signo);
  11. extern int add(int num);
  12. int main(int argc, char *argv[])
  13. {
  14. int sum = 0x00;
  15. signal(SIGSEGV, signal_handler);  /* 为SIGSEGV信号安装新的处理函数 */
  16. sum = add(sum);
  17. printf(" sum = %d \n", sum);
  18. return 0x00;
  19. }

2、静态链接情况下的错误信息分析定位

我们首先将用最基本的编译方式将他们编译成一个可执行文件并执行,如下:

  1. zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ gcc -g -rdynamic backtrace.c add.c dump.c -o backtrace
  2. zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ ./backtrace
  3. =========>>>catch signal 11 <<<=========
  4. Dump stack start...
  5. backtrace() returned 8 addresses
  6. [00] ./backtrace(dump+0x1f) [0x400a9b]
  7. [01] ./backtrace(signal_handler+0x31) [0x400b63]
  8. [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f86afc7e150]
  9. [03] ./backtrace(add1+0x1a) [0x400a3e]
  10. [04] ./backtrace(add+0x1c) [0x400a71]
  11. [05] ./backtrace(main+0x2f) [0x400a03]
  12. [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f86afc6976d]
  13. [07] ./backtrace() [0x400919]
  14. Dump stack end...
  15. 段错误 (核心已转储)

由此可见在调用完函数add1后就开始调用段错误信号处理函数了,所以问题是出在函数add1中。这似乎还不够,更准确的位置应该是在地址0x400a3e处,但这到底是哪一行呢,我们使用addr2line命令来得到,执行如下:

  1. zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e backtrace 0x400a3e
  2. /home/share/work/backtrace/add.c:13

2、动态链接情况下的错误信息分析定位

然而我们通常调试的程序往往没有这么简单,通常会加载用到各种各样的动态链接库。如果错误是发生在动态链接库中那么处理将变得困难一些。下面我们将上述程序中的add.c编译成动态链接库libadd.so,然后再编译执行backtrace看会得到什么结果呢。

  1. /* 编译生成libadd.so */
  2. gcc -g -rdynamic add.c -fPIC -shared -o libadd.so
  3.  
  4. /* 编译生成backtrace可执行文件 */
  5. gcc -g -rdynamic backtrace.c dump.c -L. -ladd -Wl,-rpath=. -o backtrace

其中参数 -L. -ladd为编译时链接当前目录的libadd.so;参数-Wl,-rpath=.为指定程序执行时动态链接库搜索路径为当前目录,否则会出现执行找不到libadd.so的错误。然后执行backtrace程序结果如下:

  1. zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ ./backtrace
  2. =========>>>catch signal 11 <<<=========
  3. Dump stack start...
  4. backtrace() returned 8 addresses
  5. [00] ./backtrace(dump+0x1f) [0x400a53]
  6. [01] ./backtrace(signal_handler+0x31) [0x400b1b]
  7. [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f8583672150]
  8. [03] ./libadd.so(add1+0x1a) [0x7f85839fa5c6]
  9. [04] ./libadd.so(add+0x1c) [0x7f85839fa5f9]
  10. [05] ./backtrace(main+0x2f) [0x400a13]
  11. [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f858365d76d]
  12. [07] ./backtrace() [0x400929]
  13. Dump stack end...
  14. 段错误 (核心已转储)

此时我们再用前面的方法将得不到任何信息,如下:

  1. zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e libadd.so 0x7f85839fa5c6
  2. ??:0

这是为什么呢?

出现这种情况是由于动态链接库是程序运行时动态加载的而其加载地址也是每次可能多不一样的,可见0x7f85839fa5c6是一个非常大的地址,和能得到正常信息的地址如0x400a13相差甚远,其也不是一个实际的物理地址(用户空间的程序无法直接访问物理地址),而是经过MMU(内存管理单元)映射过的。

有上面的认识后那我们就只需要得到此次libadd.so的加载地址然后用0x7f85839fa5c6这个地址减去libadd.so的加载地址得到的结果再利用addr2line命令就可以正确的得到出错的地方;另外我们注意到(add1+0x1a)其实也是在描述出错的地方,这里表示的是发生在符号add1偏移0x1a处的地方,也就是说如果我们能得到符号add1也就是函数add1在程序中的入口地址再加上偏移量0x1a也能得到正常的出错地址。

我们先利用第一种方法即试图得到libadd.so的加载地址来解决这个问题。我们可以通过查看进程的maps文件来了解进程的内存使用情况和动态链接库的加载情况,所以我们在打印栈信息前再把进程的maps文件也打印出来,加入如下代码:

  1. char buff[64] = {0x00};
  2. sprintf(buff,"cat /proc/%d/maps", getpid());
  3. system((const char*) buff);

然后编译执行得到如下结果(打印比较多这里摘取关键部分):

  1. ....................................................
  2. 7f0962fb3000-7f0962fb4000 r-xp 00000000 08:01 2895572 /home/share/work/backtrace/libadd.so
  3. 7f0962fb4000-7f09631b3000 ---p 00001000 08:01 2895572 /home/share/work/backtrace/libadd.so
  4. 7f09631b3000-7f09631b4000 r--p 00000000 08:01 2895572 /home/share/work/backtrace/libadd.so
  5. 7f09631b4000-7f09631b5000 rw-p 00001000 08:01 2895572 /home/share/work/backtrace/libadd.so
  6. .....................................................
  7. =========>>>catch signal 11 <<<=========
  8. Dump stack start...
  9. backtrace() returned 8 addresses
  10. [00] ./backtrace(dump+0x1f) [0x400b7f]
  11. [01] ./backtrace(signal_handler+0x83) [0x400c99]
  12. [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f0962c2b150]
  13. [03] ./libadd.so(add1+0x1a) [0x7f0962fb35c6]
  14. [04] ./libadd.so(add+0x1c) [0x7f0962fb35f9]
  15. [05] ./backtrace(main+0x2f) [0x400b53]
  16. [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f0962c1676d]
  17. [07] ./backtrace() [0x400a69]
  18. Dump stack end...
  19. 段错误 (核心已转储)

Maps信息第一项表示的为地址范围如第一条记录中的7f0962fb3000-7f0962fb4000,第二项r-xp分别表示只读、可执行、私有的,由此可知这里存放的为libadd.so的.text段即代码段,后面的栈信息0x7f0962fb35c6也正好是落在了这个区间。所有我们正确的地址应为0x7f0962fb35c6 - 7f0962fb3000 = 0x5c6,将这个地址利用addr2line命令得到如下结果:

  1. zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e libadd.so 0x5c6
  2. /home/share/work/backtrace/add.c:13

可见也得到了正确的出错行号。

接下来我们再用提到的第二种方法即想办法得到函数add的入口地址再上偏移量来得到正确的地址。要得到一个函数的入口地址我们多种途径和方法,比如生成查看程序的map文件;使用gcc的nm、readelif等命令直接对libadd.so分析等。在这里我们只介绍生成查看程序的map文件的方法,其他方法可通过查看gcc手册和google找到。

1)利用gcc编译生成的map文件,用如下命令我们将编译生成libadd.so对应的map文件如下:

  1. gcc -g -rdynamic add.c -fPIC -shared -o libadd.so -Wl,-Map,add.map

Map文件中将包含关于libadd.so的丰富信息,我们搜索函数名add1就可以找到其在.text段的地址如下:

  1. ...................................
  2. .text 0x00000000000005ac 0x55 /tmp/ccCP0hNf.o
  3. 0x00000000000005ac add1
  4. 0x00000000000005dd add
  5. ...................................

由此可知我们的add1的地址为0x5ac,然后加上偏移地址0x1a即0x5ac + 0x1a = 0x5c6,由前面可知这个地址是正确的。

四、最后再说几句

  • 通过addr2line命令,我们只需要想办法找出程序出错时的地址我们即可定位错误,这也就是加了调试信息的程序运行地址和源程序有着对应关系(gdb调试时可体会到);
  • 通过前面的叙述我们发现不管是定位发生在可执行程序中或动态链接库中的错误我们多可以利用找出符号的入口地址加上偏移量的方法来正确定位出错的地址(注意在C++中为了支持函数重载函数名通常多是做了混淆);
  • 以上实验全部是在x86的ubuntu平台下进行的,当转换到嵌入式Linux平台时只需将所有的gcc命令多要使用对应的交叉编译器的gcc命令,通常是在命令前多了个前缀,如arm-none-linux-gnueabi-addr2line,其他命令以此类推;
  • 利用程序运行时地址定位源程序位置的思想不管是在调试windows下或其他操作系统下的程序多适用,在MCU下无操作系统的情况下也同样适用,只是会因为平台和编译器的不同所使用的方法和手段会有所不同。

http://blog.csdn.net/jxgz_leo/article/details/53458366

在Linux中如何利用backtrace信息解决问题的更多相关文章

  1. Linux中查看显卡硬件信息

    Linux中查看显卡硬件信息 https://ywnz.com/linuxjc/67.html lspci -vnn | grep VGA -A 12lshw -C display 查看当前使用的显卡 ...

  2. [linux]netstat命令详解-显示linux中各种网络相关信息

    1.功能与说明 netstat 用于显示linux中各种网络相关信息.如网络链接 路由表  接口状态链接 多播成员等等. 2.参数含义介绍 -a (all)显示所有选项,默认不显示LISTEN相关-t ...

  3. netstat---显示Linux中网络系统的状态信息

    netstat命令用来打印Linux中网络系统的状态信息,可让你得知整个Linux系统的网络情况. 语法 netstat(选项) 选项 -a或--all:显示所有连线中的Socket: -A<网 ...

  4. 在linux中查询硬件相关信息

    1.查询cpu的相关 a.查询CPU的统计信息 使用命令:lscpu 得到的结果如下: Architecture: x86_64 CPU op-mode(s): -bit, -bit Byte Ord ...

  5. 在linux中获取错误返回信息&nbsp;&amp;…

    #include // void perror(const char *msg); #include // char *strerror(int errnum); #include //errno e ...

  6. linux中查找用户账户信息和登录信息的11中方法

    摘自:开源中国 微信公众号 1. id 2. groups 3. finger 4.getent 5. grep 6. lslogins 7..users 8. who 9. w 10. last或者 ...

  7. linux中用户信息及密码相关知识

    在linux中若修改用户信息.密码,组群信息.密码等.其实是在修改/etc/passwd,/etc/shadow,/etc/group,/etc/groupshadow等文件的内容. 这四个文件的意思 ...

  8. linux netstat-查看Linux中网络系统状态信息

    博主推荐:更多网络测试相关命令关注 网络测试  收藏linux命令大全 netstat命令用来打印Linux中网络系统的状态信息,可让你得知整个Linux系统的网络情况. 语法 netstat(选项) ...

  9. Linux中的文件查找技巧

    前言 Linux常用命令中,有些命令可以帮助我们查找二进制文件,帮助手册或源文件的位置,也有的命令可以帮助我们查找磁盘上的任意文件,今天我们就来看看这些命令如何使用. witch witch命令会在P ...

随机推荐

  1. QT5.8.0+MSVC2015安装以及环境配置(不需要安装VS2015)

    原文:QT5.8.0+MSVC2015安装以及环境配置(不需要安装VS2015) 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/snow_rain_ ...

  2. C# Span 入门

    原文:C# Span 入门 版权声明:博客已迁移到 http://lindexi.gitee.io 欢迎访问.如果当前博客图片看不到,请到 http://lindexi.gitee.io 访问博客.本 ...

  3. Python: 文件操作与数据读取

    文件及目录操作 python中对文件.文件夹(文件操作函数)的操作需要涉及到os模块,主要用到的几个函数是, import os 返回指定目录下的所有文件和目录名: os.listdir() 重命名: ...

  4. Windows搭建Eclipse+JDK+SDK的Android --安卓开发入门级

     一 相关下载 (1) java JDK下载: 进入该网页: http://java.sun.com/javase/downloads/index.jsp (或者直接点击下载)例如以下图: 选择 ...

  5. 机器学习、深度学习实战细节(batch norm、relu、dropout 等的相对顺序)

    cost function,一般得到的是一个 scalar-value,标量值: 执行 SGD 时,是最终的 cost function 获得的 scalar-value,关于模型的参数得到的: 1. ...

  6. 讨论IM软件“网上假货’

    概要 网上假货.在不能使用网络的情况下,IM软件还显示在线. 网上是假的"在线--当前离线"之间的状态,在这段时期.用户无法发送消息.用户可以创建假冒网上心跳的错觉(点击了解).缓 ...

  7. 执行xcopy命令后出现Invalid num of parameters错误的解决办法

    作者:朱金灿 来源:http://blog.csdn.net/clever101 在执行一条批处理命令: xcopy /s /i /y C:\ppt D:\Program doc 开始很纳闷,上网一查 ...

  8. handler looper和messageQueue

    一.用法. Looper为了应付新闻周期,在创建过程中初始化MessageQueue. Handler在一个消息到当前线程的其他线程 MessageQueue用于存储所述消息 Looper其中线程创建 ...

  9. 好玩的WPF第三弹:颤抖吧,地球!消失吧,地球!

    原文:好玩的WPF第三弹:颤抖吧,地球!消失吧,地球! 版权声明:转载请联系本人,感谢配合!本站地址:http://blog.csdn.net/nomasp https://blog.csdn.net ...

  10. 首个 C++ 编译器诞生 30 周年了,来听听 C++ 之父畅谈 C++

    原文  http://www.iteye.com/news/31076   C++ 之父 Bjarne Stroustrup 在 cfront 诞生 30 周年的访谈. 整整30年前,CFront 1 ...