利用backtrace和ucontex定位segment錯誤【轉】

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

C程序運行時,常常會碰到」segmentfault」錯誤。這是因爲程序中非法訪問內存致使的。當操做系統的內存保護機制發現進程訪問了非法內存的時候會向此進程發送一個SIGSEGV信號,致使進程直接退出,並在shell中提示segment fault。shell

 

         所以,能夠經過設置SIGSEGV信號處理函數,在處理函數中調用backtrace系列函數獲得異常時的函數調用棧信息。segmentfault

 

一: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個地址。this

         backtrace函數返回buffer中保存的地址個數,返回值不會大於size。若是返回值小於size,則說明全部的回溯信息都已經返回了,若是等於size,則有可能被截斷了。spa

 

         backtrace函數在buffer數組中返回的都是一些虛擬地址,不適於分析。backtrace_symbols函數能夠將backtrace返回的buffer中的地址,根據符號表中的信息,轉換爲字符串(函數名+偏移地址)。size參數指明瞭buffer中的地址個數。操作系統

         backtrace_symbols返回字符串數組的首地址,該字符串是在backtrace_symbols中經過malloc分配的,所以,調用者必須使用free釋放內存。若是發生了錯誤,則backtrace_symbols返回NULL。.net

 

         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就是引發異常的指令地址。

相關文章
相關標籤/搜索