转自:https://blog.csdn.net/gqtcgq/article/details/53883546

C程序运行时,经常会碰到”segmentfault”错误。这是由于程序中非法访问内存导致的。当操作系统的内存保护机制发现进程访问了非法内存的时候会向此进程发送一个SIGSEGV信号,导致进程直接退出,并在shell中提示segment fault。

因此,可以通过设置SIGSEGV信号处理函数,在处理函数中调用backtrace系列函数得到异常时的函数调用栈信息。

一:backtrace

backtrace系列函数的原型如下:

  1. #include <execinfo.h>
  2.  
  3. int backtrace(void **buffer, int size);
  4. char **backtrace_symbols(void *const *buffer, int size);
  5. void backtrace_symbols_fd(void *const *buffer, int size, int fd);

backtrace函数通过指针数组buffer返回调用程序的回溯信息,也就是所谓的函数调用栈。buffer数组中的元素是void*类型,也就是栈中保存的返回地址。

size参数指定buffer中可以保存的地址的最大个数。如果实际的回溯信息大于size,则只返回最近的size个地址。

backtrace函数返回buffer中保存的地址个数,返回值不会大于size。如果返回值小于size,则说明所有的回溯信息都已经返回了,如果等于size,则有可能被截断了。

backtrace函数在buffer数组中返回的都是一些虚拟地址,不适于分析。backtrace_symbols函数可以将backtrace返回的buffer中的地址,根据符号表中的信息,转换为字符串(函数名+偏移地址)。size参数指明了buffer中的地址个数。

backtrace_symbols返回字符串数组的首地址,该字符串是在backtrace_symbols中通过malloc分配的,因此,调用者必须使用free释放内存。如果发生了错误,则backtrace_symbols返回NULL。

backtrace_symbols_fd类似于backtrace_symbols,只不过它是把字符串信息写到文件描述符fd所表示的文件中。backtrace_symbols_fd不会调用malloc函数。

注意,编译器的优化策略,可能导致得到的回溯信息不准确。而且,对于GUN编译器而言,必须使用-rdynamic链接选项,才能正确解析出符号名。

二:示例

  1. #include <signal.h>
  2. #include <execinfo.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <ucontext.h>
  6.  
  7. #define BTSIZE 100
  8.  
  9. static void sig_handler(int sig, siginfo_t *info, void *secret)
  10. {
  11. ucontext_t *uc = (ucontext_t*) secret;
  12.  
  13. void *buffer[BTSIZE];
  14. char **strings;
  15. int nptrs = 0;
  16.  
  17. printf("in sig_handler\n");
  18. printf("sig is %d, SIGSEGV is %d\n", sig, SIGSEGV);
  19. printf("info.si_signo is %d, info.si_addr is %p\n",
  20. info->si_signo, info->si_addr);
  21.  
  22. if (sig == SIGSEGV)
  23. {
  24. nptrs = backtrace(buffer, BTSIZE);
  25. printf("backtrace() returned %d addresses\n", nptrs);
  26.  
  27. strings = backtrace_symbols(buffer, nptrs);
  28. if (strings == NULL)
  29. {
  30. perror("backtrace_symbols");
  31. exit(EXIT_FAILURE);
  32. }
  33.  
  34. printf("backtrace: \n");
  35. int j = 0;
  36. for (j = 0; j < nptrs; j++)
  37. {
  38. printf("[%d]%s\n", j, strings[j]);
  39. }
  40. free(strings);
  41.  
  42. exit(0);
  43. }
  44. }
  45.  
  46. void fun3()
  47. {
  48. int *ptr = (int *)0x123;
  49. printf("this is fun3\n");
  50.  
  51. *ptr = 0;
  52. }
  53.  
  54. void fun2()
  55. {
  56. printf("this is fun2\n");
  57. fun3();
  58. }
  59.  
  60. void fun1()
  61. {
  62. printf("this is fun1\n");
  63. fun2();
  64. }
  65.  
  66. int main()
  67. {
  68. struct sigaction act;
  69. sigemptyset(&act.sa_mask);
  70. act.sa_flags = SA_SIGINFO;
  71. act.sa_sigaction = sig_handler;
  72. sigaction(SIGSEGV, &act, NULL);
  73.  
  74. fun1();
  75. }

