c语言 backtrace

轩辕煌
2023-12-01

c语言 backtrace

版权声明:本文为CSDN博主「zhouyuming_hbxt」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zym326975/article/details/114270695
原文链接2:https://www.bilibili.com/video/BV11L411A7fC?spm_id_from=333.337.search-card.all.click&vd_source=e00de32c6d84e616df38f2063641aa28

1:函数简介
通常,应用程序使用外部调试程序(比如gdb)进行调试,但有时出于统计、分析等目的,可在程序自身内使用函数backtrace记录应用程序的堆栈轨迹。
应用程序通常使用专用调试器来调试程序,但在有些情况下,调试器可能无法使用。而且,无论怎样,当程序出现问题时,向开发人员提供尽可能多的现场信息终究是有利的

因此,C库中提供了backtrace系列函数,利用这些函数可以记录在错误出现之前应用程序的运行轨迹,帮助开发人员更快地定位问题。
注:backtrace(回溯)指的是线程中当前活跃的函数调用。

backtrack系列函数包括以下三个:
1.函数backtrace的声明位于头文件execinfo.h,函数原型是:

int backtrace(void **buffer, int size);

函数backtrace返回调用程序的堆栈回溯,结果保存在参数buffer中,buffer指向数组中的每一项都是void *类型,它们是堆栈中的返回地址。参数size指定buffer中可以存放的最大数目,如果返回地址的数量多于size,则返回最近的size个地址。因此,要想获取完整的返回地址列表,buffer必须足够大。
注:返回地址的数量正常情况下不会很大,即使大型软件,也很少有超过50层嵌套。

当函数成功返回时,返回值是buffer中实际的地址数量,这个值小于等于size;如果返回值=size,那么buffer中的返回地址可能不是全部。

2.函数backtrace_symbols的声明位于头文件execinfo.h,函数原型是

char **backtrace_symbols(void *const *buffer, int size)

函数backtrace_symbols将给定buffer中的返回地址转换为符号化的地址字符串。参数size指定buffer中的地址数量。每个地址的符号表示包括:函数名称(如果能够解析)、函数内的偏移量(十六进制)以及实际的返回地址(十六进制)。该函数的结果是字符串指针数组的地址,并且由backtrace_symbols用malloc分配存储空间,使用者负责用free释放内存(数组成员无需也不能用free释放)

注:只有使用ELF格式的二进制程序和库才能获得函数名称,并且可能还需要向连接器传输特殊选项(比如GNU ld,要传递-rdynamic)

当函数成功完成时,返回值是字符串数组的地址;否则,返回NULL

3.函数backtrace_symbols_fd原型是:

void backtrace_symbols_fd(void *const *buffer, int size, int fd);

与前面不同的是,其不会返回字符串数组,而是将字符串写入fd所代表的文件,一行一个,还有一个不同的是,它不使用malloc,因此在malloc可能会失败的某些场合,可以使用这个函数来代替。

使用它们的时候有一下几点需要我们注意的地方:

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

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

 类似资料: