概述
在程序的运行过程中,比较常见的错误包括程序运行被堵在了某个地方,以及程序崩溃。
在程序运行被堵住的时候,我们可以使用gdb
、strace
等跟踪处于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<KeyValueStore::TransImpl>)
这个函数/方法的第0x2df0个字节处了。那我们可以做下面几件事:
- 定位
RedShop::ExTable::reshuffle
在ELF可执行文件中的位置 - 定位
RedShop::ExTable::reshuffle
中第0x2df0个字节位置所对应的源码 - 查看源码,分析问题
定位RedShop::ExTable::reshuffle
的位置有下面几种方法:
- 使用
readelf -s --wide EXECUTABLE | grep --color reshuffle
来查找位置,此处的EXECUTABLE为对应可执行程序的文件名,这种方法通过对ELF文件里的符号表进行查找定位,定位准确速度快 - 使用
objdump -d --section .text EXECUTABLE | grep --color reshuffle
来查找位置,这种方法通过对ELF文件的.text节进行反编译定位,虚警概率大,查找速度慢
使用第一种方法,应该会得到类似如下的输出:
7166: 000000000129ac70 18940 FUNC GLOBAL DEFAULT [`<other>`: 88] 13 _ZN7RedShop7ExTable9reshuffleEP13KeyValueStoreSt10shared_ptrINS1_9TransImplEE
注意到这里查找到的类型确实是函数(FUNC),而函数名是依照C++命名规则混名过的名称,需要使用c++filt来反混名确认下,如下:
raphael@sigma:~$ c++filt _ZN7RedShop7ExTable9reshuffleEP13KeyValueStoreSt10shared_ptrINS1_9TransImplEE
RedShop::ExTable::reshuffle(KeyValueStore*, std::shared_ptr<KeyValueStore::TransImpl>)
确认无误后,可以得知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 EXECUTABLE
与readelf -S --wide EXECUTABLE
查看文件信息发现可执行程序没有调试信息且被剥离了无用的符号信息(stripped),所以无法直接从文件并本身获取到源码信息。在Debian系统下,软件的调试信息是单独打包存放的,因此询问了系统工程师同事,知道了包名,首先dpkg -L
获取到调试包中所有的文件,然后再使用strace -e open,openat 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 (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 0x2378:
节头:
[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 000282 00 AX 0 0 16
[15] .fini PROGBITS 0000000000000804 000804 000009 00 AX 0 0 4
[16] .rodata PROGBITS 0000000000000810 000810 000013 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000000824 000824 000044 00 A 0 0 4
[18] .eh_frame PROGBITS 0000000000000868 000868 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 0000e1 00 0 0 1
[31] .debug_str PROGBITS 0000000000000000 0016c4 00028b 01 MS 0 0 1
[32] .symtab SYMTAB 0000000000000000 001950 0006c0 18 33 49 8
[33] .strtab STRTAB 0000000000000000 002010 00021b 00 0 0 1
[34] .shstrtab STRTAB 0000000000000000 00222b 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 0x6b3
fibo.c 10 0x6cd
fibo.c 11 0x706
fibo.c 12 0x71e
fibo.c 15 0x725
fibo.c 16 0x734
fibo.c 17 0x73b
fibo.c 18 0x741
fibo.c 19 0x757
fibo.c 20 0x77f
fibo.c 20 0x781
再使用addr2line
和readelf
来确认下:
raphael@sigma:~/temp/elf$ readelf -s --wide fibo | grep --color fibo
40: 0000000000000000 0 FILE LOCAL DEFAULT ABS fibo.c
66: 000000000000068a 155 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 0x6b3 <fibo+41> and ends at 0x6cd <fibo+67>.
下面尝试下把调试信息分离出来:
raphael@sigma:~/temp/elf$ objcopy --only-keep-debug fibo fibo.debug
raphael@sigma:~/temp/elf$ strip --strip-debug --strip-unneeded fibo
raphael@sigma:~/temp/elf$ objcopy --add-gnu-debuglink=fibo.debug fibo
执行完上述操作后可以看到fibo
里面原有的所有调试信息都被剥离到fibo.debug
里面去了,此时再使用objdump --dwarf=decodedline fibo
会显示没有调试信息,但是使用addr2line
与gdb
仍然可以进行指令地址与源码位置之间的相互定位。
其它
需要了解更多信息的,当然应该看看与ELF、dwarf、readelf
、objdump
、addr2line
、gdb
等工具相关的资料。对于书籍,有两本书应该还是有用的,一本是“Learning Linux Binary Analysis”,另一本则是“Practical Binary Analysis”,前者讲原理多一些,对底层的细节掌握要求更多,更加面向不喜欢太多细节的资深程序员,后者则讲操作更多一些,更加面向需要导引的入门程序员。本文里的知识大量来源于stackoverflow等互联网资源,但是无法有效追溯,只能感谢开放繁荣的互联网了。
同时也向synh、hhao、Mame等同学表示感谢。
网友评论