main函数中,使用sigaction设置SIGSEGV信号的处理函数,通过SA_SIGINFO标志,可以得到信号发生时的额外信息,比如引起信号的内存地址等。

在fun3函数中,尝试将内存地址为0x123的内存赋值为0,这是一个明显的非法内存访问,将导致SIGSEGV信号的产生。

在SIGSEGV信号处理函数sig_handler中,首先打印出引起异常的内存地址info->si_addr,然后调用backtrace和backtrace_symbols打印出栈帧。

结果如下:

[root@localhost test]# gcc -o testbacktrace testbacktrace.c
[root@localhost test]# ./testbacktrace
this is fun1
this is fun2
this is fun3
in sig_handler
sig is 11, SIGSEGV is 11
info.si_signo is 11, info.si_addr is 0x123
backtrace() returned 7 addresses
backtrace:
[0]./testbacktrace [0x80485d0]
[1][0xec8440]
[2]./testbacktrace [0x80486ba]
[3]./testbacktrace [0x80486d3]
[4]./testbacktrace [0x804872e]
[5]/lib/libc.so.6(__libc_start_main+0xdc) [0xa9cedc]
[6]./testbacktrace [0x80484a1]

打印出了info.si_addr的值为0x123。并且打印出了7个地址信息。通过objdump,对testbacktrace进行反汇编,可以得到如下信息:

080483e8 <__libc_start_main@plt>:
80483e8: ff 25 40 9a 04 08 jmp *0x8049a40
80483ee: 68 10 00 00 00 push $0x10
80483f3: e9 c0 ff ff ff jmp 80483b8 <_init+0x18> 08048480 <_start>:
...
8048497: 68 d5 86 04 08 push $0x80486d5
804849c: e8 47 ff ff ff call 80483e8 <__libc_start_main@plt>
80484a1: f4 hlt
... 08048554 <sig_handler>:
...
80485cb: e8 78 fe ff ff call 8048448 <backtrace@plt>
80485d0: 89 45 f8 mov %eax,0xfffffff8(%ebp) 0804867f <fun3>:
...
8048685: c7 45 fc 23 01 00 00 movl $0x123,0xfffffffc(%ebp)
804868c: c7 04 24 b1 88 04 08 movl $0x80488b1,(%esp)
8048693: e8 c0 fd ff ff call 8048458 <puts@plt>
8048698: 8b 45 fc mov 0xfffffffc(%ebp),%eax
804869b: c7 00 00 00 00 00 movl $0x0,(%eax)
80486a1: c9 leave
... 080486a3 <fun2>:
...
80486b0: e8 a3 fd ff ff call 8048458 <puts@plt>
80486b5: e8 c5 ff ff ff call 804867f <fun3>
80486ba: c9 leave
... 080486bc <fun1>:
...
80486c9: e8 8a fd ff ff call 8048458 <puts@plt>
80486ce: e8 d0 ff ff ff call 80486a3 <fun2>
80486d3: c9 leave
... 080486d5 <main>:
...
8048724: e8 ff fc ff ff call 8048428 <sigaction@plt>
8048729: e8 8e ff ff ff call 80486bc <fun1>
804872e: 81 c4 a4 00 00 00 add $0xa4,%esp
...

根据上面的反汇编信息,可知backtrace返回的7个地址信息,都是call指令后面紧跟着的指令地址。这是因为call指令在将子程序的起始地址送入指令寄存器(于是CPU的下一条指令就会转去执行子程序)之前,首先会将call指令的下一条指令的所在地址入栈。所以,函数调用时的栈内容如下:

backtrace返回的buffer中保存的地址,就是所有call指令后续紧跟的返回地址。

上面的结果,因为没有加”-rdynamic”链接选项,所以打印出来的都是虚拟地址。增加”-rdynamic”后的结果如下:

