一、導讀
在程序調試過程中如果遇到程序崩潰死機的情況下我們通常多是通過出問題時的棧信息來找到出錯的地方,這一點我們在調試一些高級編程語言程序的時候會深有體會,它們通常在出問題時會主動把出問題時的調用棧信息打印出來,比如我們在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提供的接口從而導致發生段錯誤退出。它們的源程序分別如下:
- /*
- * 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;
- }
- /*
- * 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); /* 重新發送信號 */
- }
- /*
- * 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文件也打印出來,加入如下代碼:
- 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下無操作系統的情況下也同樣適用,只是會因爲平臺和編譯器的不同所使用的方法和手段會有所不同。