本文共 4444 字,大约阅读时间需要 14 分钟。
通常我们只是在调试程序的时候,才用 gdb bt命令显示当前进程(或线程)所在的堆栈。实际代码开发中,一般较少需要得到当前程序的堆栈。但是,在调试一些不容易复现、gdb难以跟踪的bug时,或在需要记录部分代被执行的上下文的情景下,就非常有必要得到当前程序运行的堆栈。
要实现上面得到当前程序堆栈的功能,需要依赖glibc中execinfo.h声明的backtrace()函数族。为此我们需要先了解glibc中backtrace是如何实现的。
可以参考glibc-2.17/debug/backtrace.c,执行过程是从栈顶遍历到栈底,一层层根据调用关系,取得当前sp的值,并保存在指定的数组里面。
/* By default assume the `next' pointer in struct layout points to the next struct layout. */#ifndef ADVANCE_STACK_FRAME# define ADVANCE_STACK_FRAME(next) BOUNDED_1 ((struct layout *) (next))#endif/* By default, the frame pointer is just what we get from gcc. */#ifndef FIRST_FRAME_POINTER# define FIRST_FRAME_POINTER __builtin_frame_address (0)#endifint__backtrace (array, size) void **array; int size;{ struct layout *current; void *__unbounded top_frame; void *__unbounded top_stack; int cnt = 0; top_frame = FIRST_FRAME_POINTER; top_stack = CURRENT_STACK_FRAME; /* We skip the call to this function, it makes no sense to record it. */ current = BOUNDED_1 ((struct layout *) top_frame); while (cnt < size){ if ((void *) current INNER_THAN top_stack || !((void *) current INNER_THAN __libc_stack_end)) /* This means the address is out of range. Note that for the toplevel we see a frame pointer with value NULL which clearly is out of range. */break; array[cnt++] = current->return_address; current = ADVANCE_STACK_FRAME (current->next); } return cnt;}weak_alias (__backtrace, backtrace)libc_hidden_def (__backtrace)
这样,程序开发者就可以直接include execinfo.h头文件,然后调用backtrace()函数。execinfo.h中列出了实现类似功能的一组函数族:
/* Store up to SIZE return address of the current program state in ARRAY and return the exact number of values stored. */ extern int backtrace (void **__array, int __size) __nonnull ((1));/* Return names of functions from the backtrace list in ARRAY in a newly malloc()ed memory block. */ extern char **backtrace_symbols (void *const *__array, int __size) __THROW __nonnull ((1));/* This function is similar to backtrace_symbols() but it writes the result immediately to a file. */ extern void backtrace_symbols_fd (void *const *__array, int __size, int __fd) __THROW __nonnull ((1));
从参数中可以想到,需要为array预备一部分存储调用栈的存储空间,后面调用的backtrace()把会进程当前执行的栈信息写到这个array里面去。
具体示例如下execinfo.c:
#include#include int main(int argc, char * argv[]){int i = 0;void * stack[1024] = { NULL,};backtrace(stack, 1024);for (i = 0; i < 32; i++) { printf("stack %d: %p\n", i, stack[i]);}return 0;}
gcc -g -o execinfo execinfo.c 完成之后,可以看一下这个程序的运行结果:
[root@localhost test]# ./execinfo stack 0: 0x4005cd stack 1: 0x7fced5bb6c05 stack 2: 0x4004b9 stack 3: (nil) .......我们接着对execinfo反汇编: objdump -alDS ./execinfo >> execinfo.S,得到execinfo.S之后,
看看0x4005cd 对应到哪一行代码: /home/qxi/test/execinfo.c:34backtrace(stack, 1024); 4005b9: 48 8d 85 f0 df ff ff lea -0x2010(%rbp),%rax 4005c0: be 00 04 00 00 mov $0x400,%esi 4005c5: 48 89 c7 mov %rax,%rdi 4005c8: e8 83 fe ff ff callq 400450/home/qxi/test/execinfo.c:36 for (i = 0; i < 32; i++) { 4005cd: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 4005d4: eb 25 jmp 4005fb /home/qxi/test/execinfo.c:37 (discriminator 2)
可以看到它指向的是backtrace()执行之后的程序地址,也就是最后一个入栈的值。接着看0x40004b9分别对应哪个函数:
Disassembly of section .text:0000000000400490 <_start>:_start(): 400490: 31 ed xor %ebp,%ebp 400492: 49 89 d1 mov %rdx,%r9 400495: 5e pop %rsi 400496: 48 89 e2 mov %rsp,%rdx 400499: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 40049d: 50 push %rax 40049e: 54 push %rsp 40049f: 49 c7 c0 80 06 40 00 mov $0x400680,%r8 4004a6: 48 c7 c1 10 06 40 00 mov $0x400610,%rcx 4004ad: 48 c7 c7 80 05 40 00 mov $0x400580,%rdi 4004b4: e8 b7 ff ff ff callq 400470 <__libc_start_main@plt> 4004b9: f4 hlt 4004ba: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
可以看到4004b9记录的是从glibc跳到main()函数之后执行的第一条指令的位置,也就是最早入栈的值。
函数调栈中压栈的值,记录着当前调用返回后会执行的下一个指令(函数)的地址. 结合上面的原理和示例分析,可以看到在应用程序中得到当前程序的调用栈的过程,就是把函数调用过程中一层层入栈的值,从栈顶一个个再次读出的过程。因此利用这个特性,再结合一些其他技术,我们可以用来实现跟踪资源泄漏、锁申请而没有释放等高级功能。
本文转自存储之厨51CTO博客,原文链接:http://blog.51cto.com/xiamachao/2064805 ,如需转载请自行联系原作者