在Linux中如何利用backtrace資訊解決程式崩潰的問題
一、導讀
在程式除錯過程中如果遇到程式崩潰宕機的情況下我們通常多是通過出問題時的棧資訊來找到出錯的地方,這一點我們在除錯一些高階程式語言程式的時候會深有體會,它們通常在出問題時會主動把出問題時的呼叫棧資訊打印出來,比如我們在eclipse中除錯java程式時。
當這些換到Linux上的C/C++環境時情況將變的稍微複雜一些,通常在這種情況下是通過拿到出問題時產生的core檔案然後再利用gdb除錯來看到出錯時的程式棧資訊,這是再好不過的了,但當某些特殊的情況如不正確的系統設定或檔案系統出現問題時導致我們沒有拿到core檔案那我們還有補救的辦法嗎?本文將介紹在程式中安排當出現崩潰退出時把當前呼叫棧通過終端打印出來並定位問題的方法。
二、輸出程式的呼叫棧
1、獲取程式的呼叫棧
在Linux上的C/C++程式設計環境下,我們可以通過如下三個函式來獲取程式的呼叫棧資訊。
-
#include <execinfo.h>
-
/* Store up to SIZE return address of the current program state in
-
ARRAY and return the exact number of values stored. */
-
int backtrace(void **array, int size);
-
/* Return names of functions from the backtrace list in ARRAY in a newly
-
malloc()ed memory block. */
-
char **backtrace_symbols(void *const *array, int size);
-
/* This function is similar to backtrace_symbols() but it writes the result
-
immediately to a file. */
-
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提供的介面從而導致發生段錯誤退出。它們的源程式分別如下:
[cpp] view plain copy
- /*
- * add.c
- */
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- int add1(int num)
- {
- int ret = 0x00;
- int *pTemp = NULL;
- *pTemp = 0x01; /* 這將導致一個段錯誤,致使程式崩潰退出 */
- ret = num + *pTemp;
- return ret;
- }
- int add(int num)
- {
- int ret = 0x00;
- ret = add1(num);
- return ret;
- }
[cpp] view plain copy
- /*
- * dump.c
- */
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <signal.h> /* for signal */
- #include <execinfo.h> /* for backtrace() */
- #define BACKTRACE_SIZE 16
- void dump(void)
- {
- int j, nptrs;
- void *buffer[BACKTRACE_SIZE];
- char **strings;
- nptrs = backtrace(buffer, BACKTRACE_SIZE);
- printf("backtrace() returned %d addresses\n", nptrs);
- strings = backtrace_symbols(buffer, nptrs);
- if (strings == NULL) {
- perror("backtrace_symbols");
- exit(EXIT_FAILURE);
- }
- for (j = 0; j < nptrs; j++)
- printf(" [%02d] %s\n", j, strings[j]);
- free(strings);
- }
- void signal_handler(int signo)
- {
- #if 0
- char buff[64] = {0x00};
- sprintf(buff,"cat /proc/%d/maps", getpid());
- system((const char*) buff);
- #endif
- printf("\n=========>>>catch signal %d <<<=========\n", signo);
- printf("Dump stack start...\n");
- dump();
- printf("Dump stack end...\n");
- signal(signo, SIG_DFL); /* 恢復訊號預設處理 */
- raise(signo); /* 重新發送訊號 */
- }
[cpp] view plain copy
- /*
- * backtrace.c
- */
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <signal.h> /* for signal */
- #include <execinfo.h> /* for backtrace() */
- extern void dump(void);
- extern void signal_handler(int signo);
- extern int add(int num);
- int main(int argc, char *argv[])
- {
- int sum = 0x00;
- signal(SIGSEGV, signal_handler); /* 為SIGSEGV訊號安裝新的處理函式 */
- sum = add(sum);
- printf(" sum = %d \n", sum);
- return 0x00;
- }
2、靜態連結情況下的錯誤資訊分析定位
我們首先將用最基本的編譯方式將他們編譯成一個可執行檔案並執行,如下:
zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ gcc -g -rdynamic backtrace.c add.c dump.c -o backtrace zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ ./backtrace =========>>>catch signal 11 <<<========= Dump stack start... backtrace() returned 8 addresses [00] ./backtrace(dump+0x1f) [0x400a9b] [01] ./backtrace(signal_handler+0x31) [0x400b63] [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f86afc7e150] [03] ./backtrace(add1+0x1a) [0x400a3e] [04] ./backtrace(add+0x1c) [0x400a71] [05] ./backtrace(main+0x2f) [0x400a03] [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f86afc6976d] [07] ./backtrace() [0x400919] Dump stack end... 段錯誤 (核心已轉儲)
由此可見在呼叫完函式add1後就開始呼叫段錯誤訊號處理函數了,所以問題是出在函式add1中。這似乎還不夠,更準確的位置應該是在地址0x400a3e處,但這到底是哪一行呢,我們使用addr2line命令來得到,執行如下:
zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e backtrace 0x400a3e /home/share/work/backtrace/add.c:13
2、動態連結情況下的錯誤資訊分析定位
然而我們通常除錯的程式往往沒有這麼簡單,通常會載入用到各種各樣的動態連結庫。如果錯誤是發生在動態連結庫中那麼處理將變得困難一些。下面我們將上述程式中的add.c編譯成動態連結庫libadd.so,然後再編譯執行backtrace看會得到什麼結果呢。
/* 編譯生成libadd.so */ gcc -g -rdynamic add.c -fPIC -shared -o libadd.so /* 編譯生成backtrace可執行檔案 */ gcc -g -rdynamic backtrace.c dump.c -L. -ladd -Wl,-rpath=. -o backtrace
其中引數 -L. -ladd為編譯時連結當前目錄的libadd.so;引數-Wl,-rpath=.為指定程式執行時動態連結庫搜尋路徑為當前目錄,否則會出現執行找不到libadd.so的錯誤。然後執行backtrace程式結果如下:
zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ ./backtrace =========>>>catch signal 11 <<<========= Dump stack start... backtrace() returned 8 addresses [00] ./backtrace(dump+0x1f) [0x400a53] [01] ./backtrace(signal_handler+0x31) [0x400b1b] [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f8583672150] [03] ./libadd.so(add1+0x1a) [0x7f85839fa5c6] [04] ./libadd.so(add+0x1c) [0x7f85839fa5f9] [05] ./backtrace(main+0x2f) [0x400a13] [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f858365d76d] [07] ./backtrace() [0x400929] Dump stack end... 段錯誤 (核心已轉儲)
此時我們再用前面的方法將得不到任何資訊,如下:
zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e libadd.so 0x7f85839fa5c6 ??:0
這是為什麼呢?
出現這種情況是由於動態連結庫是程式執行時動態載入的而其載入地址也是每次可能多不一樣的,可見0x7f85839fa5c6是一個非常大的地址,和能得到正常資訊的地址如0x400a13相差甚遠,其也不是一個實際的實體地址(使用者空間的程式無法直接訪問實體地址),而是經過MMU(記憶體管理單元)對映過的。
有上面的認識後那我們就只需要得到此次libadd.so的載入地址然後用0x7f85839fa5c6這個地址減去libadd.so的載入地址得到的結果再利用addr2line命令就可以正確的得到出錯的地方;另外我們注意到(add1+0x1a)其實也是在描述出錯的地方,這裡表示的是發生在符號add1偏移0x1a處的地方,也就是說如果我們能得到符號add1也就是函式add1在程式中的入口地址再加上偏移量0x1a也能得到正常的出錯地址。
我們先利用第一種方法即試圖得到libadd.so的載入地址來解決這個問題。我們可以通過檢視程序的maps檔案來了解程序的記憶體使用情況和動態連結庫的載入情況,所以我們在列印棧資訊前再把程序的maps檔案也打印出來,加入如下程式碼:
[cpp] view plain copy
- char buff[64] = {0x00};
- sprintf(buff,"cat /proc/%d/maps", getpid());
- system((const char*) buff);
然後編譯執行得到如下結果(列印比較多這裡摘取關鍵部分):
.................................................... 7f0962fb3000-7f0962fb4000 r-xp 00000000 08:01 2895572 /home/share/work/backtrace/libadd.so 7f0962fb4000-7f09631b3000 ---p 00001000 08:01 2895572 /home/share/work/backtrace/libadd.so 7f09631b3000-7f09631b4000 r--p 00000000 08:01 2895572 /home/share/work/backtrace/libadd.so 7f09631b4000-7f09631b5000 rw-p 00001000 08:01 2895572 /home/share/work/backtrace/libadd.so ..................................................... =========>>>catch signal 11 <<<========= Dump stack start... backtrace() returned 8 addresses [00] ./backtrace(dump+0x1f) [0x400b7f] [01] ./backtrace(signal_handler+0x83) [0x400c99] [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f0962c2b150] [03] ./libadd.so(add1+0x1a) [0x7f0962fb35c6] [04] ./libadd.so(add+0x1c) [0x7f0962fb35f9] [05] ./backtrace(main+0x2f) [0x400b53] [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f0962c1676d] [07] ./backtrace() [0x400a69] Dump stack end... 段錯誤 (核心已轉儲)
Maps資訊第一項表示的為地址範圍如第一條記錄中的7f0962fb3000-7f0962fb4000,第二項r-xp分別表示只讀、可執行、私有的,由此可知這裡存放的為libadd.so的.text段即程式碼段,後面的棧資訊0x7f0962fb35c6也正好是落在了這個區間。所有我們正確的地址應為0x7f0962fb35c6 - 7f0962fb3000 = 0x5c6,將這個地址利用addr2line命令得到如下結果:
zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e libadd.so 0x5c6 /home/share/work/backtrace/add.c:13
可見也得到了正確的出錯行號。
接下來我們再用提到的第二種方法即想辦法得到函式add的入口地址再上偏移量來得到正確的地址。要得到一個函式的入口地址我們多種途徑和方法,比如生成檢視程式的map檔案;使用gcc的nm、readelif等命令直接對libadd.so分析等。在這裡我們只介紹生成檢視程式的map檔案的方法,其他方法可通過檢視gcc手冊和google找到。
1)利用gcc編譯生成的map檔案,用如下命令我們將編譯生成libadd.so對應的map檔案如下:
gcc -g -rdynamic add.c -fPIC -shared -o libadd.so -Wl,-Map,add.map
Map檔案中將包含關於libadd.so的豐富資訊,我們搜尋函式名add1就可以找到其在.text段的地址如下:
................................... .text 0x00000000000005ac 0x55 /tmp/ccCP0hNf.o 0x00000000000005ac add1 0x00000000000005dd add ...................................
由此可知我們的add1的地址為0x5ac,然後加上偏移地址0x1a即0x5ac + 0x1a = 0x5c6,由前面可知這個地址是正確的。
四、最後再說幾句
- 通過addr2line命令,我們只需要想辦法找出程式出錯時的地址我們即可定位錯誤,這也就是加了除錯資訊的程式執行地址和源程式有著對應關係(gdb除錯時可體會到);
- 通過前面的敘述我們發現不管是定位發生在可執行程式中或動態連結庫中的錯誤我們多可以利用找出符號的入口地址加上偏移量的方法來正確定位出錯的地址(注意在C++中為了支援函式過載函式名通常多是做了混淆);
- 以上實驗全部是在x86的ubuntu平臺下進行的,當轉換到嵌入式Linux平臺時只需將所有的gcc命令多要使用對應的交叉編譯器的gcc命令,通常是在命令前多了個字首,如arm-none-linux-gnueabi-addr2line,其他命令以此類推;
- 利用程式執行時地址定位源程式位置的思想不管是在除錯windows下或其他作業系統下的程式多適用,在MCU下無作業系統的情況下也同樣適用,只是會因為平臺和編譯器的不同所使用的方法和手段會