函数的调用过程(栈帧)

一、什么是栈帧?

栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每一个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每一个函数的每次调用,都有它本身独立的一个栈帧,这个栈帧中维持着所须要的各类信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。算法

二、Add()函数的调用过程

咱们以Add()函数为例深刻的研究一下函数的调用过程。
先看一段简单的代码:
 1 #include <stdio. h>
 2 int Add(int x, int y)
 3 {
 4 int z = 0;
 5 z = x + y;
 6 return z;
 7 }
 8 int main()
 9 {
10 int a = 10;
11 int b = 20;
12 int ret = Add(a, b) ;
13 printf("ret = %d\n", ret) ;
14 return 015 }

当讲程序调试的时候, 查看【调用堆栈】(按F10进入调试-窗口-调用堆栈,或按快捷键ctrl+alt+C) ,用VS2015调试 以下图:
数组

若是用版本更老的,或其余如VC6.0等编辑器则能够看到更多信息,VS2008调试如图:数据结构

咱们发现其实main函数在 __tmai nCRTStartup 函数中调用的,而 __tmai nCRTStartup 函数是在 mai nCRTStartup 被调用的。咱们知道每一次函数调用都是一个过程。这个过程咱们一般称之为: 函数的调用过程。这个过程要为函数开辟栈空间, 用于本次函数的调用中临时变量的保存、 现场保护。 这块栈空间咱们称之为函数栈帧。
而栈帧的维护咱们必须了解ebp和esp两个寄存器。 在函数调用的过程当中这两个寄存器存放了维护这个栈的栈底和栈顶指针。好比:调用main函数, 咱们为main函数分配栈帧空间, 那么栈帧维护以下:
ebp存放了指向函数栈帧栈底的地址。esp存放了指向函数栈帧栈顶的地址。
注意:ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不一样的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。框架

1 . 从main函数的地方开始, 要展开main函数的调用就得为main函数建立栈帧, 那咱们先来看main函数栈帧的建立。转到反汇编能够更清晰的看到过程:编辑器

过程分析:

a.首先mainCRTStartup(),__mainCRTStartup()函数的调用,调main()函数;函数

b.将ebp压栈处理,保存指向栈底的ebp的地址(方便函数返回以后的现场恢复),此时esp指向新的栈顶位置;spa

c.将esp的值赋给ebp,产生新的ebp;指针

d.给esp减去一个16进制数0E4H(为main函数预开辟空间);调试

e.push ebx、esi、edi;code

f.lea指令,加载有效地址;

g.初始化预开辟的空间为0xcccccccc;

h.建立变量a与b。

2. 接下来是Add函数的调用。

参数传递过程:

 过程分析:

a.将b存入寄存器eax,再将将eax压栈;(传参过程,从左向右传递)

b.将a存入寄存器ecx,再将将ecx压栈;

c.call指令的调用,先要压栈call指令下一条指令的 地址,而后跳转(push+jmp)到Add()函数的地方(__cdecl调用约定)。
执行call指令的时候按F11 , 来到了这里。
再按F11 就进入Add函数的执行代码处。Add函数栈帧的建立:

过程分析:

a.首先将main()函数ebp压栈处理,保存指向main()函数栈帧底部的ebp的地址(方便函数返回以后的现场恢复),此时esp指向新的栈顶位置;

b.将esp的值赋给ebp,产生新的ebp,即Add()函数栈帧的ebp;

c.给esp减去一个16进制数0E4H(为Add()函数预开辟空间);

d.push ebx、esi、edi;

e.lea指令,加载有效地址;

f.初始化预开辟的空间为0xcccccccc;

g.建立变量z;

h.获取形参的a和b再相加,将结果存储到z中;

i.将结果存储到eax寄存器,经过寄存器带回函数的返回值。
剩下的就是是函数返回部分:

过程分析:

a.pop3次,edi、esi、ebx依次出栈,esp 会向下移动;

b.将ebp赋给esp,使esp指向ebp指向的地方

c.ebp 出栈,将出栈的内容给ebp(即main()函数ebp),回到main()函数的栈帧;

d.ret 指令,出栈一次,并将出栈的内容当作地址,并跳转到该地址处(pop+jmp)。

注: 栈帧这部份内容在不一样的编译器上实现存在差别, 可是思想都是一致的。

栈帧的通常总结:

1. 堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间:
➢ 函数调用框架;
➢ 传递参数;
➢ 保存返回地址;
➢ 提供局部变量空间;
➢ 等等。
以x86体系结构为例
2. 堆栈寄存器和堆栈操做
 堆栈相关的寄存器
➢ esp,堆栈指针(stack pointer)
➢ ebp,基址指针(base pointer)
堆栈操做
➢ push 栈顶地址减小4个字节(32位)
➢ pop 栈顶地址增长4个字节
❖ ebp在C语言中用做记录当前函数调用基址
3. 利用堆栈实现函数调用和返回
❖其余关键寄存器
➢ cs : eip:老是指向下一条的指令地址
● 顺序执行:老是指向地址连续的下一条指令
● 跳转/分支:执行这样的指令的时候, cs : eip的值会根据程序须要被修改
● call:将当前cs : eip的值压入栈顶, cs : eip指向被调用函数的入口地址
● ret:从栈顶弹出原来保存在这里的cs : eip的值,放在cs : eip中
● 发生中断时???
4. 函数堆栈框架的造成

❖call xxx
➢执行call以前;
➢执行call时,cs:eip原来的值指向call下一条指令,该值被保存到栈顶,而后cs:eip的值指向xxx的入口地址
❖进入xxx
➢第一条指令:pushl %ebp
➢第二条指令:movl %esp,%ebp
➢函数体中的常规操做,压栈,出栈等
❖退出xxx
movl %ebp,%esp
popl %ebp
ret

5. 堆和栈的关系
咱们平时说的堆栈实际上是指栈,而实际上堆和栈是两种不一样的内存分配。简单罗列以下各方面的异同点。
1).堆须要用户在程序中显式申请,栈不用,由系统自动完成。申请/释放堆内存的API,在C中是malloc/free,在C++中是new/delete。申请与释放必定要配对使用,不然会形成内存泄漏(memory leak),长此以往系统就无内存可用了,出现OOM(Out Of Memory)错误。通常在return/exit或break/continue等语句时容易忘记释放内存,因此检查内存泄漏的代码时要关注这些语句,看它们前面是否有必要的释放语句free/delete。
2).堆的空间比较大,栈比较小。因此申请大的内存通常在堆中申请;栈上不要有较大的内存使用,好比大的静态数组;并且除非算法必要,不然通常不要使用较深的迭代函数调用,那样栈消耗内存会随着迭代次数的增长飞涨。
3).关于生命周期。栈较短,随着函数退出或返回,本函数的栈就完成了使用;堆就要看何时释放,生命周期就何时结束。
咱们发现解析Coredump仍是跟栈的关系相对紧密,跟堆的关系是有一种产
生Coredump的缘由是访问堆内存出错。

为何研究栈帧?看一个题目 :
在VC6.0环境中, 下面代码的结果是什么?

 1 #include <stdi o. h>
 2 void fun()
 3 {
 4 int tmp = 10;
 5 int *p = (int *) (*(&tmp+1) ) ;
 6 *(p-1) = 20;
 7 }
 8 int main()
 9 {
10 int a =0;
11 fun() ;
12 printf("a = %d\n", a) ;
13 return 0;
14 }

事实上在不一样平台下这段代码有不一样的输出,可自行验证。

相关文章
相关标签/搜索