如何得到當前程序執行的堆棧
1. 背景
通常我們只是在調試程序的時候,才用 gdb bt命令顯示當前進程(或線程)所在的堆棧。實際代碼開發中,一般較少需要得到當前程序的堆棧。但是,在調試一些不容易復現、gdb難以跟蹤的bug時,或在需要記錄部分代被執行的上下文的情景下,就非常有必要得到當前程序運行的堆棧。
2. 原理
要實現上面得到當前程序堆棧的功能,需要依賴glibc中execinfo.h聲明的backtrace()函數族。爲此我們需要先了解glibc中backtrace是如何實現的。
2.1 底層庫的實現
可以參考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)
#endif
int
__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)
2.2 用戶態的使用過程
這樣,程序開發者就可以直接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裏面去。
3. 實例分析
3.1 測試程序及其輸出
具體示例如下execinfo.c:
#include <stdio.h>
#include <execinfo.h>
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)
.......
3.2 反彙編二進制及其分析
我們接着對execinfo反彙編: objdump -alDS ./execinfo >> execinfo.S,得到execinfo.S之後,
看看0x4005cd 對應到哪一行代碼:
/home/qxi/test/execinfo.c:34
backtrace(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 <backtrace@plt>
/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 <main+0x7b>
/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()函數之後執行的第一條指令的位置,也就是最早入棧的值。
4. 總結
函數調棧中壓棧的值,記錄着當前調用返回後會執行的下一個指令(函數)的地址. 結合上面的原理和示例分析,可以看到在應用程序中得到當前程序的調用棧的過程,就是把函數調用過程中一層層入棧的值,從棧頂一個個再次讀出的過程。因此利用這個特性,再結合一些其他技術,我們可以用來實現跟蹤資源泄漏、鎖申請而沒有釋放等高級功能。