C函数调用过程原理及函数栈帧分析(转)
在x86的计算机系统中,内存空间中的栈主要用于保存函数的参数,返回值,返回地址,本地变量等。一切的函数调用都要将不同的数据、地址压入或者弹出栈。因此,为了更好地理解函数的调用,我们需要先来看看栈是怎么工作的。
栈是什么?
简单来说,栈是一种LIFO形式的数据结构,所有的数据都是后进先出。这种形式的数据结构正好满足我们调用函数的方式:父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。
这里是一个push操作的例子。假设我们有一个栈,其中黄色部分是已经写入数据的区域,绿色部分是还未写入数据的区域。现在我们将0x50压入栈中:
// 将0x50的压入栈
push $0x50

我们再来看看pop操作的例子:
// 将0x50弹出栈
pop

这里有两点需要注意的,第一,上面例子中栈的生长方向是从高地址到低地址的,这是因为在下文讲的栈帧中,栈就是向下生长的,因此这里也用这种形式的栈;第二,pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问。有了这些栈的基本知识,我们现在可以来看看在x86-32bit系统下,C语言函数是如何调用的了。
栈帧是什么?
栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。在x86-32bit中,我们用 %ebp
指向栈底,也就是基址指针;用 %esp
指向栈顶,也就是栈指针。下面是一个栈帧的示意图:

一般来说,我们将 %ebp
到 %esp
之间区域当做栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中,1)“调用者”需要知道在哪里获取“被调用者”返回的值;2)“被调用者”需要知道传入的参数在哪里,3)返回的地址在哪里。同时,我们需要保证在“被调用者”返回后,%ebp
, %esp
等寄存器的值应该和调用前一致。因此,我们需要使用栈来保存这些数据。
函数调用实例
函数的调用
我们直接通过实例来看函数是如何调用的。这是一个有参数但没有调用任何函数的简单函数,我们假设它被其他函数调用。
int MyFunction(int x, int y, int z)
{
int a, b, c;
a = 10;
b = 5;
c = 2;
...
}
int TestFunction()
{
int x = 1, y = 2, z = 3;
MyFunction1(1, 2, 3);
...
}
对于这个函数,当调用时,MyFunction()
的汇编代码大致如下:
_MyFunction:
push %ebp ; //保存%ebp的值
movl %esp, $ebp ; //将%esp的值赋给%ebp,使新的%ebp指向栈顶
movl -12(%esp), %esp ; //分配额外空间给本地变量
movl $10, -4(%ebp) ;
movl $5, -8(%ebp) ;
movl $2, -12(%ebp) ;
光看代码可能还是不太明白,我们先来看看此时的栈是什么样的:

此时调用者做了两件事情:第一,将被调用函数的参数按照从右到左的顺序压入栈中。第二,将返回地址压入栈中。这两件事都是调用者负责的,因此压入的栈应该属于调用者的栈帧。我们再来看看被调用者,它也做了两件事情:第一,将老的(调用者的) %ebp
压入栈,此时 %esp
指向它。第二,将 %esp
的值赋给 %ebp
, %ebp
就有了新的值,它也指向存放老 %ebp
的栈空间。这时,它成了是函数 MyFunction()
栈帧的栈底。这样,我们就保存了“调用者”函数的 %ebp
,并且建立了一个新的栈帧。
只要这步弄明白了,下面的操作就好理解了。在 %ebp
更新后,我们先分配一块0x12字节的空间用于存放本地变量,这步一般都是用 sub
或者 mov
指令实现。在这里使用的是 movl
。通过使用 mov
配合 -4(%ebp)
, -8(%ebp)
和 -12(%ebp)
我们便可以给 a
, b
和 c
赋值了。

函数的返回
上面讲的都是函数的调用过程,我们现在来看看函数是如何返回的。从下面这个例子我们可以看出,和调用函数时正好相反。当函数完成自己的任务后,它会将 %esp
移到 %ebp
处,然后再弹出旧的 %ebp
的值到 %ebp
。这样,%ebp
就恢复到了函数调用前的状态了。
int MyFunction( int x, int y, int z )
{
int a, int b, int c;
...
return;
}
其汇编大致如下:
_MyFunction:
push %ebp
movl %esp, %ebp
movl -12(%esp), %esp
...
mov %ebp, %esp
pop %ebp
ret
我们注意到最后有一个 ret
指令,这个指令相当于 pop + jum
。它首先将数据(返回地址)弹出栈并保存到 %eip
中,然后处理器根据这个地址无条件地跳到相应位置获取新的指令。

