一. 简介
本文介绍一种在x86架构下,不使用任何外部调试工具(GDB等),在程序发生异常时及时记录堆栈信息的方法,它可以定位到库文件的函数名和行号。
- 本文先介绍backtrace系列函数,从而获取到函数调用栈的栈帧。
- 再通过BFD库(二进制文件描述库)来分析ELF文件,找到
.text节
和.debug_info节
。
- 编写一个libtrace库,调试程序可以通过
backtrace_dump()
来查看函数调用栈。
- 最后注册SIGSEGV的信号处理函数,通过libtrace定位到堆栈的源文件、函数名、行号
二. backtrace
本节的内容源自man 3 backtrace
,原文中有一个测试程序,感兴趣可以去实际运行一下代码。
2.1 函数原型
1 2 3 4 5
| #include <execinfo.h>
int backtrace(void *buffer[.size], int size);
char **backtrace_symbols(void *const buffer[.size], int size);
|
2.2 backtrace()
backtrace()
将回溯信息存放在buffer数组中。回溯信息包含一系列当前线程活跃的函数调用。buffer中的每个成员记录了函数(指令)的返回地址。
1 2 3 4 5 6 7 8 9 10 11
| buffer:[100] [0]: 0x7ffff7fbf913 [1]: 0x7ffff7fbf9cb [2]: 0x7ffff7fb9126 [3]: 0x7ffff7fb9137 [4]: 0x7ffff7fb9148 [5]: 0x555555555161 [6]: 0x7ffff7df31ca [7]: 0x7ffff7df3285 [8]: 0x555555555081 [9]: 0x0
|
2.3 backtrace_symbols()
backtrace_symbols()
将buffer中的一系列地址转换为易读的字符串。其中包含父函数名称、子函数(指令)在父亲函数中的的偏移、子函数的实际返回地址。
这里少了两层调用栈是因为我跳过了backtrace_dump()
的调用栈打印
1 2 3 4 5 6 7 8
| strings:[] [0]: /home/zrf/git/study/utils/libtest.so(+0x1126) [0x7f2f83880126] [1]: /home/zrf/git/study/utils/libtest.so(+0x1137) [0x7f2f83880137] [2]: /home/zrf/git/study/utils/libtest.so(func_name1+0xe) [0x7f2f83880148] [3]: ./main(main+0x18) [0x55d657aa8161] [4]: /lib/x86_64-linux-gnu/libc.so.6(+0x271ca) [0x7f2f836ba1ca] [5]: /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x85) [0x7f2f836ba285] [6]: ./main(_start+0x21) [0x55d657aa8081]
|
2.4 注意事项
- 这两个函数都是线程安全的
- 想要获得更符合源码的调用栈信息,CC编译时需要不进行优化(指定-O0)
- 内联函数没有栈指针
- 不使用特定的链接选项
-rdynamic
可能会无法显示函数名称
- 被定义为
static
的函数无法显示函数名
2.5 测试程序
在库libtest.so中,函数func_name3()
调用backtrace()
系列函数,将打印此时调用栈信息。
请注意func_name2()
和func_name3()
都被定义为了static
类型,所以backtrace_symbols()
无法显示函数名。

1 2 3 4 5 6 7 8 9
| Debug backtrace: func_name3 backtrace dumping 7 stack frame addresses: /home/zrf/git/study/utils/libtest.so(+0x1126) [0x7f7433a15126] /home/zrf/git/study/utils/libtest.so(+0x1137) [0x7f7433a15137] /home/zrf/git/study/utils/libtest.so(func_name1+0xe) [0x7f7433a15148] ./main(main+0x18) [0x55f9d9be8161] /lib/x86_64-linux-gnu/libc.so.6(+0x271ca) [0x7f743384f1ca] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x85) [0x7f743384f285] ./main(_start+0x21) [0x55f9d9be8081]
|
三. BFD
BFD库(二进制文件描述库)是GNU项目用于解决不同格式的目标文件的可移植性的主要机制。
以下内容摘自BFD
BFD通过对目标文件提供公共抽象视图来达成工作。一个目标文件有带有描述信息的一个“头”;可变量目的“段”,每个段都有一个名字、一些属性和一块数据;一个符号表;一组重定位入口项;诸如此类。
在内部,BFD将数据从抽象视图转换到目标处理器和文件格式所要求的位/字节布局的细节。它的关键服务包括处理字节序差异,比如在小端序主机和大端序目标之间,在32-bit和64-bit数据之间的正确转换,和重定位入口项所指定的寻址算术的细节。
尽管BFD最初设计成为可以被各种工具使用的通用库,频繁需要修补API来容纳新系统的功能,倾向于限制了它的使用;BFD的主要用户是GNU汇编器(GAS),GNU连接器(GLD),和其他GNU二进制实用程序(”binutils”)工具,和GNU调试器(GDB)。因此,BFD不单独发行,总是包括在binutils和GDB发行之中。不论如何,BFD是将GNU工具用于嵌入式系统开发的关键部件。
BFD库可以用来读取核心转储的结构化数据。
3.1 dladdr()
dladdr()
检测给定的地址是否”合法”,如果合法,就返回一个Dl_info的结构,现在解释一下这个结构体中每个字段的含义:
1. 结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| typedef struct {
const char *dli_fname;
void *dli_fbase;
const char *dli_sname;
void *dli_saddr; } Dl_info;
|
2. 举例讲解dladdr
我举一个详细的例子来讲解这个结构体
在libtest.so中,我定义了一个名为func_name1()
的函数,他的代码如下:
1 2 3 4
| void func_name1() { func_name2(); }
|
接下来描述这个函数被调用时的栈信息:
- 0x7ffff7fb8000附近有ELF格式信息
- 0x7ffff7fb8394附近有函数名字符串
- 0x7ffff7fb913a附近有函数的具体指令

