C语言中的函数扮演者很重要的角色,函数可以使程序更简洁,可读性更
好,更便于复用,在实际运用中,很多程序基本都是由许多函数组成的,
为了更好的使用函数,我们需要对函数做一个深度的剖析。而理解函数的关键在于“栈帧”二字,函数的调用实际就是栈帧的创建与释放过程,
在研究函数调用过程之前,先介绍几个关键词。
内存,c/c++编译的程序占用内存分布如下图
寄存器EIP:存储当前CPU正在执行指令的下一条指令;
通用寄存器:eax,ebx,存储数据;
寄存器ebp:保存的是栈底地址,称为帧指针;
寄存器esp:保存的是栈顶地址,称为栈指针:
栈指针和帧指针一次只能存储一个地址,所以,任何时候,这一对指针
指向的是同一个函数的栈帧。在函数执行过程中,栈指针esp会随着数 据
的入栈和出栈而移动,因此函数中对大部分数据的访问都基于帧指针ebp
进行。即esp负责数据的存储与丢弃,而ebp负责数据的读取.要详细了解函数调用过程,必须得对应相应的汇编代码。
下面以一个简单程序为例,来深入研究函数的调用过程。
这里写代码片
#include <stdio.h>
#include <windows.h>
int Add(int x,int y)
{
int z=0;
z=x+y;
return z;
}
int main()
{
int a=0xAAAAAAAA;
int b=0xBBBBBBBB;
int c=Add(a,b);
printf("%c",c);
system("pause");
r,eturn 0;
}
一 main函数栈帧创建
我们知道main函数其实也是函数,作为程序的入口,它在使用过程中也
是被调用的,main函数开始被mainCRTStartup函数调用。要展开main
函数的调用就得为main函数创建栈帧,下面先来看下main函数栈帧的创建过程
查看寄存器如下
二 ADD函数调用
main函数栈帧创建成功后,接下来就是Add函数的调用,参数传递过程如下
这里我们主要要明白两点,(1)call指令的两个功能,功能1是将main函
数的pc压入堆栈,因为pc指针里面放的始终是下一条要执行指令的地
址,这里把pc压入堆栈进行保护,是因为在执行fun函数的时候pc指针里
面放的将会是fun函数需要执行里面的内容,同样的道理,我们知道某一
时刻只能有一个栈帧结构,当要去到fun函数的时候,寄存器ebp就要存
储fun函数的帧指针,所以main函数的帧指针就要保存起来,即图
中“main ret”,功能二是跳转到目标函数函数地址(见图中蓝色标记)
查看main函数的汇编语言也可以详细的看到这一点。
1.将当前正在执行的指令的下一条指令地址压入栈中
2 跳到目标函数地址处
三 Add函数代码执行处
按F11得
再按f11就进入Add函数内部
过程详解如下:
此时已经进入Add函数内部,开始形成Add函数自己的栈帧
首先将ebp压入栈中(ebp是寄存器,在CPU中,栈在内存里)
压ebp最终目的是为了形成新的栈帧,压入后ebp成为新栈帧的栈底,
而后,执行 mov esp-8语句,将esp往下移动八个字节,此时Add函数
栈帧空间创建成功,如图
所以以后在Add定义的变量按理来说都应该在Add函数创建的栈帧里面。
栈帧创建成功以后,开始执行Add函数内的操作,继续看汇编指令,(中
间其他准备操作指令跳过)
该指令是将a放入寄存器eax中,继续往下走,可以看到此时CPU将a与b相加,把值再放入寄存器eax中,即本来eax里面放的是a值:AAAAAAAA,
执行语句后放的是+b的和,
而后执行下一条语句,将eax的值保存在变量z中(变量z是之前定义在
Add栈帧中的临时变量),此时Add函数内部操作执行完毕。下一步开始
执行函数返回值的操作。开始将z的值放入eax寄存器里,让eax将返回值
带回去,Z会随着Add函数栈帧的释放而消失。)
四 返回值部分
esp移到和ebp相同的位址,销毁Add函数的栈帧。接着“ebp” 弹出,
将存储内容”main ebp“转移到ebp中,回到main函数栈帧里面,同
时add esp向上移4字节至存储“main ebp”的起始位置处。程序
执行下一句,使得ebp不动,esp继续上移4字节到“main ret,
继续F11遇到ret指令,将main ret中地址保存在esp中,并跳到该
地址处,再按F11将eax中值给c。如此,函数返回值 z 就被带到main函
数所求值 c里面。详细过程如下图
至此,一个函数的调用过程完成。得出以下结论
- 栈是往低(小)地址方向扩展的,而esp指向当前栈顶处的元素。通过使用push和pop指令我们可以把数据压入栈中或从栈中弹出。
- 指令CALL和RET用于处理函数调用和返回操作。 调用指令CALL的作用是把返回地址压入栈中并且跳转到被调用函数开始处执行。返回地址是程序中紧随调用指令CALL后面一条指令的地址。因此当被调函数返回时就会从该位置继续执行。返回指令RET用于弹出栈顶处的地址并跳转到该地址处。在使用该指令之前,应该先正确处理栈中内容,使得当前栈指针所指位置内容正是先前CALL指令保存的返回地址。可以发现,CALL和RET指令均完成两项工作,从栈中把返回地址存储或恢复,跳转PC到指定位置,可以说,两者的操作是相反的。若返回值是一个整数或一个指针,那么寄存器eax将被默认用来传递返回值。
- .函数临时变量在自己的栈帧里,生命周期随栈帧存在而存在,随栈帧释放而消失,
- 常规下,函数返回值存放在寄存器里,由寄存器返回相关参数;
- 形参室例化 顺序从右向左,如例中c=add(a+b),先在Add函数中创建b再创建a.重点内容