总结
到这里,C函数的调用过程就基本讲完了。函数的调用其实不难,只要搞懂了如何保存以及还原 %ebp
和 %esp
,就能明白函数是如何通过栈帧进行调用和返回的了。希望这篇文章对你有帮助!
引用
在我学习栈帧以及写这篇文章的过程中,参考了下面这些文章,在这我感谢他们对我提供的大力的帮助。如果你对这些文章感兴趣,请访问以下链接:
1. x86 Instruction Set Reference
2. x86 Disassembly/Functions and Stack Frames
3. x86 Assembly Guide
C函数调用过程原理及函数栈帧分析(转)的更多相关文章
- c函数调用过程原理及函数栈帧分析
转载自地址:http://blog.csdn.net/zsy2020314/article/details/9429707 今天突然想分析一下函数在相互调用过程中栈帧的变化,还是想尽量以比 ...
- Cortex-M3双堆栈MSP和PSP+函数栈帧
为了防止几百年以后找不到该文章,特此转载 ------------------------------------------------开始转载--------------------------- ...
- 计算机是如何计算的、运行时栈帧分析(神奇i++续)
关于i++的疑问 通过JVM javap -c 查看字节码执行步骤了解了i++之后,衍生了一个问题: int num1=50; num1++*2执行的是imul(将栈顶两int类型数相乘,结果入栈), ...
- 函数调用过程中,函数参数的入栈顺序,why?
C语言函数参数入栈顺序为从右至左.具体原因为:C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数.通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底.除非知道参数个数,否则是无法通 ...
- MIPS架构上函数调用过程的堆栈和栈帧
转载于CSDN:http://blog.csdn.net/do2jiang/article/details/5404566 在计算机科学中,Call stack是指存放某个程序的正在运行的函数的信息的 ...
- 图文并茂-超详解 CS:APP: Lab3-Attack(附带栈帧分析)
CS:APP:Lab3-ATTACK 0. 环境要求 关于环境已经在lab1里配置过了.lab1的连接如下 实验的下载地址如下 说明文档如下 http://csapp.cs.cmu.edu/3e/at ...
- 自己动手实现arm函数栈帧回溯【转】
转自:http://blog.csdn.net/dragon101788/article/details/18668505 内核版本:2.6.14 glibc版本:2.3.6 CPU平台:arm gl ...
- C语言的函数调用过程(栈帧的创建与销毁)
从汇编的角度解析函数调用过程 看看下面这个简单函数的调用过程: int Add(int x,int y) { ; sum = x + y; return sum; } int main () { ; ...
- [Android Pro] 深入理解函数的调用过程——栈帧
cp :http://blog.csdn.net/x_perseverance/article/details/78897637 每一个函数被调用时,都会为函数开辟一块空间,这块空间就称为栈帧. 首先 ...
随机推荐
- 通过继承Thread类实现多线程
(1)继承Thread类(2)重写run(方法(3)通过start0方法启动线程 一定的缺点: Java中的类是单继承的,一旦继承了Thread类,就不允许再去继承其它的类 线程和主方法之间的执行顺序 ...
- Ubuntu中linux虚拟机全屏
登录客户机操作系统.在虚拟机中装载CD驱动器启动终端,使用tar解压缩安装程序,然后执行vmware-insall.pl安装VMware Tools. 1.进入文件界面,找到左侧“设备”右击“安装VM ...
- 安装nginx 以及nginx负载均衡
a 安装 [root@localhost ~]# yum -y install gcc automake autoconf libtool make yum install gcc gcc-c++ ...
- iOS的UILabel设置多行显示
label.lineBreakMode = NSLineBreakByWordWrapping; label.numberOfLines = ;
- 【NOIP2016提高组day1】换教室
题目 对于刚上大学的牛牛来说,他面临的第一个问题是如何根据实际情况申请合适的 课程. 在可以选择的课程中,有 2n 节课程安排在 n 个时间段上. 在第 i ( 1 ≤ i ≤ n )个 时间段上,两 ...
- 【NOIP2016提高A组模拟10.15】算循环
题目 分析 一步步删掉循环, 首先,原式是\[\sum_{i=1}^n\sum_{j=1}^m\sum_{k=i}^n\sum_{l=j}^m\sum_{p=i}^k\sum_{q=j}^l1\] 删 ...
- 【leetcode】1179. Reformat Department Table
题目如下: SQL Schema Table: Department +---------------+---------+ | Column Name | Type | +------------- ...
- Vue3.0 Function API---------引用
1.了解 Vue 3.0 是否有 break change,就像 Python 3 / Angular 2 一样? 不,100% 兼容 Vue 2.0,且暂未打算废弃任何 API(未来也不).之前有草 ...
- C#调用Python(一)
python文件中未引入其他包.模块 以下方法不适用于pyhton 文件有第三方包.模块,有第三方包,模块的实现方法,请戳这里→https://www.cnblogs.com/zhuanjiao/p/ ...
- shiro框架学习-5-自定义Realm
1. 自定义Realm基础 步骤: 创建一个类 ,继承AuthorizingRealm->AuthenticatingRealm->CachingRealm->Realm 重写授权方 ...