今天我们要学习的是函数栈帧的创建与销毁,学完这部分内容,我们可以解决下面的几个问题:

局部变量是怎么创建的?

为什么局部变量的值是随机值?

函数是怎么传参的?传参顺序是怎样的?

形参和实参是什么关系?

函数调用是怎么做的?

函数调用是怎么做的?

函数调用结束后是怎么返回的?

学习函数栈帧的创建与销毁不仅可以学习到这些知识,还能修炼自己的内功,也能搞懂后期更多的知识。

进入正题.
今天讲解的时候,使用的环境是VS2013,不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。

预备知识

在学习函数栈帧的创建与销毁之前,我们要了解以下寄存器:

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址

esp和ebp共同维护函数栈帧,程序调用哪个函数,这两个指针就去维护那个函数调用开辟的空间

常用的汇编指令:

  • mov:数据转移指令
  • push:数据入栈,同时esp栈顶寄存器也要发生改变
  • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • sub:减法命令
  • add:加法命令
  • call:函数调用,1. 压入返回地址 2. 转入目标函数
  • jump:通过修改eip,转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip命令

正文部分

我们以下面这个程序来分析:

#includeint Add(int x, int y){int z = 0;z = x + y;return z;}int main(){int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%d\n", c);return 0;}

在VS2013中,main函数也是被其他函数调用的,具体是:

main函数栈帧的创建

下面是反汇编代码:

我们已经知道,main函数是由其他函数调用的,在调用main函数前,已经为_tmainCRTStartup在栈区开辟了空间。

此时的情景:

进入main函数第一步:

进行压栈:

接着进行下一步操作:

将esp的值赋给ebp,因此ebp就移到了esp的位置,再将esp减一个八进制的数字0E4H,因此esp向上移动:

此时的情景变成了下图所示:

可以看出,esp和ebp已经在维护新的空间了,而这块空间就是为main函数开辟的

接下来是三个压栈的操作:

在压栈的同时,esp指针也会向上移动

接下来,将ebp+FFFFFF1CHF加载到edi里面,lea是加载的意思。

如果勾选上显示符号名,我们会惊奇的发现:

这步操作是将ebp-0E4H加载到edi里面,

接着进行下一步:

这几步的操作是:从edi开始向下的空间初始化为CCCCCCCC总共初始化ecx次,每次初始化8个字节;

到这里,为main函数栈帧的开辟就完成了

执行main函数内部代码

将变量a,b,c初始化:

这些步骤是在main函数栈帧中的存入变量a,b,c的值,并将其赋值。如果没有赋初始值,此时变量里的值仍是cccccccc,正好和”烫烫烫“的二进制编码一样,这就是为什么我们如果不初始化变量,可能在屏幕上输出”烫烫烫“的原因了。

Add函数栈帧的创建

前面4步执行的操作:

Add函数的执行

call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

Add函数的汇编代码:

前面这几步和main函数的前几步执行过程相似,是为Add函数准备栈帧

上面代码执行完的结果:

创建临时变量z:

执行相加操作:

ebp+8就是10的地址,ebp+12就是20的地址:

相加后,将z的值放在eax这个寄存器中, 寄存器是不会被销毁的,因此,当z被销毁后,z的值仍然保存了下来。

当执行完Add函数是,就要释放内存给Add函数分配的内存空间。

这三步将edi,esi, ebx 出栈,同时esp也增加会向下移动。

将esp赋给ebp,同时这部分的空间也会还给操作系统。

返回值的带回方法:将eax中存放的z的值赋给ebp-20h

main函数的空间销毁和Add函数类似,就不过多赘述了。