一、背景
在Linux上的C/C++环境如何调试程序崩溃问题?通常在这种情况,通过拿到出问题时产生的core文件,然后再利用gdb调试来看到出错时的程序栈信息。但某些特殊的情况,如不正确的系统设置或文件系统出现问题时,导致我们没有拿到core文件,那我们还有补救的办法吗?
二、相关说明
1.函数说明
在Linux上的C/C++编程环境下,我们可以通过如下三个函数来获取程序的调用栈信息。它们由GNU C Library提供,关于它们更详细的介绍可参考Linux Programmer's Manual(https://man7.org/linux/man-pages/man3/backtrace.3.html)中关于backtrack相关函数的介绍。
#include <execinfo.h>
int backtrace(void *buffer[.size], int size);
缓冲区(buffer)中将存储一系列活动函数的堆栈帧信息地址,类型为void*。size参数指定可以存储在缓冲区中的最大数量。如果信息数量大于设定的size值,则只会返回最近调用函数的相应信息地址,因此若想获取完整的信息,请确保缓冲区和size足够大。
原则上backtrace()的返回值应该小于size,否则说明size设置不足够,有部分信息被截断。
char **backtrace_symbols(void *const *array, int size);
将backtrace()获取到的 缓冲区(buffer)转换为一个字符串数组。size参数指定缓冲区中的地址数。调用成功,则会返回指向这些字符串的指针;失败,会返回NULL
void backtrace_symbols_fd(void *const buffer[.size], int size, int fd);
backtrace_symbols_fd()和backtrace_symbols()采用相同的缓冲区和大小参数,但它将字符串写入文件描述符fd。
2.注意事项
- backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后多将不能正确得到程序栈信息;
- backtrace_symbols的实现需要符号名称的支持,在gcc编译过程中需要加入-rdynamic参数;
- 内联函数没有栈帧,它在编译过程中被展开在调用的位置;
- 尾调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。
3.捕获系统异常信号
当程序出现异常时,通常伴随着会收到一个由内核发过来的异常信号,如当对内存出现非法访问时将收到段错误信号SIGSEGV,然后才退出。利用这一点,当我们在收到异常信号后将程序的调用栈进行输出,它通常是利用signal()函数。
三、从backtrace信息分析定位问题
1、程序样例
为了更好的说明和分析问题,我这里将举例一个小程序,它有三个文件组成分别是backtrace.c、dump.c、test.c。
- test.c:提供了对空指针的赋值操作,这样人为的造成段错误的发生;
- dump.c:用于输出backtrace信息;
- backtrace.c:程序入口main函数,它会先注册段错误信号的处理函数,然后再调用test()函数来触发段错误。
它们的源程序分别如下:
1.1.test.c文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int test()
{
int *pTemp = NULL;
*pTemp = 0x01; /* 这将导致一个段错误,致使程序崩溃退出 */
return (*pTemp);
}
1.2.dump.c文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <execinfo.h>
void signal_handler(int signo)
{
printf("\n=========>>>catch signal %d <<<=========\n", signo);
void *buffer[BACKTRACE_SIZE];
int nptrs = backtrace(buffer, BACKTRACE_SIZE);
printf("backtrace() returned %d addresses\n", nptrs);
char **strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (int j = 0; j < nptrs; j++){
printf(" [%02d] %s\n", j, strings[j]);
}
free(strings);
signal(signo, SIG_DFL); /* 恢复信号默认处理 */
raise(signo); /* 重新发送信号 */
}
1.3.backtrace.c文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> /* for signal */
extern void signal_handler(int signo);
extern int test();
int main(int argc, char *argv[])
{
signal(SIGSEGV, signal_handler); /* 为SIGSEGV信号安装新的处理函数 */
test();
return 0;
}
最后为了支持bracktrace,编译指令如下
gcc -g -rdynamic backtrace.c test.c dump.c -o backtrace
2.错误分析
为了更清晰的展示分析过程,将使用实际项目的程序进行演示,也可以自行使用上面的样例进行测试。
2.1.静态链接错误分析
<1>首先,在实际使用的代码中增加如下代码,代码位于msacv.c的第453和454行
段错误代码
char *url=ms_null;
strcpy(url, "123");
<2>编译并执行代码,得到如下的backtrace信息:
2024-06-17 16:17:52[I][SIGNAL][mssignal_innerapi_backTrace 65]================Catch a signal(11):Segmentation violation
2024-06-17 16:17:52[I][SIGNAL][mssignal_innerapi_backTrace 72]backtrace() returned 6 addresses
2024-06-17 16:17:52[I][SIGNAL][mssignal_innerapi_backTrace 79][00] /usr/local/msacv/lib/libmscommon.so(+0x59a73) [0x7f97344efa73]
2024-06-17 16:17:52[I][SIGNAL][mssignal_innerapi_backTrace 79][01] /lib/x86_64-linux-gnu/libc.so.6(+0x37970) [0x7f9733491970]
2024-06-17 16:17:52[I][SIGNAL][mssignal_innerapi_backTrace 79][02] ./out_x8664_msacv_gdb() [0x5d00c1]
2024-06-17 16:17:52[I][SIGNAL][mssignal_innerapi_backTrace 79][03] ./out_x8664_msacv_gdb(main+0x1c25) [0x5d23dd]
2024-06-17 16:17:52[I][SIGNAL][mssignal_innerapi_backTrace 79][04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f973347e09b]
2024-06-17 16:17:52[I][SIGNAL][mssignal_innerapi_backTrace 79][05] ./out_x8664_msacv_gdb(_start+0x2a) [0x40fb2a]
查看以上信息,有用的信息为[SIGNAL][mssignal_innerapi_backTrace 79][02] ./out_x8664_msacv_gdb() [0x5d00c1]
<3>使用addr2line命令获取最终信息: addr2line -e out_x8664_msacv_gdb 0x5d00c1
/home/msos/src/main/msacv.c:454
结果:问题发生在msacv.c文件的第454行,结果符合预期
2.2.动态链接错误分析
<1>首先,在实际使用的代码中增加如下代码,代码位于msptc.c的第1144和1145行
段错误代码
char *url=ms_null;
strcpy(url, "123");
<2>编译并执行代码,得到如下的backtrace信息:
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 65]================Catch a signal(11):Segmentation violation
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 72]backtrace() returned 9 addresses
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][00] /usr/local/msacv/lib/libmscommon.so(+0x59a73) [0x7f825a8c9a73]
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][01] /lib/x86_64-linux-gnu/libc.so.6(+0x37970) [0x7f825986b970]
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][02] /usr/local/msacv/lib/libptc.so(msptc_api_register+0x199a) [0x7f8259d05553]
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][03] ./out_x8664_msacv_gdb(register_all+0x13) [0x41c5a9]
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][04] ./out_x8664_msacv_gdb() [0x5ce260]
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][05] ./out_x8664_msacv_gdb() [0x5cff70]
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][06] ./out_x8664_msacv_gdb(main+0x1c25) [0x5d23cb]
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][07] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f825985809b]
2024-06-17 16:29:51[I][SIGNAL][mssignal_innerapi_backTrace 79][08] ./out_x8664_msacv_gdb(_start+0x2a) [0x40fb2a]
查看以上信息,有用的信息为/usr/local/msacv/lib/libptc.so(msptc_api_register+0x199a) [0x7f8259d05553]。
<3>使用addr2line命令获取最终信息: addr2line -e out_x8664_msacv_gdb 0x7f8259d05553
??:0
是不是觉得很莫名其妙?都是个啥。
出现这种情况是由于动态链接库是程序运行时动态加载的,而加载地址每次可能不一样。0x7f8259d05553是一个非常大的地址,也不是一个实际的物理地址,而是经过MMU(内存管理单元)映射过的。
**如何解决?**核心思想是通过map文件将0x7f8259d05553地址转换为实际地址。
2.2.1.通过进程maps文件获取实际地址
<1>在获取backtrace信息时,打印出进程的maps文件信息,如代码:
char buff[64] = {0x00};
sprintf(buff,"cat /proc/%d/maps > /var/log/msacv_signal", getpid());
system((const char*) buff);
<2>由上面的backtrace信息可知道问题发生在libptc.so库中,直接在"/var/log/msacv_signal"文件中搜索libptc.so,就可以得到代码段地址。(如以下地址)
...
7f8259ceb000-7f8259cee000 rw-p 00000000 00:00 0
7f8259cee000-7f8259cf5000 r--p 00000000 103:03 3421573 /usr/local/msacv/lib/libptc.so
7f8259cf5000-7f8259d56000 r-xp 00007000 103:03 3421573 /usr/local/msacv/lib/libptc.so
7f8259d56000-7f8259d73000 r--p 00068000 103:03 3421573 /usr/local/msacv/lib/libptc.so
7f8259d73000-7f8259d74000 ---p 00085000 103:03 3421573 /usr/local/msacv/lib/libptc.so
7f8259d74000-7f8259d78000 r--p 00085000 103:03 3421573 /usr/local/msacv/lib/libptc.so
7f8259d78000-7f8259d7a000 rw-p 00089000 103:03 3421573 /usr/local/msacv/lib/libptc.so
7f8259d7a000-7f8259eb5000 rw-p 00000000 00:00 0
由上面信息可以得到libptc.so库的代码段地址范围为7f8259cee000-7f8259d7a000。而前面得到的0x7f8259d05553也正好在这个区间。我们使用公式"发生问题代码地址"-"代码段起始地址"就可以得到实际的地址。
0x7f8259d05553-0x7f8259cee000=0x17553
<3>使用addr2line命令获取最终信息: addr2line -e /usr/local/msacv/lib/libptc.so 0x17553
/home/msos/extern/src/msptc/src/msptc.c:1145
结果:问题发生在msptc.c文件的第1145行,结果符合预期。
2.2.2.通过add.map文件获取实际地址
<1>编译时,增加-Wl,-Map,add.map选项,如下
<2>Map文件中将包含关于动态库的信息,我们搜索函数名msptc_api_register就可以找到其在.text段的地址为0x15BB9;
<3>结合信息为/usr/local/msacv/lib/libptc.so(msptc_api_register+0x199a) ,将0x15BB9+0x199a=0x17553;
<4>使用addr2line命令获取最终信息: addr2line -e /usr/local/msacv/lib/libptc.so 0x17553
/home/msos/extern/src/msptc/src/msptc.c:1145
结果:问题发生在msptc.c文件的第1145行,结果符合预期。