轉自:https://blog.csdn.net/gqtcgq/article/details/53883546linux
C程序運行時,常常會碰到」segmentfault」錯誤。這是因爲程序中非法訪問內存致使的。當操做系統的內存保護機制發現進程訪問了非法內存的時候會向此進程發送一個SIGSEGV信號,致使進程直接退出,並在shell中提示segment fault。shell
所以,能夠經過設置SIGSEGV信號處理函數,在處理函數中調用backtrace系列函數獲得異常時的函數調用棧信息。segmentfault
一:backtrace數組
backtrace系列函數的原型以下:函數
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連接選項,才能正確解析出符號名。
二:示例
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結構,打印出指令寄存器的內容。
代碼以下:
在使用sigaction函數設置SIGSEGV信號的處理函數時,使用SA_SIGINFO標誌,能夠獲得信號發生時的更多信息。
當信號發生調用處理函數sig_handler時,傳遞給該函數的第三個參數,是一個ucontext_t類型的結構,該結構在頭文件ucontext.h中定義,其中包含了信號發生時的CPU狀態,也就是全部寄存器的內容。
函數getMcontextEip用於返回指令寄存器的內容。使用該內容,替換buffer[1]的內容。代碼運行結果以下:
能夠看見buffer[1]的內容已經被替換成了信號發生時的指令寄存器內容。經過objdump,獲得fun3的彙編指令以下:
地址0x804889f就是引發異常的指令地址。