函数调用栈的获取原理分析【转】

简介:

转自:http://hutaow.com/blog/2013/10/15/dump-stack/

上一篇文章《在Linux程序中输出函数调用栈》,讲述了在Linux中如何利用backtrace获取调用栈,本篇文章主要介绍一下获取函数调用栈的原理,并给出相应的实现方式。

要了解调用栈,首先需要了解函数的调用过程,下面用一段代码作为例子:

#include <stdio.h>

int add(int a, int b) { int result = 0; result = a + b; return result; } int main(int argc, char *argv[]) { int result = 0; result = add(1, 2); printf("result = %d \r\n", result); return 0; } 

使用gcc编译,然后gdb反汇编main函数,看看它是如何调用add函数的:

(gdb) disassemble main 
Dump of assembler code for function main: 0x08048439 <+0>: push %ebp 0x0804843a <+1>: mov %esp,%ebp 0x0804843c <+3>: and $0xfffffff0,%esp 0x0804843f <+6>: sub $0x20,%esp 0x08048442 <+9>: movl $0x0,0x1c(%esp) # 给result变量赋0值 0x0804844a <+17>: movl $0x2,0x4(%esp) # 将第2个参数压栈(该参数偏移为esp+0x04) 0x08048452 <+25>: movl $0x1,(%esp) # 将第1个参数压栈(该参数偏移为esp+0x00) 0x08048459 <+32>: call 0x804841c <add> # 调用add函数 0x0804845e <+37>: mov %eax,0x1c(%esp) # 将add函数的返回值赋给result变量 0x08048462 <+41>: mov 0x1c(%esp),%eax 0x08048466 <+45>: mov %eax,0x4(%esp) 0x0804846a <+49>: movl $0x8048510,(%esp) 0x08048471 <+56>: call 0x80482f0 <printf@plt> 0x08048476 <+61>: mov $0x0,%eax 0x0804847b <+66>: leave 0x0804847c <+67>: ret End of assembler dump. 

可以看到,参数是在add函数调用前压栈,换句话说,参数压栈由调用者进行,参数存储在调用者的栈空间中,下面再看一下进入add函数后都做了什么:

(gdb) disassemble add
Dump of assembler code for function add: 0x0804841c <+0>: push %ebp # 将ebp压栈(保存函数调用者的栈基址) 0x0804841d <+1>: mov %esp,%ebp # 将ebp指向栈顶esp(设置当前函数的栈基址) 0x0804841f <+3>: sub $0x10,%esp # 分配栈空间(栈向低地址方向生长) 0x08048422 <+6>: movl $0x0,-0x4(%ebp) # 给result变量赋0值(该变量偏移为ebp-0x04) 0x08048429 <+13>: mov 0xc(%ebp),%eax # 将第2个参数的值赋给eax(准备运算) 0x0804842c <+16>: mov 0x8(%ebp),%edx # 将第1个参数的值赋给edx(准备运算) 0x0804842f <+19>: add %edx,%eax # 加法运算(edx+eax)结果保存在eax中 0x08048431 <+21>: mov %eax,-0x4(%ebp) # 将运算结果eax赋给result变量 0x08048434 <+24>: mov -0x4(%ebp),%eax # 将result变量的值赋给eax(eax将作为函数返回值) 0x08048437 <+27>: leave # 恢复函数调用者的栈基址(pop %ebp) 0x08048438 <+28>: ret # 返回(准备执行下条指令) End of assembler dump. 

进入add函数后,首先进行的操作是将当前的栈基址ebp压栈(此栈基址是调用者main函数的),然后将ebp指向栈顶esp,接下来再进行函数内的处理流程。函数结束前,会将函数调用者的栈基址恢复,然后返回准备执行下一指令。这个过程中,栈上的空间会是下面的样子:

函数调用过程中栈的情况

可以发现,每调用一次函数,都会对调用者的栈基址(ebp)进行压栈操作,并且由于栈基址是由当时栈顶指针(esp)而来,会发现,各层函数的栈基址很巧妙的构成了一个链,即当前的栈基址指向下一层函数栈基址所在的位置,如下图所示:

调用栈中各层函数栈基址间的关系

了解了函数的调用过程,想要回溯调用栈也就很简单了,首先获取当前函数的栈基址(寄存器ebp)的值,然后获取该地址所指向的栈的值,该值也就是下层函数的栈基址,找到下层函数的栈基址后,重复刚才的动作,即可以将每一层函数的栈基址都找出来,这也就是我们所需要的调用栈了。

下面是根据原理实现的一段获取函数调用栈的代码,供参考。

#include <stdio.h>

/* 打印调用栈的最大深度 */
#define DUMP_STACK_DEPTH_MAX 16

/* 获取寄存器ebp的值 */
void get_ebp(unsigned long *ebp) { __asm__ __volatile__ ( "mov %%ebp, %0" :"=m"(*ebp) ::"memory"); } /* 获取调用栈 */ int dump_stack(void **stack, int size) { unsigned long ebp = 0; int depth = 0; /* 1.得到首层函数的栈基址 */ get_ebp(&ebp); /* 2.逐层回溯栈基址 */ for (depth = 0; (depth < size) && (0 != ebp) && (0 != *(unsigned long *)ebp) && (ebp != *(unsigned long *)ebp); ++depth) { stack[depth] = (void *)(*(unsigned long *)(ebp + sizeof(unsigned long))); ebp = *(unsigned long *)ebp; } return depth; } /* 测试函数 2 */ void test_meloner() { void *stack[DUMP_STACK_DEPTH_MAX] = {0}; int stack_depth = 0; int i = 0; /* 获取调用栈 */ stack_depth = dump_stack(stack, DUMP_STACK_DEPTH_MAX); /* 打印调用栈 */ printf(" Stack Track: \r\n"); for (i = 0; i < stack_depth; ++i) { printf(" [%d] %p \r\n", i, stack[i]); } return; } /* 测试函数 1 */ void test_hutaow() { test_meloner(); return; } /* 主函数 */ int main(int argc, char *argv[]) { test_hutaow(); return 0; } 

源文件下载:链接

执行gcc dumpstack.c -o dumpstack编译并运行,执行结果如下:

 Stack Track: 
 [0] 0x8048475 
 [1] 0x8048508 
 [2] 0x804855c 
 [3] 0x804856a

Comments

comments powered by Disqus






本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/sky-heaven/p/7654975.html,如需转载请自行联系原作者

相关文章
|
7天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
72 9
|
1天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
4天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
6天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
28 4
|
10天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
29天前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
26 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
29天前
初步认识栈和队列
初步认识栈和队列
57 10
|
23天前
数据结构(栈与列队)
数据结构(栈与列队)
16 1
|
29天前
|
算法
数据结构与算法二:栈、前缀、中缀、后缀表达式、中缀表达式转换为后缀表达式
这篇文章讲解了栈的基本概念及其应用,并详细介绍了中缀表达式转换为后缀表达式的算法和实现步骤。
42 3
|
28天前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
63 1