在x86的计算机系统中,内存空间中的栈主要用于保存函数的参数,返回值,返回地址,本地变量等。一切的函数调用都要将不同的数据、地址压入或者弹出栈。因此,为了更好地理解函数的调用,我们需要先来看看栈是怎么工作的。

1、栈是什么?

简单来说,栈是一种LIFO形式的数据结构,所有的数据都是后进先出。这种形式的数据结构正好满足我们调用函数的方式:父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。

这里是一个push操作的例子。假设我们有一个栈,其中黄色部分是已经写入数据的区域,绿色部分是还未写入数据的区域。现在我们将0x50压入栈中:

// 将0x50的压入栈
push $0x50

图一:压栈操作

我们再来看看pop操作的例子:

// 将0x50弹出栈
pop

图二:出栈操作

这里有两点需要注意的,第一,上面例子中栈的生长方向是从高地址到低地址的,这是因为在下文讲的栈帧中,栈就是向下生长的,因此这里也用这种形式的栈;第二,pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问。有了这些栈的基本知识,我们现在可以来看看在x86-32bit系统下,C语言函数是如何调用的了。

2、栈帧是什么?

栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。在x86-32bit中,我们用 %ebp 指向栈底,也就是基址指针;用 %esp 指向栈顶,也就是栈指针。下面是一个栈帧的示意图:

图三:栈帧示意图

一般来说,我们将 %ebp 到 %esp 之间区域当做栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中,

1)“调用者”需要知道在哪里获取“被调用者”返回的值;

2)“被调用者”需要知道传入的参数在哪里,

3)返回的地址在哪里。同时,我们需要保证在“被调用者”返回后,%ebp%esp 等寄存器的值应该和调用前一致。因此,我们需要使用栈来保存这些数据。

3、函数调用实例

3.1 函数的调用

我们直接通过实例来看函数是如何调用的。这是一个有参数但没有调用任何函数的简单函数,我们假设它被其他函数调用。

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) 我们便可以给 ab 和 c 赋值了。

图五:本地变量赋值后的栈帧

3.2 函数的返回

上面讲的都是函数的调用过程,我们现在来看看函数是如何返回的。从下面这个例子我们可以看出,和调用函数时正好相反。当函数完成自己的任务后,它会将 %esp 移到 %ebp 处,然后再弹出旧的 %ebp 的值到 %ebp。这样,%ebp 就恢复到了函数调用前的状态了。

int MyFunction( int x, int y, int z )
{int a, int b, int c;...return;
}

其汇编大致如下:

_MyFunction:push %ebpmovl %esp, %ebpmovl -12(%esp), %esp...mov %ebp, %esppop %ebpret

我们注意到最后有一个 ret 指令,这个指令相当于 pop + jum。它首先将数据(返回地址)弹出栈并保存到 %eip 中,然后处理器根据这个地址无条件地跳到相应位置获取新的指令。

图六:被调用者返回后的栈帧

总结

到这里,C函数的调用过程就基本讲完了。函数的调用其实不难,只要搞懂了如何保存以及还原 %ebp 和 %esp,就能明白函数是如何通过栈帧进行调用和返回的了。

参考

1. x86 Instruction Set Reference 
2. x86 Disassembly/Functions and Stack Frames 
3. x86 Assembly Guide

4. C函数调用过程原理及函数栈帧分析

5. 浅析函数的调用过程