[root@localhost test]# gcc -o testbacktrace testbacktrace.c -rdynamic
[root@localhost test]# ./testbacktrace
this is fun1
this is fun2
this is fun3
in sig_handler
sig is 11, SIGSEGV is 11
info.si_signo is 11, info.si_addr is 0x123
backtrace() returned 7 addresses
backtrace:
[0]./testbacktrace [0x80487b0]
[1][0xda2440]
[2]./testbacktrace(fun2+0x17) [0x804889a]
[3]./testbacktrace(fun1+0x17) [0x80488b3]
[4]./testbacktrace(main+0x59) [0x804890e]
[5]/lib/libc.so.6(__libc_start_main+0xdc) [0x3daedc]
[6]./testbacktrace [0x8048681]

这样可以在不使用objdump的情况下,大体了解函数调用的关系了。

三:指令地址

上面通过backtrace可以大体得到”segmentfault”错误时的函数调用栈,然而仅凭backtrace还是不能得到引起异常的指令地址(甚至连引起异常的函数也无法得到)。

在Redis的源码中,看到了打印指令地址的方法。使用ucontext_t结构,打印出指令寄存器的内容。

代码如下:

  1. #include <signal.h>
  2. #include <execinfo.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <ucontext.h>
  6.  
  7. #define BTSIZE 100
  8.  
  9. static void *getMcontextEip(ucontext_t *uc) {
  10. #if defined(__APPLE__) && !defined(MAC_OS_X_VERSION_10_6)
  11. /* OSX < 10.6 */
  12. #if defined(__x86_64__)
  13. return (void*) uc->uc_mcontext->__ss.__rip;
  14. #elif defined(__i386__)
  15. return (void*) uc->uc_mcontext->__ss.__eip;
  16. #else
  17. return (void*) uc->uc_mcontext->__ss.__srr0;
  18. #endif
  19. #elif defined(__APPLE__) && defined(MAC_OS_X_VERSION_10_6)
  20. /* OSX >= 10.6 */
  21. #if defined(_STRUCT_X86_THREAD_STATE64) && !defined(__i386__)
  22. return (void*) uc->uc_mcontext->__ss.__rip;
  23. #else
  24. return (void*) uc->uc_mcontext->__ss.__eip;
  25. #endif
  26. #elif defined(__linux__)
  27. /* Linux */
  28. #if defined(__i386__)
  29. return (void*) uc->uc_mcontext.gregs[14]; /* Linux 32 */
  30. #elif defined(__X86_64__) || defined(__x86_64__)
  31. return (void*) uc->uc_mcontext.gregs[16]; /* Linux 64 */
  32. #elif defined(__ia64__) /* Linux IA64 */
  33. return (void*) uc->uc_mcontext.sc_ip;
  34. #endif
  35. #else
  36. return NULL;
  37. #endif
  38. }
  39.  
  40. static void sig_handler(int sig, siginfo_t *info, void *secret)
  41. {
  42. ucontext_t *uc = (ucontext_t*) secret;
  43.  
  44. void *buffer[BTSIZE];
  45. char **strings;
  46. int nptrs = 0;
  47.  
  48. printf("in sig_handler\n");
  49. printf("sig is %d, SIGSEGV is %d\n", sig, SIGSEGV);
  50. printf("info.si_signo is %d, info.si_addr is %p\n",
  51. info->si_signo, info->si_addr);
  52.  
  53. if (sig == SIGSEGV)
  54. {
  55. nptrs = backtrace(buffer, BTSIZE);
  56. printf("backtrace() returned %d addresses\n", nptrs);
  57.  
  58. if (getMcontextEip(uc) != NULL)
  59. buffer[1] = getMcontextEip(uc);
  60.  
  61. strings = backtrace_symbols(buffer, nptrs);
  62. if (strings == NULL) {
  63. perror("backtrace_symbols");
  64. exit(EXIT_FAILURE);
  65. }
  66.  
  67. printf("backtrace: \n");
  68. int j;
  69. for (j = 0; j < nptrs; j++)
  70. {
  71. printf("[%d]%s\n", j, strings[j]);
  72. }
  73. free(strings);
  74.  
  75. exit(0);
  76. }
  77. }
  78.  
  79. void fun3()
  80. {
  81. int *ptr = (int *)0x123;
  82. printf("this is fun3\n");
  83.  
  84. *ptr = 0;
  85. }
  86.  
  87. void fun2()
  88. {
  89. printf("this is fun2\n");
  90. fun3();
  91. }
  92.  
  93. void fun1()
  94. {
  95. printf("this is fun1\n");
  96. fun2();
  97. }
  98.  
  99. int main()
  100. {
  101. struct sigaction act;
  102. sigemptyset(&act.sa_mask);
  103. act.sa_flags = SA_SIGINFO;
  104. act.sa_sigaction = sig_handler;
  105. sigaction(SIGSEGV, &act, NULL);
  106.  
  107. fun1();
  108. }