1 2 3 4 5 6
| 两个字符串指针: dli_fname字段指向了libtest.so的绝对路径 dli_sname字段指向了函数的名称"func_name1" 两个地址指针: dli_saddr字段指向了函数func_name1()的汇编指令起始地址。 dli_fbase字段指向了libtest.so在虚拟内存的地址
|
我们再看下backtrace()
的buffer字段中的地址值0x7ffff7fb9148

再结合func_name1()的汇编指令来观察:
1 2 3 4 5 6 7 8
| 000000000000113a <func_name1>: 113a: 55 push %rbp 113b: 48 89 e5 mov %rsp,%rbp 113e: b8 00 00 00 00 mov $0x0,%eax 1143: e8 e1 ff ff ff call 1129 <func_name2> 1148: 90 nop 1149: 5d pop %rbp 114a: c3 ret
|
可以发现指令90 5D C3
就是func_name1()
执行完调用func_name2()
(call 1129 <func_name2>
)后要执行的指令。到此为止,我们应该就可以利用这些指针的相对位置,来定位调用栈的行号了。
我们可以使用backtrace()
的buffer字段中的地址值0x7ffff7fb9148
来减去这个库文件的基地址0x7ffff7fb8000
,得到一个偏移0x1148
,结合刚刚的汇编指令,可以发现这个地址刚好就是从func_name2()
返回后要执行的指令所在地址。
3. addr2line
在介绍libbdf之前我们先介绍一个工具来查看指令所在的行——addr2line
查看刚刚打印的堆栈信息
1 2 3 4 5 6 7 8 9
| Debug backtrace: func_name3 backtrace dumping 7 stack frame addresses: /home/zrf/git/study/utils/libtest.so(+0x1126) [0x7f7433a15126] /home/zrf/git/study/utils/libtest.so(+0x1137) [0x7f7433a15137] /home/zrf/git/study/utils/libtest.so(func_name1+0xe) [0x7f7433a15148] ./main(main+0x18) [0x55f9d9be8161] /lib/x86_64-linux-gnu/libc.so.6(+0x271ca) [0x7f743384f1ca] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x85) [0x7f743384f285] ./main(_start+0x21) [0x55f9d9be8081]
|
例如:我们想看func_name1中具体是从哪一行调用了backtrace()
我们已经拿到了库文件的路径,也拿到了指令在库函数中的位置func_name1+0xe
:
1 2 3 4
| zrf@debian:~/git/study/utils$ addr2line -e /home/zrf/git/study/utils/libtest.so func_name1+0xe -f -p func_name1 at /home/zrf/git/study/utils/libtest.c:17
|
3.2 libbfd
我们暂时总结一下现在收集到的栈信息(可以结合上文中提到的栈图片来观察):
- libtest库的地址:
0x7ffff7fb8000
- 父函数字符串:
0x7ffff7fb8394
- 父函数完整的汇编指令:
0x7ffff7fb913a
- 子函数回溯栈指针:
0x7ffff7fb9148
接下来,我们将使用这些地址信息,并结合libbfd库,来定位回溯栈地址对应的指令所在的源文件、函数名和代码行。
1. ELF文件格式
我将ELF文件分为五个部分:
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct elf_file { struct elf_header; struct program_header_table; struct section_header_table; struct sysbol_table; struct dynamic_symbol_table; };
|
我们首先要做的就是找到ELF文件的符号表并记录下来,再找到当前指令的调试信息在符号表中的偏移,我们拿着符号表和偏移地址就可以找到找到.debug_info、.debug_line等信息,这些节中就包含当前指令地址所对应的源码文件、函数名和行号。
我们可以通过readelf --debug libtest.so
来查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <2b7> DW_AT_external : 1 <2b7> DW_AT_name : (indirect string, offset: 0x2e): func_name3 <2bb> DW_AT_decl_file : 1 <2bb> DW_AT_decl_line : 4 <2bc> DW_AT_decl_column : 6 <2bc> DW_AT_low_pc : 0x1129 <2c4> DW_AT_high_pc : 0x20 <2cc> DW_AT_frame_base : 1 byte block: 9c (DW_OP_call_frame_cfa) <2ce> DW_AT_call_all_tail_calls: 1
The File Name Table (offset 0x38, lines 7, columns 2): Entry Dir Name 0 0 (indirect line string, offset: 0): libtest.c 1 0 (indirect line string, offset: 0): libtest.c 2 1 (indirect line string, offset: 0x99): stddef.h 3 2 (indirect line string, offset: 0xa2): types.h 4 3 (indirect line string, offset: 0xaa): struct_FILE.h 5 3 (indirect line string, offset: 0xb1): FILE.h 6 0 (indirect line string, offset: 0xb8): backtrace.h
|
2. BFD重要的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| bfd *bfd_openr (const char *filename, const char *target);
bfd_get_dynamic_symtab_upper_bound(abfd)
bfd_canonicalize_dynamic_symtab(abfd, asymbols)
bfd_canonicalize_symtab(abfd, location)
void bfd_map_over_sections (bfd *abfd, void (*func) (bfd *abfd, asection *sect, void *obj), void *obj);
#define bfd_find_nearest_line(abfd, sec, syms, off, file, func, line)
|
我们来看下libtrace库中最重要的函数backtrace_dump()
流程图

