Linux程序调试优化(2)—— 一次学会嵌入式Linux下程序崩溃定位


写程序时,难免碰到程序崩溃的现象,而如何排查这些内存崩溃问题,例如访问空指针、访问非法内存、栈溢出等,如何快速解决该类问题,则是一项比较重要(比较头秃)的技能。
在这里汇总了几个最常见的方法,分别是
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函数中,但没有看到相应崩溃的具体行号和具体行数,对定位崩溃帮助有限
image.png

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查看挂死堆栈
image.png
这里能看到充分的提示,挂死在crashFunc,在test.cpp的第十三行,常见的问题提示到这份上,基本缕缕代码逻辑都可以解决。
但是这里要注意,使用-g属性,会将更多调试信息收集放到可执行文件中,将会大大增加可执行文件的体积。如下是使用-g和不使用-g的对比,明显大了一倍不止,所以不适用正式释放给测试的版本
image.png

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

image.png
image.png

与之对应的,添加-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选项的可执行程序

到此为止!另外也有程序崩溃时堆栈已经被破坏的情况,这种等之后单独再总结一篇~

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值