文章目录
写程序时,难免碰到程序崩溃的现象,而如何排查这些内存崩溃问题,例如访问空指针、访问非法内存、栈溢出等,如何快速解决该类问题,则是一项比较重要(比较头秃)的技能。
在这里汇总了几个最常见的方法,分别是
1)gdb调试
2)coredump堆栈回溯、
3)signal捕捉挂死信号+backtrace回溯堆栈+ -rdynamic编译参数
4)signal捕捉挂死信号+backtrace回溯堆栈+addr2line获取具体函数地址
#include <iostream>
#include <string.h>
int main(int argc, char **argv)
{
char *str = NULL;
strcpy(str, "hello world");
printf("str is %s\n", str);
return 0;
}
像这个访问空指针,运行则会直接提示Segmentation fault
xzx@ubuntu:~/share/project_ipc/debug_tools/crash$ ./test
Segmentation fault (core dumped)
1.gdb调试
多用于程序开发阶段,能稳定复现的问题,直接挂个gdb再复现一次,看到函数调用栈后,基本就能发现问题原因。因为是在虚拟机环境下调试,所以直接执行gdb便可以,如果是嵌入式环境,需要交叉编译gdb工具.
gcc编译时,其中的-g参数,标识在编译时加入调试信息,在发生问题之后,能在排查时带来更大的帮助,但相应的会增加可执行程序的体积。这里分别对使用-g和不使用-g去进行分类讨论。
1.1 gcc 编译时不带-g
xzx@ubuntu:~/share/project_ipc/debug_tools/crash$ g++ test.cpp -o test
xzx@ubuntu:~/share/project_ipc/debug_tools/crash$ gdb ./test
输入r,让程序跑到崩溃处,使用bt查看堆栈,能看到程序崩溃在main函数中,但没有看到相应崩溃的具体行号和具体行数,对定位崩溃帮助有限
1.2 gcc 编译时带-g
xzx@ubuntu:~/share/project_ipc/debug_tools/crash$ g++ -g test.cpp -o test
xzx@ubuntu:~/share/project_ipc/debug_tools/crash$ gdb ./test
同样在崩溃后,输入bt查看挂死堆栈
这里能看到充分的提示,挂死在crashFunc,在test.cpp的第十三行,常见的问题提示到这份上,基本缕缕代码逻辑都可以解决。
但是这里要注意,使用-g属性,会将更多调试信息收集放到可执行文件中,将会大大增加可执行文件的体积。如下是使用-g和不使用-g的对比,明显大了一倍不止,所以不适用正式释放给测试的版本。
2.coredump栈回溯
当设备已经不在调试阶段,要给到测试验证功能的时候,就没法再用gdb直接进行调试。
这时可以借助coredump文件,在崩溃发生时,生成core文件,崩溃发生后,可以简单把core文件拿来,使用gdb便能看到挂死时的堆栈,快速定位问题。
2.1 使能core文件生成
1)设置core文件最大大小
正常系统上默认未打开core文件生成,可以通过ulimit -c 512设置生成core文件最大大小。
设置后输入ulimit -a 确认,可以看到core file size 为512,注意这里也不能设置的太大,因为太多core文件会挤爆机器的空间,造成其他严重问题!
xzx@ubuntu:~/share/project_ipc/debug_tools/crash$ ulimit -c 512
xzx@ubuntu:~/share/project_ipc/debug_tools/crash$ ulimit -a
core file size (blocks, -c) 512
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 31504
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 1048576
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 31504
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
2)设置core文件生成路径
这里我设置的是自己当前程序的路径,文件格式名为core+程序名+pid+unix时间戳
echo "/home/xzx/share/project_ipc/debug_tools/crash/cor%e-%p-%t" > /proc/sys/kernel/core_pattern
设置好这两个之后,便可以使用。
2.1 借助core文件回溯堆栈
xzx@ubuntu:~/$ ./test
Segmentation fault (core dumped)
xzx@ubuntu:~/$ ls
1.map cortest-99532-1713865380 test test.cpp
xzx@ubuntu:~/$ gdb test cortest-99532-1713865380
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Core was generated by `./test'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000056122f20c7ef in crashFunc () at test.cpp:13
13 strcpy(str, "hello world");
(gdb) bt
#0 0x000056122f20c7ef in crashFunc () at test.cpp:13
#1 0x000056122f20c84e in main (argc=1, argv=0x7ffc431caa38) at test.cpp:27
(gdb)
运行可执行程序后,可以看到生成了cortest-99532-1713865380文件,使用gdb test cortest-99532-1713865380后
结果直接使用gdb看到的一样,能准确定位到挂死函数的具体位置。(这里如果编译时没使用-g选项,则看不到行号)
3.backtrace捕捉SIGSEGV信号
使用coredump文件进行堆栈回溯有一定的风险,如果设置的参数不对,程序反复崩溃的情况下,可能会将机器内的空间全部占用,造成其他严重问题。
可以使用backtrace函数,当发生崩溃时,直接捕捉SIGSEGV/SIGPIPE信号等,在进程退出之前将挂死时的堆栈打印到日志文件中,通常这是最简单高效的方法。(uclibc目前不支持backtrace函数,可以借助libunwind实现)
改写以下demo,加入对崩溃信号的处理,并打印堆栈
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <execinfo.h>
void sigHandleFunc(int signo)
{
printf("sigHandleFunc recv sig:%d", signo);
int j, nptrs;
void *buffer[100];
char **strings;
nptrs = backtrace(buffer, 100);
printf("backtrace() returned %d addresses\n", nptrs);
/* The call backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO)
would produce similar output to the following: */
strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (j = 0; j < nptrs; j++){
printf("%s\n", strings[j]);
}
free(strings);
signal(signo,SIG_DFL);
}
void normalFunc()
{
return ;
}
void crashFunc()
{
char *str = NULL;
strcpy(str, "hello world");
printf("str is %s\n", str);
return ;
}
int main(int argc, char **argv)
{
int i = 0;
signal(SIGSEGV, sigHandleFunc); //segmentation violation
signal(SIGABRT, sigHandleFunc); //abort program (formerly SIGIOT)
signal(SIGPIPE, sigHandleFunc); //floating-point exception
while (1)
{
if (i++ == 10)
{
printf("crashFunc begin\n");
crashFunc();
printf("crashFunc done\n");
}
else
{
printf("normalFunc begin\n");
normalFunc();
printf("normalFunc done\n");
}
}
return 0;
}
运行后,得到的结果为:
alientek@ubuntu16:~/study_linux_project/linux_unix/study_note/crash$ ./test
sigHandleFunc recv sig:11backtrace() returned 6 addresses
./test() [0x4009c1]
/lib/x86_64-linux-gnu/libc.so.6(+0x354c0) [0x7fd462d4c4c0]
./test() [0x400abd]
./test() [0x400b45]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fd462d37840]
./test() [0x4008a9]
Segmentation fault
但是这里只看到了挂在main函数以及之后的偏移地址,但却没看到具体的函数以及行号,这里有两个方法可以解决:
1)编译时添加-rdynamic参数,那么崩溃时能给backtrace提供更多符号信息。但是会增大动态符号表,降低程序运行中动态寻找符号表的效率
这里分别编译了了两种情况下的可执行程序,能看到使用-rdynamic参数编译的可执行程序,.dynsym表明显更大。
g++ -rdynamic test.cpp -o have_rdynamic
g++ test.cpp -o no_rdynamic
与之对应的,添加-rdynamic后的可执行程序挂死时将能看到更多信息,包括详细的函数堆栈,如下:
sigHandleFunc recv sig:11backtrace() returned 6 addresses
./have_rdynamic(_Z13sigHandleFunci+0x4b) [0x400c11]
/lib/x86_64-linux-gnu/libc.so.6(+0x354c0) [0x7f0b14ed34c0]
./have_rdynamic(_Z9crashFuncv+0x1e) [0x400d0d]
./have_rdynamic(main+0x65) [0x400d95]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f0b14ebe840]
./have_rdynamic(_start+0x29) [0x400af9]
Segmentation fault
2)使用addr2line等函数,进行反汇编操作找到函数名,推荐该方法,对程序性能没有影响,这里也是重点介绍这种方法。
这里有一个小技巧,因为使用-g编译参数时,我们能得到更多调试信息,但是这种方式会增大可执行程序的体积,不适合作为release版本,所以我们可以选择在编译时,生成带-g的debug版本,以及不带-g的release版本,将release版本作为正式测试版本,而debug版本的可以保存起来,当发生问题时,可以用addr2line配合debug版本,将崩溃的地址转化为对应的函数名以及行号!
这里我们模拟这种情况,分别编译两个可执行程序,debug和release
g++ test.cpp -o release
g++ -g test.cpp -o debug
假设release版本得到的结果如下:
sigHandleFunc recv sig:11backtrace() returned 6 addresses
./no_rdynamic() [0x4009c1]
/lib/x86_64-linux-gnu/libc.so.6(+0x354c0) [0x7fa3040364c0]
./no_rdynamic() [0x400abd]
./no_rdynamic() [0x400b45]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fa304021840]
./no_rdynamic() [0x4008a9]
Segmentation fault
那么拿到这份信息之后,我们用addr2line+debug版本进行堆栈回溯。
alientek@ubuntu16:~/$ addr2line -e debug 0x4009c1 -f
_Z13sigHandleFunci
/home/alientek/study_linux_project/linux_unix/study_note/crash/test.cpp:17
alientek@ubuntu16:~/$ addr2line -e debug 0x400abd -f
_Z9crashFuncv
/home/alientek/study_linux_project/linux_unix/study_note/crash/test.cpp:45
即避免了调试信息对可执行程序体积、执行效率的影响,又能准确定位挂死堆栈,效果立竿见影!
4.其他工具
其他一些可用的,包括strace工具、内存消毒工具等等,都可以debug,主要还是根据具体情况来选择对应的方法
5.总结
方法名 | 优点 | 缺点 |
---|---|---|
gdb调试 | 简单方便,还能调试其他问题 | 基本仅适用于debug阶段 |
coredump文件+gdb栈回溯 | 可用于release版本 | 需要debug版本挂死才能看到具体行号 使用不当会引入其他严重问题 |
signal捕捉挂死信号+ backtrace回溯堆栈+ rdynamic编译参数 | 不需要-g选项,不增大应用程序体积 | 会增大动态符号表,降低程序运行中动态寻找符号表的效率 |
signal捕捉挂死信号+ backtrace回溯堆栈+ addr2line获取具体函数地址 | 不需要-g选项,不增大应用程序体积 不需要-rdynamic编译参数,不会降低程序运行中动态寻找符号表的效率 可用于release版本 | 没啥缺点其实,唯一就是需要保留一份带-g选项的可执行程序 |
到此为止!另外也有程序崩溃时堆栈已经被破坏的情况,这种等之后单独再总结一篇~