函数栈帧的创建与销毁


函数栈帧的创建与销毁

函数栈帧的基本概念

c/c++程序中进行函数调用,需要在进程地址空间的栈区为这个函数申请一块栈空间,这一块栈空间称为函数的栈帧。除了内联函数之外,普通函数的调用都有创建函数栈帧和销毁函数栈帧的过程,函数栈帧的创建与销毁过程均在进程地址空间中进行,执行一个函数,需要CPU进行计算并执行相关指令。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XVo4Anpl-1669559349428)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127193253169.png)]

函数栈帧的创建与销毁过程

函数的调用过程是CPU执行相关指令的过程,在CPU中存在大量的寄存器,常用的寄存器有:

eax 通用寄存器,用于存放临时数据,例如函数的返回值
ebx 通用寄存器,存放临时数据
ebp 栈底寄存器,保存当前正在调用函数的栈帧的栈底
esp 栈顶寄存器,保存当前正在调用函数的栈帧的栈顶
eip 指令寄存器,指令寄存器中保存CPU当前正在执行的指令的下一条指令的地址,指令寄存器也称为PC指针

以下面的代码为例说明函数栈帧的创建与销毁过程:

#include<stdio.h>
int MyAdd(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int x = 0xA;
	int y = 0xB;
	int z = 0;
	z=MyAdd(x, y);
	printf("%d\n", z);
	return 0;
}

main函数为主函数,在Linux下,main函数是由__libc_start_main函数调用的,__libc_start_main函数由操作系统进行调用。

main函数调用

int x=0xA

这条语句转化为汇编指令为

mov dword ptr [ebp-8],0Ah

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OFc773kD-1669559349429)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127200424970.png)]

此时eip寄存器中存放mov dword ptr [ebp-14h],0Bh这条指令的地址

int y=0xB

这条语句转化为汇编指令为

mov dword ptr [ebp-14h],0Bh

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SNDZUF7G-1669559349430)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127200753738.png)]

此时eip寄存器中存放mov dword ptr [ebp-20h],0这条指令的地址。此时,局部变量x和y已经在main函数的栈帧中形成。

int z=0

这条语句转化为汇编指令为

mov dword ptr [ebp-20h],0

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w2iWflrb-1669559349430)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127201322039.png)]

此时eip寄存器中存放mov eax,dward ptr [ebp-14h]这条指令的地址。

可以看出,在栈上的变量地址不一定是连续的,可能是操作系统在设计的时候出于安全的考虑。

z=MyAdd(x,y)

z=MyAdd(x,y)这条语句对应多条汇编指令。

mov eax,dward ptr [ebp-14h]

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f0T24Mte-1669559349431)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127202107960.png)]

此时eip寄存器中存放push eax这条指令的地址

push eax

把eax的值压入栈中,同时栈顶寄存器的值发生相应变化。

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qObLnqg9-1669559349431)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127202439873.png)]

此时eip寄存器中存放mov ecx,dward ptr [ebp-8]这条指令的地址

mov ecx,dward ptr [ebp-8]

把ebp-8地址处的值拷贝一份到ecx通用寄存器中,实际上是将x的值拷贝到ecx中。

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VO6yZjZM-1669559349432)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127202648795.png)]

此时eip寄存器中存放push ecx这条指令的地址

push ecx

把ecx的值压入栈中,同时栈顶寄存器esp的值发生变化。

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Mgqe6I2-1669559349432)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127202759720.png)]

此时eip寄存器中存放call MyAdd这条指令的地址.

以上指令实际上是在进行参数的拷贝,根据实参拷贝形参并把形参压栈,此时MyAdd函数还没有开始正式调用,且通过观察可以发现,在进行实参拷贝时,先拷贝y,在拷贝x,说明形参的实例化顺序是从右到左的。

可以得出2点结论

  1. 形参实例化的顺序是从右向左的,在传参的过程中,是先实例化右边的参数,在实例化左边的参数。形参实例化的顺序与x,y定义的顺序无关。
  2. 函数调用需要传参形成临时拷贝,临时拷贝在函数正式调用之前已经完成,是函数正式调用的前置工作。
call MyAdd

call MyAdd这一条汇编指令对应2个操作:

  1. 压入返回地址。MyAdd函数执行结束以后需要返回到main函数,返回地址就是返回到main函数栈帧的中对应的地址,在转移至目标函数MyAdd之前,要把这个地址压入栈中。否则MyAdd函数执行完毕就无法回到main函数继续执行main函数后面的语句。在上面的代码中,这个返回地址是add esp 8这条指令的地址,在MyAdd函数调用完毕会用到这个地址。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zjDOASc7-1669559349433)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127205344281.png)]

  1. 转移至目标函数。此时eip中保存的是jump MyAdd指令的地址

call MyAdd指令的关键点是压入返回地址,它确保了在函数调用完毕以后能正常的回到原来的函数的栈帧。

jmp MyAdd

jmp MyAdd并非立马跳到MyAdd函数开始执行MyAdd函数的语句,而是修改eip的值为MyAdd函数第一条汇编指令的地址。执行jmp MyAdd之后,eip中保存的就是MyAdd函数第一条汇编指令的地址,至此,可以开始正式调用MyAdd函数。

MyAdd函数调用
push ebp