在使用sigaction函数设置SIGSEGV信号的处理函数时,使用SA_SIGINFO标志,可以得到信号发生时的更多信息。

当信号发生调用处理函数sig_handler时,传递给该函数的第三个参数,是一个ucontext_t类型的结构,该结构在头文件ucontext.h中定义,其中包含了信号发生时的CPU状态,也就是所有寄存器的内容。

函数getMcontextEip用于返回指令寄存器的内容。使用该内容,替换buffer[1]的内容。代码运行结果如下:

  1. [root@localhost test]# gcc -o testbacktrace testbacktrace.c -rdynamic
  2. [root@localhost test]# ./testbacktrace
  3. this is fun1
  4. this is fun2
  5. this is fun3
  6. in sig_handler
  7. sig is 11, SIGSEGV is 11
  8. info.si_signo is 11, info.si_addr is 0x123
  9. backtrace() returned 7 addresses
  10. backtrace:
  11. [0]./testbacktrace [0x80487bb]
  12. [1]./testbacktrace(fun3+0x1c) [0x804889f]
  13. [2]./testbacktrace(fun2+0x17) [0x80488be]
  14. [3]./testbacktrace(fun1+0x17) [0x80488d7]
  15. [4]./testbacktrace(main+0x59) [0x8048932]
  16. [5]/lib/libc.so.6(__libc_start_main+0xdc) [0xd6dedc]
  17. [6]./testbacktrace [0x8048681]

可以看见buffer[1]的内容已经被替换成了信号发生时的指令寄存器内容。通过objdump,得到fun3的汇编指令如下:

  1. 08048883 <fun3>:
  2. 8048883: 55 push %ebp
  3. 8048884: 89 e5 mov %esp,%ebp
  4. 8048886: 83 ec 18 sub $0x18,%esp
  5. 8048889: c7 45 fc 23 01 00 00 movl $0x123,0xfffffffc(%ebp)
  6. 8048890: c7 04 24 b1 8a 04 08 movl $0x8048ab1,(%esp)
  7. 8048897: e8 98 fd ff ff call 8048634 <puts@plt>
  8. 804889c: 8b 45 fc mov 0xfffffffc(%ebp),%eax
  9. 804889f: c7 00 00 00 00 00 movl $0x0,(%eax)
  10. 80488a5: c9 leave
  11. 80488a6: c3 ret

地址0x804889f就是引起异常的指令地址。

