Linux反匯編調試方法
Linux內核模塊或者應用程序經常因為各種各樣的原因而崩潰,一般情況下都會打印函數調用棧信息,那么,這種情況下,我們怎么去定位問題呢?本文檔介紹了一種反匯編的方法輔助定位此類問題。
代碼示例如下:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define PRINT_DEBUG
#define MAX_BACKTRACE_LEVEL 10
#define BACKTRACE_LOG_NAME "backtrace.log"
static void show_reason(int sig, siginfo_t *info, void *secret)
{
void *array[MAX_BACKTRACE_LEVEL];
size_t size;
#ifdef PRINT_DEBUG
char **strings;
size_t i;
size = backtrace(array, MAX_BACKTRACE_LEVEL);
strings = backtrace_symbols(array, size);
printf("Obtain %zd stack frames.\n", size);
for(i = 0; i < size; i++)
printf("%s\n", strings[i]);
free(strings);
#else
int fd = open(BACKSTRACE_LOG_NAME, O_CREAT | O_WRONLY);
size = backtrace(array, MAX_BACKTRACE_LEVEL);
backtrace_symbols_fd(array, size, fd);
close(fd);
#endif
exit(0);
}
void die() {
char *str1;
char *str2;
char *str3;
char *str4 = NULL;
strcpy(str4, "ab");
}
void let_it_die() {
die();
}
int main(int argc, char **argv){
struct sigaction act;
act.sa_sigaction = show_reason;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART | SA_SIGINFO;
sigaction(SIGSEGV, &act, NULL);
sigaction(SIGUSR1, &act, NULL);
sigaction(SIGFPE, &act, NULL);
sigaction(SIGILL, &act, NULL);
sigaction(SIGBUS, &act, NULL);
sigaction(SIGABRT, &act, NULL);
sigaction(SIGSYS, &act, NULL);
let_it_die();
return 0;
}
在該示例中,我們通過自定義的信號處理函數,在程序異常時通過調用backtrace()和backtrace_symbols()函數獲取並打印函數調用棧信息。接下來我們編譯運行該程序.
編譯的時候,添加了-g和–rdynamic選項,主要是添加調試信息。可以看到,運行時出現異常,打印了函數調用棧,棧層數(stack frame)為7層。棧幀信息中,內核在獲取函數調用棧信息時是逐層往上逆推的,所以函數調用的順序是倒序的,即main->let_it_die()->die()。
函數調用棧的每一行顯示的格式是:出問題的代碼所在的可執行文件(符號+相對位移)[加載地址]
以“./backtrace(main+0xf7) [0x80488cd]”這行調用棧信息為例,說明當前的可執行文件是backtrace, 代碼行為main符號所在地址往下偏移0xf7行,正常情況下通過可執行文件反匯編出來的匯編代碼中,main符號所在地址加相對位移等於后面的加載地址。有時候,可能因為版本更新,我們重新編譯代碼生成可執行文件之后,再反匯編分析問題時,因為代碼和出問題時相比較有更新,那么main+0xf7可能就不等於出問題時打印的調用棧的加載地址。通過計算符號加相對位移的值,然后與加載地址比較,可以確認代碼是否與出問題時保持一致。
接下來我們反匯編可執行文件
justin@ubuntu:~/workspace/backtrace$ objdump -dS backtrace > backtrace.asm
接下來我們通過分析反匯編出來的匯編代碼和出問題時的調用棧信息定位問題,首先從最底層調用棧開始,即./backtrace() [0x804873d],在匯編代碼中搜索0x804873d地址符,如下:
可以看到對應着代碼行size = backtrace(array, MAX_BACKTRACE_LEVEL); 通過查看代碼可以知道,該函數是在show_reason函數中被調用的,嘗試着在匯編代碼中找到show_reason符號的地址:
分析代碼,發現show_reason函數是異常發生時的自定義異常處理函數,嘗試着找上一層函數調用棧信息打印的地址0xb7707410,發現不在匯編代碼中,說明是一個外部地址,因為show_reason是程序內部異常時才會被調用的,所以導致程序異常的一定是內部的代碼,所以接下來我們分析下一行調用棧信息,即./backtrace(die+0x18) [0x80487c0]。首先,在匯編代碼中找到die符號,其地址為0x80487a8。
用該地址加載相對位移0x18等於0x80487c0和函數調用棧顯示的加載地址一致。在匯編代碼中找到該地址所在行:
可以看到,出問題的是在strcpy(str4, “ab”);這一行代碼,可以明顯的看到前面定義的str4是空指針,往空指針指向區域復制字符串就會導致空指針異常。
當然,更多情況下,我們會因為找不到出問題時對應的代碼,或者導出來的可執行文件在編譯的時候沒有添加調試選項而無法反匯編出源碼和匯編代碼相對照的反匯編信息。對於前一種情況,加載地址就沒有參考意義了,我們只能通過在反匯編信息中找到符號,通過相對位移找到出錯行,並和現有代碼對照分析問題。對於后者,我們只能定位到出錯行的匯編代碼,然后閱讀匯編代碼片段分析問題的原因。
通常我們使用objdump反匯編分析問題是,還會用到另外兩個特別實用的命令,即nm和addr2line。
nm是用來從可執行文件中導出符號表,其作用和readelf –s或者objdump –T(t)類似
通過nm命令查找的die符號和let_it_die符號的地址和反匯編出來的地址是一致的。
addr2line命令可以通過指定地址從可以執行文件里面打印符號、可執行文件和出錯代碼行:
這里我嘗試着找./backtrace(die+0x18) [0x80487c0]中的加載地址,發現出錯時調用的die函數,出錯行是第80行:
可以很清晰地看到,addr2line定位的正是die函數中調用strcpy所在的行。