如何得到当前程序执行的堆栈

简介:

如何得到当前程序执行的堆栈

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. 总结

函数调栈中压栈的值,记录着当前调用返回后会执行的下一个指令(函数)的地址. 结合上面的原理和示例分析,可以看到在应用程序中得到当前程序的调用栈的过程,就是把函数调用过程中一层层入栈的值,从栈顶一个个再次读出的过程。因此利用这个特性,再结合一些其他技术,我们可以用来实现跟踪资源泄漏、锁申请而没有释放等高级功能。













本文转自存储之厨51CTO博客,原文链接:http://blog.51cto.com/xiamachao/2064805 ,如需转载请自行联系原作者

相关文章
|
1月前
|
存储 SQL 安全
理解堆栈和内存溢出
【10月更文挑战第05天】
30 3
|
6月前
|
Linux
使用backtrace打印程序crash堆栈
使用backtrace打印程序crash堆栈
67 0
|
存储 Java
堆栈的区别是什么
堆和栈是计算机内存中两种不同的数据结构,它们用来存储程序运行时所需的数据。虽然堆和栈都是用于存储数据的,但它们在内存管理和数据访问方面有着明显的区别。下面我将详细解释堆和栈的区别。
225 0
|
存储 Linux C++
linux进程的堆栈空间_代码段(指令,只读)、数据段(静态变量,全局变量)、堆栈段(局部变量)、栈【转】
转自:http://blog.csdn.net/gongweijiao/article/details/8207333 原文参见:http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201215115845553/    一)概述   .堆栈是一个用户空间的内存区域,进程使用堆栈作为临时存储。
1108 0
|
Java 中间件 Unix
JVM:如何分析线程堆栈
英文原文:JVM: How to analyze Thread Dump 在这篇文章里我将教会你如何分析JVM的线程堆栈以及如何从堆栈信息中找出问题的根因。在我看来线程堆栈分析技术是Java EE产品支持工程师所必须掌握的一门技术。
1706 0
特殊堆栈
数据结构栈的使用
Java多线程-程序运行堆栈分析
class文件内容 class文件包含JAVA程序执行的字节码;数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符;文件开头有一个0xcafebabe(16进制)特殊的一个标志。 JVM运行时数据区 线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁 方法区 JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据。
959 0
|
C++ API 数据建模
Windbg查看调用堆栈(k*)
https://www.52pojie.cn/thread-664189-1-1.html       无论是分析程序崩溃原因,还是解决程序hang问题,我们最常查看的就是程序调用堆栈。
1874 0