通过调用栈诊断程序崩溃入门

概述

在程序的运行过程中,比较常见的错误包括程序运行被堵在了某个地方,以及程序崩溃。

在程序运行被堵住的时候,我们可以使用gdbstrace等跟踪处于R状态的程序运行状态,也可以像之前说过的那样,使用procfs的syscall与stack虚拟文件诊断处于S或者D状态的程序问题。

在程序崩溃的时候,很多时候都会有应用程序崩溃时调用栈的,如果应用程序崩溃的现象重现比较麻烦,那么应该能从应用程序崩溃时的调用栈定位到对应的源代码,至少能够通过静态源码进行一定的分析工作,从而快速定位程序可能出现的问题。

案例分析

下面我们来详细说说通过这种方法定位程序问题的步骤。

首先给一个真实的案例,在程序中出现了崩溃的情况,其调用栈如下:

1: (()+0x145882c) [0x2000245882c]
2: (()+0x19890) [0x2000b987890]
3: (RedShop::ExTable::reshuffle(KeyValueStore*, std::shared_ptr<KeyValueStore::TransImpl>)+0x2df0) [0x2000229da60]
4: (RedShop::_txc_write_nodes(RedShop::TransContext*, std::shared_ptr<KeyValueStore::TransImpl>)+0x218) [0x2000229f888]
5: ...

顶部的两个函数没有函数名,而且函数体特别大(至少有0x145882c = 21,334,060个字节与0x19890 = 104,592字节),正常的函数显然是没有这么大的,所以我们可以先把它们看成是系统的异常处理函数,暂时忽略过去,那么实际出现问题的地方就是RedShop::ExTable::reshuffle(KeyValueStore*, std::shared_ptr)这个函数/方法的第0x2df0个字节处了。那我们可以做下面几件事:

  1. 定位RedShop::ExTable::reshuffle在ELF可执行文件中的位置
  2. 定位RedShop::ExTable::reshuffle中第0x2df0个字节位置所对应的源码
  3. 查看源码,分析问题

定位RedShop::ExTable::reshuffle的位置有下面几种方法:

  1. 使用readelf -s --wide EXECUTABLE | grep --color reshuffle来查找位置,此处的EXECUTABLE为对应可执行程序的文件名,这种方法通过对ELF文件里的符号表进行查找定位,定位准确速度快
  2. 使用objdump -d --section .text EXECUTABLE | grep --color reshuffle来查找位置,这种方法通过对ELF文件的.text节进行反编译定位,虚警概率大,查找速度慢

使用第一种方法,应该会得到类似如下的输出:

  7166: 000000000129ac70 18940 FUNC    GLOBAL DEFAULT [``: 88]    13 _ZN7RedShop7ExTable9reshuffleEP13KeyValueStoreSt10shared_ptrINS1_9TransImplEE

注意到这里查找到的类型确实是函数(FUNC),而函数名是依照C++命名规则混名过的名称,需要使用c++filt来反混名确认下,如下:

raphael@sigma:~$ c++filt _ZN7RedShop7ExTable9reshuffleEP13KeyValueStoreSt10shared_ptrINS1_9TransImplEE
RedShop::ExTable::reshuffle(KeyValueStore*, std::shared_ptr)

确认无误后,可以得知RedShop::ExTable::reshuffle的地址是000000000129ac70,大小为18940个字节,运行python -c "print hex(0x000000000129ac70+0x2df0)"得知出现问题的指令地址为0x129da60。

接下来可以使用addr2line查找指令对应的源码位置,如下:

raphael@sigma:~$ addr2line -af 0x129da60 -e EXECUTABLE
0x000000000129da60
_ZN7RedShop7ExTable9reshuffleEP13KeyValueStoreSt10shared_ptrINS1_9TransImplEE
??:?

