修訂:2013-02-16
其實還可以使用 glibc 的 backtrace_symbols 函數,把棧幀各傳回位址裡面的數字位址翻譯成符号描述的
修訂:2011-06-11
背景知識:
· 在linux/unix中的信号處理機制,知道signal函數與sigaction的差別
· 段錯誤的概念,CPU中斷處理的步驟,中斷向量表的分類
· 知道CPU Exception分為Fault、trap和abort,了解他們的基本差別
· 段錯誤和浮點錯誤屬于Fault,産生Fault時會将出錯指令的位址入棧,而不是下一條将執行指令的位址
· 在linux/unix裡可以通過調用backstrace來擷取棧幀的資訊
· 文中用到的幾個頭檔案和函數,都屬于glibc,是以不用擔心出現找不到頭檔案和連結錯誤的情況
· addr2line是個系統自帶的小工具,用來轉換編譯出來的位址和源碼行号
背景知識大家可以看書,google,看手冊(建議可以簡單閱讀一下本文列出來的參考資料)…,這裡不想粘貼大量的背景知識,本文主要介紹在 linux / unix 裡面,如何捕獲段錯誤并輸出發生錯誤時的代碼執行路徑,最後還提供了一個封裝好的頭檔案。
OK,下面直奔主題:
——先要抓住段錯誤,别讓它跑了
捕獲段錯誤的方式很簡單,針對段錯誤的信号調用 sigaction 注冊一個處理函數就可以了。
struct sigaction act;
int sig = SIGSEGV;
sigemptyset(&act.sa_mask);
act.sa_sigaction = OnSIGSEGV;
act.sa_flags = SA_SIGINFO;
if(sigaction(sig, &act, NULL)<0)
{
perror("sigaction:");
}
信号處理函數
void OnSIGSEGV(int signum, siginfo_t *info, void *ptr)
{
//TO DO: 輸出堆棧資訊
abort();
}
——接下來,分析出錯時的函數調用路徑
發生段錯誤時的函數調用關系展現在棧幀上,可以通過在信号處理函數中調用 backstrace 來擷取棧幀資訊,backstrace 的具體描述可google之/閱讀頭檔案execinfo.h。修改後的處理函數如下:
void * array[25]; /* 25 層,太夠了 : ),你也可以自己設定個其他值 */
int nSize = backtrace(array, sizeof(array)/sizeof(array[0]));
for (int i=nSize-3; i>=2; i--){ /* 頭尾幾個位址不必輸出,看官要是好奇,輸出來看看就知道了 */
/* 修正array使其指向正在執行的代碼 */[f1]
printf("SIGSEGV catched when running code at %x\n", (char*)array[i] - 1);
}
——進一步定位到出錯的具體位置
要想輸出出錯的具體位置,必須用到信号處理函數的第三個參數,在linux/unix環境下,該指針指向一個ucontext_t結構。這個結構的具體情況,可以通過閱讀頭檔案ucontext.h得知。此結構體裡面包含了發生段錯誤時的寄存器現場,其中就包含EIP寄存器,該寄存器的内容正是段錯誤時的指令位址(因為段錯誤是一種Fault)。
進一步修正後的信号處理函數如下:
void * array[25];
int nSize = backtrace(array, sizeof(array)/sizeof(array[0]));
for (int i=nSize-3; i>2; i--){ /* 頭尾幾個位址不必輸出 */
/* 對array修正一下,使位址指向正在執行的代碼 */
printf("signal[%d] catched when running code at %x\n", signum, (char*)array[i] - 1);
if (NULL != ptr){
ucontext_t* ptrUC = (ucontext_t*)ptr;
int *pgregs = (int*)(&(ptrUC->uc_mcontext.gregs));
int eip = pgregs[REG_EIP];
if (eip != array[i]){ /* 有些處理器會将出錯時的 EIP 也入棧 */
printf("signal[%d] catched when running code at %x\n", signum, (char*)array[i] - 1);
}
printf("signal[%d] catched when running code at %x\n", signum, eip); /* 出錯位址 */
}else{
printf("signal[%d] catched when running code at unknown address\n", signum);
——調用函數的路徑、出錯的位置都輸出了,但是你能看懂輸出麼
好了,現在棧幀裡面的位址和出錯位置的位址都已經以十六進制的形式輸出了,但是這是編譯後的位址,而不是源碼的行号,你能看懂麼?是以還需要借助一個linux/unix自帶的小工具addr2line,将這些列印出來的指令位址轉換為行号、函數名。
執行情況的一個示例:
[root@suse tcpBreak]# ./a.out
signal[11] catched when running code at 804861d
signal[11] catched when running code at 8048578
signal[11] catched when running code at 804855a
[root@ suse tcpBreak]# addr2line 804861d 8048578 804855a -s -C -f -e a.out
main
newsig.cpp:55
oops()
newsig.cpp:32
error(int)
newsig.cpp:27
上面輸出的内容,其具體含義是:
捕獲的信号序号是 11 (SIGSEGV)
執行路徑是第52行--第32行--第27行
調用關系是main--oops--error,在error函數内部,即檔案的第27行發生了段錯誤。
——一點讨論
· 你可能已經閱讀了 execinfo.h,發現其中有一個 backtrace_symbols,想通過調用這個函數來輸出stack frame上面的函數名…你不妨試一下
· 将 backtrace 得到的 array 位址元素減 1 就能得到調用地點麼?的确是這樣的,減 1 不保證位址落到函數調用時跳轉指令的起始處,但可以保證指向了該指令的最後一個位元組,而該指令位址經addr2line轉換後[f2] ,就對應了發生函數調用的行号。
· 可不可以不調用 backstrace 來得到棧幀中的内容?可以的,因為這些内容都在棧裡,你要是明确地知道偏移,就可以得知函數調用棧,但是要費很多心思,而且估計你自己寫的模仿 backstrace 的代碼,可移植性成了問題。
· 通過 gdb 調試 core檔案 不是直接看得到記憶體映像麼,還有必要搞得這麼複雜麼?一般情況下當然不必要,上面所列解決方法的優點在無法正常産生 core 檔案的情況[f3] 下才得以展現。
· 需要在編譯時添加選項 -g 麼?當然需要了,不在可執行檔案中記錄行号資訊,addr2line上哪裡去找行号。否則隻能得到函數名稱,無法得到行号資訊。
——頭疼,想直接用行不行,能來個直接可以用的代碼麼
該頭檔案捕獲了浮點錯誤和段錯誤,像上面示例所說的,在出錯時會向 STDOUT 輸出一系列位址後退出程式,再使用 addr2line 對輸出的位址進行轉換,bingo,調用路徑一目了然展示在你眼前啦!
标注:
參考資料:
中斷與異常
<a href="http://blog.csdn.net/shaohaigod1981/archive/2009/11/04/4767915.aspx" target="_blank">http://blog.csdn.net/shaohaigod1981/archive/2009/11/04/4767915.aspx</a>
<a href="http://hi.baidu.com/hilyjiang/blog/item/cdd7ebb417f8be728bd4b2a1.html" target="_blank">http://hi.baidu.com/hilyjiang/blog/item/cdd7ebb417f8be728bd4b2a1.html</a>
<a href="http://www.logix.cz/michal/doc/i386/chp09-08.htm" target="_blank">http://www.logix.cz/michal/doc/i386/chp09-08.htm</a>
man 7 signal