C语言--backtrace调用栈信息解决程序崩溃问题总结

1、背景介绍
        在程序调试过程中如果遇到程序崩溃死机的情况下我们通常多是通过出问题时的栈信息来找到出错的地方,这一点我们在调试一些高级编程语言程序的时候会深有体会,它们通常在出问题时会主动把出问题时的调用栈信息打印出来,比如我们在eclipse中调试java程序时。
       当这些换到Linux上的C/C++环境时情况将变的稍微复杂一些,通常在这种情况下是通过拿到出问题时产生的core文件然后再利用gdb调试来看到出错时的程序栈信息,这是再好不过的了,但当某些特殊的情况如不正确的系统设置或文件系统出现问题时导致我们没有拿到core文件那我们还有补救的办法吗?本文将介绍在程序中安排当出现崩溃退出时把当前调用栈通过终端打印出来并定位问题的方法。

2、输出程序的调用栈
2.1 获取程序的调用栈
        在Linux上的C/C++编程环境下,我们可以通过如下三个函数来获取程序的调用栈信息。

#include <execinfo.h>
 
/* 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);

       它们由GNU C Library提供,关于它们更详细的介绍可参考Linux Programmer’s Manual中关于backtrace相关函数的介绍。
    使用它们的时候有一下几点需要我们注意的地方:

  • backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后多将不能正确得到程序栈信息;
  • backtrace_symbols的实现需要符号名称的支持,在gcc编译过程中需要加入-rdynamic参数;
  • 内联函数没有栈帧,它在编译过程中被展开在调用的位置;
  • 尾调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。

2.2 捕获系统异常信号输出调用栈
       当程序出现异常时通常伴随着会收到一个由内核发过来的异常信号,如当对内存出现非法访问时将收到段错误信号SIGSEGV,然后才退出。利用这一点,当我们在收到异常信号后将程序的调用栈进行输出,它通常是利用signal()函数,关于系统信号的总结可以参考我的另外一篇文章:
https://zhouyuming.blog.csdn.net/article/details/114437653

3、从backtrace信息分析定位问题
3.1 测试程序

        为了更好的说明和分析问题,我这里将举例一个小程序,它有三个文件组成分别是backtrace.c、dump.c、add.c,其中add.c提供了对一个数值进行加一的方法,我们在它的执行过程中故意使用了一个空指针并为其赋值,这样人为的造成段错误的发生;dump.c中主要用于输出backtrace信息,backtrace.c则包含了我们的main函数,它会先注册段错误信号的处理函数然后去调用add.c提供的接口从而导致发生段错误退出。它们的源程序分别如下:

/* 
 *   add.c 
 */  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
  
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 <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <signal.h>       /* for signal */  
#include <execinfo.h>     /* 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 <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <signal.h>       /* for signal */  
#include <execinfo.h>     /* 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;  
} 

3.2 静态链接情况下的错误信息分析定位

       我们首先将用最基本的编译方式将他们编译成一个可执行文件并执行,如下:

gcc -g -rdynamic backtrace.c add.c dump.c -o 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命令来得到,执行如下:

addr2line -e backtrace 0x400a3e

/home/share/work/backtrace/add.c:13

3.3 动态链接情况下的错误信息分析定位
       然而我们通常调试的程序往往没有这么简单,通常会加载用到各种各样的动态链接库。如果错误是发生在动态链接库中那么处理将变得困难一些。下面我们将上述程序中的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程序结果如下:

./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...
段错误 (核心已转储)

此时我们再用前面的方法将得不到任何信息,如下:

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命令得到如下结果:

addr2line -e libadd.so 0x5c6
/home/share/work/backtrace/add.c:13

可见也得到了正确的出错行号。

        接下来我们再用提到的第二种方法即想办法得到函数add的入口地址再上偏移量来得到正确的地址。要得到一个函数的入口地址我们多种途径和方法,比如生成查看程序的map文件;使用gcc的nm、readelif等命令直接对libadd.so分析等。在这里我们只介绍生成查看程序的map文件的方法,其他方法可通过查看gcc手册和google找到。
      利用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,由前面可知这个地址是正确的。