在这里返回的源码位置为??:?,显然是没有得到源码信息,使用file EXECUTABLEreadelf -S --wide EXECUTABLE查看文件信息发现可执行程序没有调试信息且被剥离了无用的符号信息(stripped),所以无法直接从文件并本身获取到源码信息。在Debian系统下,软件的调试信息是单独打包存放的,因此询问了系统工程师同事,知道了包名,首先dpkg -L获取到调试包中所有的文件,然后再使用strace -e open addr2line -af 0x129da60 -e EXECUTABLE可以跟踪到addr2line会从什么路径读取调试信息,然后将调试包里对应的文件复制到对应的路径即可。

经过上述处理后,再次运行addr2line,如下:

raphael@sigma:~$ addr2line -af 0x129da60 -e EXECUTABLE
0x000000000129da60
_ZNSt13__atomic_baseIiEppEv
/usr/include/c++/x.y/bits/atomic_base.h:296

这下倒是可以看到源码信息了,但是却发现是系统自带的C++标准库(版本为x.y)中的源码位置,在这种情况下,我们可以逐步回退,直到回退到项目源码位置,再从源码位置定位。

假设我们使用的是4字节一指令长度的指令集架构,那么可以写一个python文件生成一系列的地址与源码对应的查询:

addr = 0x129da60

print 'addr2line -af -e EXECUTABLE',
for a in range(60):
    print hex(addr - a*4),

这样就可以回溯足够长的指令,最终定位到原始项目的源码位置,通过分析源码定位问题所在了。

扩展

通过addr2line能够从指令地址定位到源码,那么反过来呢?如何从源码定位到指令地址?很遗憾,常见的工具里并没有line2addr这样的命令,不过gdb可以来帮忙。

下面是一个简单的C程序:

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

long long data[10] = {1, 1};

long long fibo(int n)
{
    if (n < 2)
        return 1;

    if (data[n])
        return data[n]; 
    data[n] = fibo(n-1) + fibo(n-2);
    return data[n];
}

int main(int argc, char* argv[])
{
    int n = 5;
    if (argc > 1)
        n = atoi(argv[1]);
    printf("fibo %d: %lld\n", n, fibo(n));
}

我们来带上调试信息编译下:gcc -g fibo.c -o fibo

再看看是否有调试信息:readelf -S --wide fibo,结果如下:

There are 35 section headers, starting at offset 0x2380:

节头:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        0000000000000238 000238 00001c 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            0000000000000254 000254 000020 00   A  0   0  4
  [ 3] .note.gnu.build-id NOTE            0000000000000274 000274 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0000000000000298 000298 00001c 00   A  5   0  8
  [ 5] .dynsym           DYNSYM          00000000000002b8 0002b8 0000c0 18   A  6   1  8
  [ 6] .dynstr           STRTAB          0000000000000378 000378 000089 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          0000000000000402 000402 000010 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         0000000000000418 000418 000020 00   A  6   1  8
  [ 9] .rela.dyn         RELA            0000000000000438 000438 0000c0 18   A  5   0  8
  [10] .rela.plt         RELA            00000000000004f8 0004f8 000030 18  AI  5  23  8
  [11] .init             PROGBITS        0000000000000528 000528 000017 00  AX  0   0  4
  [12] .plt              PROGBITS        0000000000000540 000540 000030 10  AX  0   0 16
  [13] .plt.got          PROGBITS        0000000000000570 000570 000008 08  AX  0   0  8
  [14] .text             PROGBITS        0000000000000580 000580 000292 00  AX  0   0 16
  [15] .fini             PROGBITS        0000000000000814 000814 000009 00  AX  0   0  4
  [16] .rodata           PROGBITS        0000000000000820 000820 000013 00   A  0   0  4
  [17] .eh_frame_hdr     PROGBITS        0000000000000834 000834 000044 00   A  0   0  4
  [18] .eh_frame         PROGBITS        0000000000000878 000878 000130 00   A  0   0  8
  [19] .init_array       INIT_ARRAY      0000000000200de8 000de8 000008 08  WA  0   0  8
  [20] .fini_array       FINI_ARRAY      0000000000200df0 000df0 000008 08  WA  0   0  8
  [21] .dynamic          DYNAMIC         0000000000200df8 000df8 0001e0 10  WA  6   0  8
  [22] .got              PROGBITS        0000000000200fd8 000fd8 000028 08  WA  0   0  8
  [23] .got.plt          PROGBITS        0000000000201000 001000 000028 08  WA  0   0  8
  [24] .data             PROGBITS        0000000000201040 001040 000070 00  WA  0   0 32
  [25] .bss              NOBITS          00000000002010b0 0010b0 000008 00  WA  0   0  1
  [26] .comment          PROGBITS        0000000000000000 0010b0 00001d 01  MS  0   0  1
  [27] .debug_aranges    PROGBITS        0000000000000000 0010cd 000030 00      0   0  1
  [28] .debug_info       PROGBITS        0000000000000000 0010fd 0003aa 00      0   0  1
  [29] .debug_abbrev     PROGBITS        0000000000000000 0014a7 00013c 00      0   0  1
  [30] .debug_line       PROGBITS        0000000000000000 0015e3 0000e3 00      0   0  1
  [31] .debug_str        PROGBITS        0000000000000000 0016c6 00028b 01  MS  0   0  1
  [32] .symtab           SYMTAB          0000000000000000 001958 0006c0 18     33  49  8
  [33] .strtab           STRTAB          0000000000000000 002018 00021b 00      0   0  1
  [34] .shstrtab         STRTAB          0000000000000000 002233 000147 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

