一、简介

1、addr2line:地址转为源码行号

2、backtrace:获取程序调用栈

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

  1. #include <execinfo.h>
  2. /* Store up to SIZE return address of the current program state in
  3. ARRAY and return the exact number of values stored. */
  4. int backtrace(void **array, int size);
  5. /* Return names of functions from the backtrace list in ARRAY in a newly
  6. malloc()ed memory block. */
  7. char **backtrace_symbols(void *const *array, int size);
  8. /* This function is similar to backtrace_symbols() but it writes the result
  9. immediately to a file. */
  10. void backtrace_symbols_fd(void *const *array, int size, int fd);

它们由 GNU C Library 提供,关于它们更详细的介绍可参考 Linux Programmer’s Manual 中关于 backtrack 相关函数的介绍。

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

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

Backtrace 中,一般都只有一些地址。但是利用 addr2line 这个工具,就可以找到对应的代码行。前提条件是可执行程序或者动态链接库编译的时候带 - g 选项。

具体来说,分两种情况:

  • 如果关注的一行 backtrace 位于一个可执行文件中,那么直接addr2line -e <executable> <address>
  • 如果关注的 backtrace 位于一个动态链接库中,那么麻烦一些,因为动态链接库的基地址不是固定的。这个时候,首先要把进程的 memory map 找来。在 Linux 下,进程的 memory map 可以在/proc/<pid>/maps文件中得到。然后在这个文件中找到动态链接库的基地址,然后将 backtrace 中的地址 - 动态链接库的基地址,得到偏移地址 offset address, 最后addr2line -e <shared library> <offset address>

当然,用 GDB 也可以找出地址对应的代码行。不过相比 addr2line,GDB 需要将 BUG 现象重现一遍,所以对于不好重现的 BUG,或是随机重现的 BUG 来说,使用 addr2line 就可以直接从 backtrace 找到对应的代码行,不需要重现现象,比 GDB 使用起来更简单。

二、从 backtrace 信息分析定位问题

1、测试程序

为了更好的说明和分析问题,将通过一个小程序来进行分析,程序由分别是 mian.c、call_backtrace.c 、math_lib.c组成,其中 math_lib.c 提供了除法计算;call_backtrace.c 中主要用于输出 backtrace 信息;main.c则包含了我们的 主函数,它会先注册段错误信号的处理函数然后去调用math_lib.c中的除法,并使除数为0。源程序如下:

/*
 * math_lib.c
 *
 *  Created on: Jun 1, 2022
 *      Author: mate
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int devide(int a, int b)
{
    /*应该对b进行输入检查*/
    return a/b;
}

int add(int a, int b)
{
    return a+b;
}
/*
 * call_backtrace.c
 *
 *  Created on: Jun 1, 2022
 *      Author: mate
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>        /* for signal */
#include <execinfo.h>     /* for backtrace() */

#define BACKTRACE_SIZE   64

static 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 call_backtrace_handler(int signo)
{

    #if 0
    char buff[512] = {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);           /* 重新发送信号 */
}
/*
 * main.c
 *
 *  Created on: Jun 1, 2022
 *      Author: mate
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>        /* for signal */
#include <execinfo.h>     /* for backtrace() */

#include "call_backtrace.h"
#include "math_lib.h"

int main(int argc, char *argv[])
{
    int a = 1;
    int b = 0;
    signal(SIGFPE, call_backtrace_handler);  /* 为SIGFPE信号安装新的处理函数 */
    printf(" sum = %d \n", devide(a,b));
    return 0;
}

注意:网上大多数教程使用的是SIGSEGV,段错误信号,但是我使用SIGFPE信号。
在POSIX兼容的平台上,SIGFPE是当一个进程执行了一个错误的算术操作时发送给它的信号。SIGFPE的符号常量在头文件signal.h中定义。因为在不同平台上,信号数字可能变化,因此常使用信号名称。FPE是floating-point exception(浮点异常)的首字母缩略字。产生SIGFPE信号时并非一定要涉及浮点算术,之所以不修改名字是因为这么做会破坏向下兼容性。

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

我们首先将用最基本的编译方式将他们编译成一个可执行文件并执行,如下:
Screenshot from 2022-06-15 15-23-31.png
注意:根据网上的教程,大多传给addr2line的地址是绝对地址,但是我这里不行,最后传入相对地址才正常转换。如上图所示。

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 程序结果如下:

zoulm@zoulm-VirtualBox:/home/share/work/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...
段错误 (核心已转储)

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

zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ 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 命令得到如下结果:

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

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

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

  1. 利用 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,由前面可知这个地址是正确的。

通过在编译过程中生成 so 包的 map 文件,其中存放的是堆栈的各种信息,实际上除了生成 map 之外,还可以通过 readelf -s 和 nm 两个命令来查询逻辑地址,实现对问题的定位。
例如:
addr2line和backtrace - 图2

其中 libtest_lib,so 对应的是 libadd.so 这个包,libtest_lib.so 是我在自己电脑上打包起的名字。可以看出其实都是一样的地址。

参考资料