#/bin/bash
# assembler2c.sh
#帮助函数
function help
{
    echo ""
    echo $1
    echo ""
    echo "Usage: $0 libXXX.dbg FunctionName Offset"
    echo ""
    echo "          libXXX.dbg    --  The dbg file of the exception dymanic library."
    echo "          FunctionName  --  Exception function name in callstack."
    echo "          Offset        --  Exception instruction offset in exception function. It must be start with 0x."
    echo ""
}

#检查参数
if [ $# != 3 ]; then
    help "Error : Argu must be 3!"
    exit 1
fi

#检查文件是否存在
if [ ! -f $1 ]; then
    help "File $1 not Found!"
    exit 1
fi

#检查文件格式是否是ELF格式
type=`file $1 | grep ELF | wc -l`
if [ $type == 0 ]; then
    help "$1 :file type is not ELF!"
    exit 1
fi

#检查函数是否包含在dbg文件中
containflag=`objdump -t $1 | grep " $2$" | wc -l`
if [ $containflag == 0 ]; then
    help "$1 :Function not in this dbg file!"
    exit 1
fi

#获取函数基地址
baseaddr=`objdump -t $1 | grep " $2$" | grep .text | awk {'print $1'}`

#获取指令最终地址
destaddr=`printf "%x" $[0x$baseaddr+$3]`
if [ $? != 0 ]; then
    help "please check argu!"
    exit 1
fi

#打印输出
addr2line -e $1 0x$destaddr
echo ""

使用方法:
sh assembler2c.sh libxx.dbg function_name offset_of_function

1、根据程序异常栈查找出错代码
2、gdb *.so

3、file *.gdb

4、l *(func_name+0xxxx) 0xxxx为接口func_name的偏移地址

objdump相关符号表查找:

objdump -t xxx.dbg | grep xxx

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux 下,程序崩溃后可以通过调用信息来帮助我们定位问题。我们可以使用一些工具和技术来获取程序崩溃时的调用信息。 1. 使用 GDB 调试器 GDB 是一个功能强大的调试器,在程序崩溃时可以用它来获取调用信息。我们可以在终端中使用以下命令来使用 GDB 调试器: ``` gdb <程序名> ``` 进入 GDB 调试器后,可以使用 `bt` 命令来打印当前的调用信息。 2. 使用 Core Dump 文件 Core Dump 文件是程序崩溃时产生的一种文件,它可以包含程序崩溃时的状态信息,包括调用信息。我们可以使用一些工具来分析 Core Dump 文件,比如 GDB 或者 Valgrind 工具。 在 Linux 中,可以使用以下命令来开启 Core Dump 文件的生成: ``` ulimit -c unlimited ``` 这个命令会将 Core Dump 文件的大小设置为无限制,这样当程序崩溃时就会生成 Core Dump 文件。 3. 使用 backtrace() 函数 在程序崩溃时,我们可以使用 backtrace() 函数来获取当前的调用信息。这个函数定义在 execinfo.h 头文件中,使用时需要链接 libexecinfo 库。 backtrace() 函数的原型如下: ```c int backtrace(void **buffer, int size); ``` 第一个参数是一个指向 void* 类型的数组,用来存储调用信息;第二个参数是数组的大小,用来控制 backtrace() 函数最多返回多少个调用帧的信息。 在程序崩溃时,我们可以在程序的信号处理函数中调用 backtrace() 函数来获取当前的调用信息,然后输出到日志文件中。 4. 使用 libunwind 库 libunwind 是一个开源的 C/C++ 库,用于获取当前线程的调用信息。它可以在程序崩溃时获取调用信息,也可以在程序运行时获取调用信息。 使用 libunwind 库时,我们需要在程序中链接 libunwind 库,并且包含 libunwind.h 头文件。然后,我们可以使用 libunwind 库中的函数来获取当前线程的调用信息,例如: ```c #include <libunwind.h> void print_backtrace() { unw_cursor_t cursor; unw_context_t context; // 获取当前线程的上下文信息 unw_getcontext(&context); // 初始化 cursor unw_init_local(&cursor, &context); // 遍历调用帧,输出每个帧的信息 while (unw_step(&cursor) > 0) { unw_word_t ip; unw_get_reg(&cursor, UNW_REG_IP, &ip); printf("%lx\n", (long)ip); } } ``` 上面的代码中,我们定义了一个函数 `print_backtrace()`,它可以输出当前线程的调用信息。使用时,我们只需要在程序崩溃调用这个函数即可。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值