Linux内核调试方法总结之反汇编
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所在的行。