在程序调试过程中如果遇到程序崩溃死机的情况下我们通常多是通过出问题时的栈信息来找到出错的地方,这一点我们在调试一些高级编程语言程序的时候会深有体会,它们通常在出问题时会主动把出问题时的调用栈信息打印出来,比如我们在eclipse中调试java程序时。
当这些换到Linux上的C/C++环境时情况将变的稍微复杂一些,通常在这种情况下是通过拿到出问题时产生的core文件然后再利用gdb调试来看到出错时的程序栈信息,这是再好不过的了,但当某些特殊的情况如不正确的系统设置或文件系统出现问题时导致我们没有拿到core文件那我们还有补救的办法吗?
本文将介绍在程序中安排当出现崩溃退出时把当前调用栈通过终端打印出来并定位问题的方法。
在Linux上的C/C++编程环境下,我们可以通过如下三个函数来获取程序的调用栈信息。
#include
/* 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);
使用它们的时候有一下几点需要我们注意的地方:
backtrace
的实现依赖于栈指针(fp
寄存器),在gcc
编译过程中任何非零的优化等级(-On
参数)或加入了栈指针优化参数-fomit-frame-pointer
后多将不能正确得到程序栈信息;backtrace_symbols
的实现需要符号名称的支持,在gcc
编译过程中需要加入-rdynamic
参数;Tail-call Optimization
)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。当程序出现异常时通常伴随着会收到一个由内核发过来的异常信号,如当对内存出现非法访问时将收到段错误信号SIGSEGV
,然后才退出。
利用这一点,当我们在收到异常信号后将程序的调用栈进行输出,它通常是利用signal()
函数,关于系统信号的
为了更好的说明和分析问题,我这里将举例一个小程序,它有三个文件组成分别是backtrace.c
、dump.c
、add.c
,其中add.c
提供了对一个数值进行加一的方法,我们在它的执行过程中故意使用了一个空指针并为其赋值,这样人为的造成段错误的发生;
dump.c
中主要用于输出backtrace
信息,backtrace.c
则包含了我们的man
函数,它会先注册段错误信号的处理函数然后去调用add.c
提供的接口从而导致发生段错误退出。它们的源程序分别如下:
/*
* add.c
*/
#include
#include
#include
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
#include
#include
#include /* for signal */
#include /* 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
#include
#include
#include /* for signal */
#include /* 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;
}
我们首先将用最基本的编译方式将他们编译成一个可执行文件并执行,如下:
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
然而我们通常调试的程序往往没有这么简单,通常会加载用到各种各样的动态链接库。如果错误是发生在动态链接库中那么处理将变得困难一些。下面我们将上述程序中的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
,由前面可知这个地址是正确的。