C函数调用过程原理及函数栈帧分析相关推荐

  1. 【软件开发底层知识修炼】二十三 ABI-应用程序二进制接口三之深入理解函数栈帧的形成与摧毁

    上两篇文章我们初步接触了ABI-应用程序二进制接口的概念,点击链接查看上一篇文章:[软件开发底层知识修炼]二十二 ABI-应用程序二进制接口 二.了解了为什么会有ABI的存在.本篇文章继续学习ABI ...

  2. 函数调用过程详解:函数栈帧的创建与销毁

    前言:我们在学习C语言的过程中,可以会产生很多疑问,比如: 局部变量是怎么创建的 为什么局部变量的值不做初始化就是随机值 函数是怎么传参的?传参的顺序是怎么样的? 形参和实参是什么关系? 函数调用是怎 ...

  3. 【深入理解函数栈帧:探索函数调用的内部机制】

    本章我们要介绍的不是数学中的函数,而是C语言中的函数哟! 本章重点 了解汇编指令 深刻理解函数调用过程 样例代码: #include <stdio.h> int MyAdd(int a, ...

  4. 程序员内功修炼——函数栈帧的创建与销毁

    一.什么是函数的栈帧 c语言是由函数构成的,那么函数是如何进行传参的?如何调用的?如何返回值的?这些问题与函数的栈帧有关. 函数栈帧:就是函数调用过程中程序的调用栈所开辟的空间,这些空间用来存放: 1 ...

  5. 函数栈帧的创建与销毁

    目录 前言 一.预备知识 1.内存区域的划分和分配 2.栈帧简介 3.寄存器简介 二.函数栈帧介绍 1.源代码 2.如何查看汇编代码 3.函数栈帧的创建与销毁(重点) 三.小彩蛋 总结 前言 最近在学 ...

  6. C语言内功修炼之函数栈帧的创建与销毁(举例加图解)

    大家可能会函数栈帧不了解,可能都没有听过这个,不用着急,在理解函数栈帧之前,我们先来了解一下程序对内存使用的分区大概情况:  区域 作用 栈区(stack) 由编译器自动分配和释放,存放函数的参数值, ...

  7. 函数栈帧(详细图解)

    目录 一.栈 二.常用寄存器及简单汇编指令 三.理解栈帧 3.1 main函数栈帧创建 3.1.1 main函数栈帧创建动态演示 3.2 局部变量创建 3.2.1 局部变量创建动态演示 3.3 函数传 ...

  8. (动图详解)汇编视角观察函数栈帧的创建和销毁

    目录 ​1.阅读本文的价值 ​2.函数栈帧及栈的概念 ​3.部分寄存器及汇编指令 ​4.main函数的调用 5.main函数的栈帧创建 ​6.变量的栈帧创建 ​6.函数传参 ​7.函数内部运算及销毁 ...

  9. 内功修炼《函数栈帧的创建和销毁》建议收藏

    文章目录 前言 一. 寄存器的概念 二. 通用寄存器的结构 三. 指针寄存器和变址寄存器 四. EBP和ESP 五.总结 前言 在前期的学习过程中,我们可能会有很多的困惑: 1️⃣ 局部变量是怎么创建 ...

最新文章

  1. linux 创建crontab文件位置,[基础教程]linux系统的crontab计划任务添加和删除
  2. mysql+xml+注释,springboot整合mybatis完整示例, mapper注解方式和xml配置文件方式实现(我们要优雅地编程)...
  3. springMVC 前台向后台传数组
  4. 数据挖掘中的概念描述
  5. Conway#39;s law(康威定律)
  6. php+原生代码+赋值,js重新赋值原生的方法
  7. Adobe illustrator 论文图形编辑和排版 - 连载 1
  8. python绘制糖葫芦_python简单爬虫(一)
  9. Android中ButterKnife的详细使用
  10. 当物联网遇上云原生:K8s向边缘计算渗透中
  11. java ceilingentry_java.util.TreeMap.ceilingKey()方法实例
  12. 刷题总结——art2(ssoj)
  13. 【已解决】ModuleNotFoundError: No module named ‘web’的解决办法:
  14. android 数据持久化——读写SD卡中的内容
  15. sim卡iccid编辑软件_一篇文章扫盲手机SIM卡相关知识
  16. 解决Win7下JMF读取摄像头错误
  17. matlab遗传算法工具箱介绍和详细使用方法【matlab优化算法工具箱】
  18. DeviceOrientation ---实现指南针
  19. ones/测试工程师面试
  20. JPA中@Basic注解详解

热门文章

  1. n步自举法:时序差分方法与蒙特卡洛方法的结合
  2. 加密算法使用(五):RSA使用全过程
  3. AFNetWorking 之 网络请求的基本知识
  4. 常用应用层传输协议和端口
  5. 强大的日志分析工具AWStats经典备忘
  6. Stanford Parser的学习之开篇(一)
  7. alert中文信息时乱码,html页面和script标签中均已设置为utf-8依然乱码
  8. linux 动态内存分配,具体来说,fork()如何处理Linux中malloc()动态分配的内存?
  9. JAXB处理java对象与xml格式之间的转换
  10. 财务报表五力、五性分析雷达图