目录
前言
在C语言编写时,我们总会把一些功能单独写成一个函数,在主函数中调用,只需要在调用时通过函数名将实参传给形参就实现了整个函数调用过程,但实际的调用过程底层很复杂,这其中关系到函数栈帧。
函数栈帧是什么?
栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息,比如该函数的返回地址和局部变量。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
内存分区
栈区:从高地址向低地址延伸的,主要用来存放局部变量,函数调用开辟的空间,与堆共享一段空间。
堆区:由低地址向高地址增长,动态开辟的空间就在这里(malloc,realloc,calloc,free),与栈共享一段空间。
静态区:主要存放全局变量和静态变量。
寄存器
ebp | ebp是基址指针,保存调用者函数的地址,总是指向当前栈帧栈底 |
esp | esp是被调函数指针,总指向函数栈栈顶 |
esx | 累加器,用来乘除法,与函数返回值(本篇主要关注第二个功能) |
eax | 通用寄存器,保存临时数据,常用于返回值 |
eip | 指令寄存器,保存当前指令的下一条指令的地址 |
汇编指令
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
简单来讲,esp和ebp是两个指针ebp指向当前栈帧栈底,esp指向函数栈栈顶。
栈帧创建与销毁过程
#define _CRT_SECURE_NO_WARNING
#include<stdio.h>
#include<stdlib.h>
int Add(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("ret = %d\n", ret);
system("pause");
return 0;
}
函数执行之前的准备工作
将ADD函数需要的参数a=10和b=20入栈
函数执行
保护现场(保护ebp)
由于马上要建立新的栈帧,因此对ebp和esp都得变动,为了在调用add函数后能将ebp还原到初始位置,因此需要对ebp进行保护,即将ebp的值压入栈。
创建调用函数的栈帧空间
令ebp指向当前的esp位置,并且创建一块合适大小的空间
保存局部变量
将ADD函数创建的变量int c=0放入开辟的栈帧空间
进行运算
执行c=a+b
函数执行结束,进行函数返回
存储返回值
达到目的ADD(a,b),现在我们希望回到main函数中继续往下执行,所以要对ADD函数桢进行销毁,但是main'函数还没有拿到ADD的返回值,此时就是前面提到的eax寄存器发挥作用,我们将返回值存储到eax寄存器中。
ebp回到上一个栈底
此时ebp拿到之间存储的上一栈帧栈底的值,回到相应的位置,于此同时,存储的ebp没有用了,也将被销毁。
销毁形参
形参进行销毁(所以,形参的改变不会影响实参,因为地址不同)
回到上一栈帧
main函数拿到返回值,此时注意,这个上一栈帧代表的是什么,我们直到main其实也是一个函数,所以也有自己的栈帧,所以说这个上一栈帧就是main函数的栈帧,所以此时main函数的sum拿到eax的值,所以说,我们只有一个寄存器,因此C语言函数只能由一个返回值。
查看汇编指令
int Add(int a, int b)
{
006D1820 push ebp //push指令会压入ebp寄存器
006D1821 mov ebp,esp //move指令会把esp的值存放到ebp中,相当于产生了add函数的栈帧
006D1823 sub esp,0C0h //这里大可先不必多研究
006D1829 push ebx
006D182A push esi
006D182B push edi
return a + b;
006D182C mov eax,dword ptr [a]
006D182F add eax,dword ptr [b]
}
int Add(int a, int b)
{
00681800 push ebp //push指令会压入ebp寄存器
00681801 mov ebp,esp //move指令会把esp的值存放到ebp中,相当于产生了add函数的栈帧
00681803 sub esp,0CCh //这里大可先不必多研究
00681809 push ebx //将寄存器ebx的值压栈
0068180A push esi //将寄存器esi的值压栈
0068180B push edi //将寄存器esi的值压栈
int t = 0;
0068180C mov dword ptr [t],0
t = a + b;
00681813 mov eax,dword ptr [a] //exa通用寄存器,保留临时数据a
00681816 add eax,dword ptr [b] //exa通用寄存器,保留临时数据a
00681819 mov dword ptr [t],eax //把exa寄存器的值交给t
return t;
0068181C mov eax,dword ptr [t] //将t的值交给eax寄存器
}
创建add函数栈帧,创建临时变量,计算后将结果存在eax,由eax返回,销毁add栈帧。