把当前栈底寄存器的值压入栈中,注意:当前栈底寄存器中保存的是main函数栈底的地址

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hqw7huuD-1669559349433)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127210421281.png)]

此时eip寄存器中存放mov ebp esp这条指令的地址

mov ebp esp #是把esp的内容拷贝到ebp

把esp的内容拷贝到ebp中,这样ebp就不在指向main函数的栈底。这条指令是把一个寄存器的内容拷贝到另外一个寄存器,没有进行内存的访问,执行速度很快。

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZwxXrJDB-1669559349434)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127210928164.png)]

此时eip寄存器中存放sub esp,0CCh这条指令的地址

sub esp,0CCh

将当前esp寄存器中的值减去一个值,让esp和ebp维护一段栈空间,这一段栈空间就是MyAdd函数的栈帧。

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mzdomPJQ-1669559349434)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127211214208.png)]

上面的汇编指令sub esp,0CCh是把esp的值减去0CCh,调用不同的函数,这个值是不一样的,并非都是减去0CCh,这个值取决于调用的函数的规模,例如该函数中定义了多少局部对象。编译器是可以提前预知到函数的规模大小的,根据函数的规模,确定调用该函数需要多少的栈空间,编译器可以通过sizeof计算出函数中所有栈上变量所需空间的总和,从而决定应该为该函数创建多大的栈帧。因此,sizeof计算变量的大小是在编译时完成的。

此时eip寄存器中存放mov dwrod ptr [ebp-8],0这条指令的地址

int c=0

mov dwrod ptr [ebp-8],0

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D3pIbXbq-1669559349435)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127212624241.png)]

此时eip寄存器中存放mov eax,dword ptr [ebp+8]这条指令的地址

c=a+b

这条语句转化为汇编指令为

mov eax,dword ptr [ebp+8]

把ebp+8位置的值放到eax中,实际上就是把形参的值放到eax中

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9IwlnM5h-1669559349435)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127212847949.png)]

此时eip寄存器中存放add eax,dword ptr [ebp+0Ch]这条指令的地址

add eax,dword ptr [ebp+0Ch]

把ebp+0Ch位置的值加到eax上,其实就是把形参b的值加到eax上,此时eax中存放的值就是a+b

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GMHfTB1w-1669559349436)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127213126568.png)]

此时eip寄存器中存放mov dword ptr [ebp-8],eax这条指令的地址

mov dword ptr [ebp-8],eax

把eax的值给到ebp-8位置,就是把eax的值给到ebp-8

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZV3yVh0u-1669559349436)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127213418015.png)]

至此,c=a+b计算完成。

此时eip寄存器中存放mov eax,dword ptr [ebp-8]这条指令的地址

mov eax,dword ptr [ebp-8]

即把c的值保存到寄存器中,这是在为函数返回做准备。如果函数返回小对象,则是通过寄存器保存的。

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-apGeCl02-1669559349437)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127213904532.png)]

至此MyAdd函数正式调用完毕,需要销毁栈帧和返回main函数。

MyAdd函数栈帧销毁
mov ebp esp

把ebp的值给esp。esp发生变化

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-laMgE1Ve-1669559349437)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127214519029.png)]

MyAdd函数的栈帧被释放,但是MyAdd函数的栈帧数据并没有被清空。注意此时ebp和esp指向位置存放的值是main函数的栈底。

此时eip寄存器中存放pop ebp这条指令的地址

返回到main函数
pop ebp

栈顶位置的数据(main函数栈底放入地址)给ebp寄存器,同时弹栈,esp发生改变。

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iIOgCapi-1669559349438)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127215725293.png)]

此时esp指向的栈顶位置的数据就是在call MyAdd时压入的返回地址。

此时eip寄存器中存放ret这条指令的地址

ret

ret是恢复返回地址的汇编指令,在这里ret的作用类似于pop eib,即把栈顶位置的数据放到eip中,此时栈顶位置的数据正是call MyAdd时压入的返回地址。就是add esp,8这条指令的地址。

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mxtqsjJE-1669559349438)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127221145681.png)]

此时eip寄存器中存放add esp,8这条指令的地址

add esp,8

把esp的值+8

对应如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DDQAe2Tv-1669559349439)(C:\Users\19199\Desktop\c++研发\修订文档\函数栈帧的创建与销毁.assets\image-20221127221216689.png)]

这样就正式的恢复到了MyAdd函数调用之前的样子。

此时eip寄存器中存放mov dword ptr [ebp-20h],eax这条指令的地址

mov dword ptr [ebp-20h],eax

把eax的值给到ebp-20h的位置,eax中的值就是MyAdd函数的返回值。ebp-20h位置对应z,就是把返回值给z.

这就是整个函数栈帧的创建和销毁过程。

总结

  • 函数传参,形参实例化的顺序是从右向左的,在函数正式调用之前,形参就已经在栈中形成了
  • 函数在开始正式调用之前,需要先压入返回地址,保证函数调用完毕以后能够回到原函数
  • 在函数调用之前,需要将原函数栈底的地址压入栈中,对应的汇编指令是push ebp
  • 函数的返回值是通过寄存器拿到的(对于返回小对象的情况)
  • 函数栈帧的创建与销毁过程均是在进程地址空间中完成的
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值