前言
函数是c语言的基本模块,我们编写的c语言程序都是由一个或多个函数组成的,而当我们调用一个函数时是需要在栈区中开辟一段空间的,那么这段空间是如何开辟的,函数调用结束后又是如何返回上一级函数的呢?
首先我们先编写一段简单的代码,是用函数实现两数求和。
代码如下:代码的实现是在vs2013中实现的
#include <stdio.h>
int Add(int i_number_one, int i_number_two); //求和函数的声明
int main(void)
{
//声明两个变量
int i_number_one = 20;
int i_number_two = 30;
int i_sum = 0;
//调用求和函数
i_sum = Add(i_number_one, i_number_two);
//打印结果
printf("%d", i_sum);
getchar(); //代码的实现是在vs2013中实现的,getchar()为了让程序运行框不是
//一闪而过
return 0;
}
//Add函数的实现
int Add(int i_number_one, int i_number_two)
{
int i_sum = 0;
i_sum = i_number_one + i_number_two;
return i_sum;
}
我们用F10打开程序调试,然后在打开反汇编窗口和调用堆栈窗口,这两个窗口在vs的菜单栏中的调试->窗口下可以选择
调用堆栈的功能是用来记录函数的调用过程,从中我们会发现当程序在执行main函数时已经运行了很多的代码,其实这是c语言编译器内置的代码,是运行程序必备的,从中我们也可以得出一个结论,main函数也是一个被调用的函数
我们都知道我们编写的c程序在开始运行的时候,要进行 预处理 -> 编译 -> 反汇编 -> 链接 这四个过程,而反汇编窗口顾名思义就是对程序反汇编过程的显性体现。
而我们今天的关键问题-函数栈帧的创建与销毁是如何进行的,通过我们对反汇编代码的解读就会体现出来!
1.ebp和esp的作用
我们可以看到程序运行的第一个汇编指令为 push ebp,是将通用寄存器中的 bp 寄存器压栈 ,然后就是 mov ebp,esp 是将sp寄存器中的值传给bp寄存器,这时我们就会有疑问,为什么c程序开始运行main()函数时要先使用两个寄存器?这两个寄存器是干什么用的呢?
函数的调用是要在栈区开辟一段空间的,而这段空间是通过esp和ebp这两个寄存器来维护,ebp中存放这段空间的栈底的地址,而esp是用来存放栈顶的的地址。
我们知道c程序的开始运行是从main函数开始的,那为什么我们在程序开始运行的时候,反而先压栈了一个栈底地址?(向栈空间中压入了一个栈底的地址)
这也恰恰验证了c语言中main函数其实也是一个被调用的函数,当main运行完了后还要返回到调用main()函数的函数中。
所以我们的程序的栈区示意图应该为
开始调用main函数前:
开始调用main函数后:
然后是mov ebp,esp将esp此时的地址放到ebp寄存器中,此时ebp就和esp指向同一个地址,这个地址就是main函数栈空间的栈底空间。
然后后面的汇编代码为:
sub可以类比为c语言中的减法 ‘-’ 同样操作数都为两个:
sub esp,0E4h
这句话的意思是相当于: esp = esp - 0E4H(十六进制数) 可以理解为将esp向上移动数个字节的空间。
然后是
将ebx esi edi压入栈中,每次压栈esp就会减1,
然后就是
将ebp-0E4h这段空间的起始地址存入edi中,ecx中保存的是这段空间的字节个数,eax中保存的是对空间初始化的的值这个是随机值,编译器不同值不同,然后开始将从edi保存的起始地址开始向下39h个字节初始化为eax中的值。
通过调试窗口中的内存我们可以清楚的看到这个过程
然后就是:
然后就是:
在main函数栈帧的上边开辟空间放两个参数的值
这汇编代码是记录下一条代码的值
然后程序的执行就会进入到Add函数中
这里Add函数的开辟栈空间的方法同main函数一样,不在过多的赘述
主要看Add函数的销毁
首先将想要return回main的函数放到eax中,然后将edi,esi,ebx出栈,每次出栈esp要减1,然后将Add函数的栈底地址给esp,然后Pop ebp,此时程序为Add函数所开辟的栈帧空间释放了,然后将之前记录的main函数的栈底空间,给ebp,ret是子程序返回指令,回到存储的下一条指令的地址的位置。
然后再将esp加8,意思也就是让esp指向main的空间中的edi的地址,此时程序的执行返回main函数中,开始执行其他剩余的代码,main的栈空间的销毁同Add函数的相同
此时我们也就明白了,函数栈帧的创建与销毁了!