利用backtrace和ucontex定位segment错误【转】的更多相关文章

  1. 利用backtrace和ucontex定位segment错误

    C程序运行时,经常会碰到"segmentfault"错误.这是由于程序中非法访问内存导致的.当操作系统的内存保护机制发现进程访问了非法内存的时候会向此进程发送一个SIGSEGV信号 ...

  2. 嵌入式 linux下利用backtrace追踪函数调用堆栈以及定位段错误

    嵌入式 linux下利用backtrace追踪函数调用堆栈以及定位段错误 2015-05-27 14:19 184人阅读 评论(0) 收藏 举报  分类: 嵌入式(928)  一般察看函数运行时堆栈的 ...

  3. linux下利用backtrace追踪函数调用堆栈以及定位段错误

    一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的. 在glibc ...

  4. Linux下利用backtrace追踪函数调用堆栈以及定位段错误[转]

    来源:Linux社区  作者:astrotycoon 一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序 ...

  5. 用户态使用 glibc/backtrace 追踪函数调用堆栈定位段错误【转】

    转自:https://blog.csdn.net/gatieme/article/details/84189280 版权声明:本文为博主原创文章 && 转载请著名出处 @ http:/ ...

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

    在Linux中如何利用backtrace信息解决问题 一.导读 在程序调试过程中如果遇到程序崩溃死机的情况下我们通常多是通过出问题时的栈信息来找到出错的地方,这一点我们在调试一些高级编程语言程序的时候 ...

  7. 利用backtrace和objdump进行分析挂掉的程序

    转自:http://blog.csdn.net/hanchaoman/article/details/5583457 汇编不懂,先把方法记下来. glibc为我们提供了此类能够dump栈内容的函数簇, ...

  8. html利用锚点实现定位代码实例

    本章节介绍介绍一下如何利用锚点实现定位,使用锚点实现定位是html固有的功能,当然比较简单,也实现了基本的功能,但是功能相对简单一些,如果想要实现平滑的定位可以参阅jquery实现的点击页面动画方式平 ...

  9. [置顶] 利用Global.asax的Application_Error实现错误记录,错误日志

    利用Global.asax的Application_Error实现错误记录 错误日志 void Application_Error(object sender, EventArgs e) { // 在 ...

随机推荐

  1. Beta版本总结

    beta 阶段的 postmortem 报告 1. 每个成员在beta 阶段的实践和alpha 阶段有何改进? 成员  Beta阶段的实践和alpha 阶段有何改进  黄山成 beta阶段较alpha ...

  2. CMake系列之一:概念

    不同的make工具遵循不同的规范和标准,因此针对不同的标准需要不同的Makefile文件.CMake利用一种平台无关的CMakeList.txt文件定制编译流程,根据目标用户的平台生成本地化的Make ...

  3. 【python】自学笔记

    参考文献 1.环境安装 1.1 python 工作环境 2.7.14 1.2 pycharm community2018.1.1 4 x64 2.第一行代码 2.1 python交互模式, >& ...

  4. Fantacy团队第一次站立会议

    1.队名解释 首先队名Fantacy,并没有任何含义,想取幻想(Fantasy)之名,却并非幻想一词,因为我们组的基础并不好,幻想需要有了坚实的基础才能实现,没有基础等于空想.所以我们组的目的是,提升 ...

  5. Python模块-pymssql

    目录 工作原理 常用封装 Python默认的数据库是 SQLlite,不过它对MySql以及SQL server的支持也可以.如果想链接操作SQL server,需使用第三方包pymssql pyms ...

  6. Spring注入的不同方式

    1.直接创建一个Bean <bean id="dboperate" class="study.spring2.Test"></bean> ...

  7. 本地安装apk后直接打开,按下Home键再重新打开,然后按下返回键时页面展示错误的处理方法

    情景: 1.下载apk到手机本地,点击本地apk开始安装 2.安装完成后,一般会有 “完成” 和 “打开” 两个按钮,点击 “完成” 按钮时是没有问题的,不管它 3.点击 “打开” 按钮,进入到首页( ...

  8. 伪数组(arguments及字符串)转数组的方法 贼简单

    超简单的伪数组转数组的方法, 简单到令人发指! (这里伪数组使用arguments) 1.使用 Array.prototype.slice Array.prototype.slice.call(arg ...

  9. LOJ2542 PKUWC2018随机游走(概率期望+容斥原理)

    如果直接dp,状态里肯定要带上已走过的点的集合,感觉上不太好做. 考虑一种对期望的minmax容斥:其中Max(S)为遍历完S集合的期望步数,Min(S)为遍历到S集合中一个点的期望步数.当然才不管怎 ...

  10. 使用Ubuntu的Crontab定时任务需要注意的地方

    Ubuntu使用crontab定时任务  网上有很多教程,现在记录下我遇到的一些问题,需要注意的地方: 1.定时任务的日志存放路径 网上的说法:cron的日志存放在 /var/log/cron 里面 ...