显然其中的.debug_line等节包含了对应的源码-指令对应信息,我们来确认下:

raphael@sigma:~/temp/elf$ objdump --dwarf=decodedline fibo

fibo:     文件格式 elf64-x86-64

.debug_line 节的内容:

CU: ./fibo.c:
File name                            Line number    Starting address    View
fibo.c                                         7               0x68a
fibo.c                                         8               0x696
fibo.c                                         9               0x69c
fibo.c                                        11               0x6a6
fibo.c                                        12               0x6c3
fibo.c                                        13               0x6dd
fibo.c                                        14               0x716
fibo.c                                        15               0x72e
fibo.c                                        18               0x735
fibo.c                                        19               0x744
fibo.c                                        20               0x74b
fibo.c                                        21               0x751
fibo.c                                        22               0x767
fibo.c                                        23               0x78f
fibo.c                                        23               0x791

再使用addr2linereadelf来确认下:

raphael@sigma:~/temp/elf$ readelf -s --wide fibo | grep --color fibo
    40: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS fibo.c
    66: 000000000000068a   171 FUNC    GLOBAL DEFAULT   14 fibo
raphael@sigma:~/temp/elf$ addr2line -af -e fibo 0x696
0x0000000000000696
fibo
/home/raphael/temp/elf/fibo.c:8

使用gdb来从源代码反查指令范围:

raphael@sigma:~/temp/elf$ gdb fibo -ex 'i line fibo.c:9' --batch
Line 9 of "fibo.c" starts at address 0x69c  and ends at 0x6a6 .

如果还要进行更多的实验,不妨不带调试信息重新编译程序,或者将调试信息独立成文件来测试,结果应该是类似的。

其它

需要了解更多信息的,当然应该看看与ELF、dwarf、readelfobjdumpaddr2linegdb等工具相关的资料。对于书籍,有两本书应该还是有用的,一本是“Learning Linux Binary Analysis”,另一本则是“Practical Binary Analysis”,前者讲原理多一些,对底层的细节掌握要求更多,更加面向不喜欢太多细节的资深程序员,后者则讲操作更多一些,更加面向需要导引的入门程序员。

同时也向synh、hhao、Mame等同学表示感谢。

发表评论

电子邮件地址不会被公开。 必填项已用*标注