3.3 最终效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Debug backtrace: func_name3 backtrace dumping 7 stack frame addresses: /home/zrf/git/study/utils/libtest.so @ 0x7fc3c8714000 [0x7fc3c8715126] -> source file[/home/zrf/git/study/utils/libtest.c] function[func_name3] line[7] /home/zrf/git/study/utils/libtest.so @ 0x7fc3c8714000 [0x7fc3c8715137] -> source file[/home/zrf/git/study/utils/libtest.c] function[func_name2] line[12] /home/zrf/git/study/utils/libtest.so @ 0x7fc3c8714000 (func_name1+0xe) [0x7fc3c8715148] -> source file[/home/zrf/git/study/utils/libtest.c] function[func_name1] line[17] ./main @ 0x55d2d7f65000 (main+0x18) [0x55d2d7f66161] /lib/x86_64-linux-gnu/libc.so.6 @ 0x7fc3c8522000 [0x7fc3c85491ca] -> source file[./csu/../sysdeps/x86/libc-start.c] function[__libc_start_call_main] line[74] /lib/x86_64-linux-gnu/libc.so.6 @ 0x7fc3c8522000 (__libc_start_main+0x85) [0x7fc3c8549285] -> source file[./csu/../csu/libc-start.c] function[call_init] line[128] ./main @ 0x55d2d7f65000 (_start+0x21) [0x55d2d7f66081]
|
3.4 总结
我们可以使用这种方法更加准确的定位到库文件的源码名称、函数名(backtrace_symbols()
是无法显示被定义为static
类型的函数名的,但是我们这个方法可以拿到函数名)、以及行号。
我将这种方法编译为了一个动态库——libtrace.so,如果想使用这个功能,在希望打印调用栈时调用backtrace_dump()
就可以了
1 2 3 4 5 6
|
void backtrace_dump(char *label, FILE *file, bool detailed);
|
由于libtrace只对库文件中的符号表做了打印行号的功能,所以针对本地的符号是不打印行号的(如果有必要的话,当然后续可以继续完善)。
四. 通过信号处理函数打印调用栈
开发中,难免会遇到堆栈情况,除了gdb外,我们也可以注册SIGSEGV信号的信号处理函数,如果发生堆栈,则打印调用栈
看下代码:
main()
函数中会调用terrible_function()
,他在一个名为libtest.so的库中,并且这个函数会产生堆栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <stdio.h> #include <stdlib.h> #include <signal.h> #include "backtrace.h"
extern void terrible_function();
static void segv_handler(int signal) { backtrace_dump("SIGSEGV", NULL, true); abort(); }
int main() { backtrace_init();
struct sigaction action; action.sa_handler = segv_handler; sigaction(SIGSEGV, &action, NULL);
terrible_function();
backtrace_deinit(); return 0; }
|
libtest.so中的terrible_function()
1 2 3 4 5 6
| #include <stdio.h>
void terrible_function() { printf("%s\n", __LINE__); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| zrf@debian:~/git/study/utils$ ./main Debug backtrace: SIGSEGV dumping 10 stack frame addresses: ./main @ 0x557de421b000 [0x557de421c1ad] /lib/x86_64-linux-gnu/libc.so.6 @ 0x7f3cc9464000 [0x7f3cc949ffd0] -> source file[libc_sigaction.c] function[__restore_rt] line[0] /lib/x86_64-linux-gnu/libc.so.6 @ 0x7f3cc9464000 [0x7f3cc95cb618] -> source file[./string/../sysdeps/x86_64/multiarch/strlen-evex.S] function[__strlen_evex] line[79] /lib/x86_64-linux-gnu/libc.so.6 @ 0x7f3cc9464000 [0x7f3cc94c2168] -> source file[./stdio-common/./stdio-common/vfprintf-process-arg.c] function[__vfprintf_internal] line[397] /lib/x86_64-linux-gnu/libc.so.6 @ 0x7f3cc9464000 (_IO_printf+0xab) [0x7f3cc94b656b] -> source file[./stdio-common/./stdio-common/printf.c] function[__printf] line[37] /home/zrf/git/study/utils/libtest.so @ 0x7f3cc9656000 (terrible_function+0x1d) [0x7f3cc9657126] -> source file[/home/zrf/git/study/utils/libtest.c] function[terrible_function] line[6] ./main @ 0x557de421b000 (main+0x46) [0x557de421c1f8] /lib/x86_64-linux-gnu/libc.so.6 @ 0x7f3cc9464000 [0x7f3cc948b1ca] -> source file[./csu/../sysdeps/x86/libc-start.c] function[__libc_start_call_main] line[74] /lib/x86_64-linux-gnu/libc.so.6 @ 0x7f3cc9464000 (__libc_start_main+0x85) [0x7f3cc948b285] -> source file[./csu/../csu/libc-start.c] function[call_init] line[128] ./main @ 0x557de421b000 (_start+0x21) [0x557de421c0c1] Aborted
|
可以发现我们获取了完整的调用栈,可以定位到source file[/home/zrf/git/study/utils/libtest.c] function[terrible_function] line[6]
这个函数出了问题
五. libtrace
本节将介绍libtrace库的代码实现。
5.1 backtrace()
这里不做赘述,请直接参考man 3 backtrace
,我们的目的就是通过这个函数拿到栈帧。
5.2 dladdr()
已经很详细的讲解了这个函数,请回顾3.1 节
。我们的目的是通过这个函数拿到动态库在内存中的基地址,使用栈帧减去基地址得到指令在动态库中的偏移。
1 2 3 4 5 6 7
| dladdr(this->frames[i], &info); void *ptr = this->frames[i];
if (strstr(info.dli_fname, ".so")) { ptr = (void*)(this->frames[i] - info.dli_fbase); }
|
5.3 print_sourceline()
1 2 3 4 5 6 7
|
static void print_sourceline(FILE *file, char *filename, void *ptr);
|
这个函数中有三个重要步骤
- 在缓存中查找是否存在动态库的bfd实例,如果存在则返回实例,否则创建一个实例,并加入缓存
- 调用
bfd_map_over_sections()
遍历每个节,找到.text
- 在.text节中使用
bfd_find_nearest_line()
来获取源文件、函数名、行号
1 2 3 4 5 6 7 8 9 10
| pthread_mutex_lock(&bfd_mutex);
entry = get_bfd_entry(filename); if (entry) { data->entry = entry; bfd_map_over_sections(entry->abfd, (void*)find_addr, data); } pthread_mutex_unlock(&bfd_mutex);
|
1 2 3 4 5 6 7 8 9 10
| static void find_addr(bfd *abfd, asection *section, bfd_find_data_t *data) { vma = bfd_section_vma(section); data->found = bfd_find_nearest_line(abfd, section, data->entry->syms, data->vma - vma, &source, &function, &line);
println(data->file, " -> source file[%s] function[%s] line[%d]\n", source, function, line); }
|
5.4 源码下载连接