在Linux中如何解决程序崩溃的问题

一、背景

在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行,结果符合预期。

相关推荐
Lary_Rock1 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
云飞云共享云桌面3 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮4 小时前
Linux 使用中的问题
linux·运维
hikktn5 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
观音山保我别报错5 小时前
C语言扫雷小游戏
c语言·开发语言·算法
dsywws5 小时前
Linux学习笔记之vim入门
linux·笔记·学习
幺零九零零6 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
小林熬夜学编程8 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法