函数栈帧的创建与销毁

相关概念

eax,ebx,ecx,edx都是寄存器的名称.

ebp和esp也是寄存器,这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的.

每一个函数调用都要在栈区上创建一个空间. 为这个函数开辟的空间就叫做这个函数的函数栈帧.

假设现在有这样一段代码:

ebp叫做栈底指针,esp叫做栈顶指针.栈的使用习惯是先使用高地址在使用低地址.

在vs2013中main函数也是被其他函数调用的.不同的编译器,函数调用过程中栈帧的创建略有差异,具体细节取决于编译器的实现.

main函数被__tmainCRTStartup函数调用,__tmainCRTStartup被mainCRTStartup函数调用.

将代码转到反汇编来查看具体的细节.

由于此时ebp和esp指向的是调用main函数的函数的栈顶和栈底,此时调用main函数之后,就要为main函数开辟一块空间作为main函数的函数栈帧.

此时先将ebp压栈,这个ebp的值记录的是__tmainCRTStartup函数的栈底.

压栈之后,esp会自动指向栈顶元素.

mov ebp,esp,将esp的值付给ebp,此时ebp就指向了栈顶,为新开辟一块空间做准备.

sub esp,0E4h,将esp的值减去0E4h,那么0E4h就是为main函数预开辟的栈帧大小,这块空间会给main函数来使用.

push ebx,push esi,push edi压进去三个值,在后续操作中会使用到.

lea的意思是load effective address,加载有效地址,把后面的地址加载到edi中..

word是双字,dword就是四字.

这段操作的意思就是从末尾地址开始,每次写四个字节,一共写39h次,写的内容为0CCCCCCCCh.

就是为刚才创建好的main函数的函数栈帧进行初始化工作,所以c语言的变量一定要初始化,要不然就是随机值,我们也不知道会初始化为什么,这是由编译器决定的.

.

走到这里,就完成了main函数栈帧的开辟和初始化.

将0Ah也就是a的值放到ebp-8的位置.(指定了变量a的存储空间并把a的值放进去)

将14h也就是b的值放到ebp-14的位置.

将0也就是c的值放到ebp-20的位置.

所以如果变量不赋值,变量的空间里存储的就是0CCCCCCCCh.

局部变量的创建在栈上也不一定是连续的,可能会跨过一些空间,取决于编译器的实现.

接下来就开始调用Add函数.

将ebp-14h里的值放到eax中,再将eax压栈.也就是将b的值压栈.

把ebp-8里的值放到ecx中,再将ecx压栈,也就是将a的值压栈.

所以这两个操作就是在传参.

call调用函数.call在调用add函数的同时,会将call指令下一条指定的地址压栈,也就是将00C21450进行压栈.因为调用完add函数之后还要回来执行下面的指令,所以要记录一下.

执行add函数.

和main函数的逻辑一样,先为add函数开辟空间和初始化.

接下来将ebp-8的地址里放入一个0也就是z的值.

把ebp+8里的值放入eax,在把ebp+12的值加到eax,也就是10+20就是30了.

加完之后再把eax的值放入z的空间里.

所以传参的变量根本不是再add的函数栈帧里创建的,而是再真正的调用add函数之前,我们就完成了参数的传递,将参数进行压栈,等到add函数里用到形参的时候,再回头去找已经入栈的形参的值.

所以形参是实参的一份临时拷贝,在add函数里面改变形参的值根本不会影响到实参.

参数是从右向左传的,先传的b再传的a.

在执行完z=x+y之后,进行返回return z.

在返回的时候,是将z中的值放到了eax寄存器中,进行保存,因为add函数执行完之后,开辟的空间就都被销毁了,包括z.所以要将返回的内容先保存到寄存器当中.

.

函数执行完之后,接下来要进行add函数栈帧的销毁了,先执行三个pop.

在将ebp的值赋给esp,此时esp就指向了ebp所指的位置,那么上面的add的空间就没有变量指向了.

接下来进行pop,将弹出的指赋给ebp.

这次的pop弹出的是ebp指向main函数的那个地址,我们在之前是压栈进去的.

所以,此时ebp就重新指向了main函数的函数栈帧的首地址.

ret指令就是从栈顶弹出了当时记录的call指令的下一条指令的地址.

这样就顺顺利利的回到了main函数里.

当回到main函数的时候,就回到了call指令的下一条指令.

我们回到main函数里,执行add esp 8,也就是让esp指向了下面的地址,跨过了8个字节.

这八个字节就是形参的空间,也就是将形参销毁,将空间还给操作系统了.

在将eax的值放到ebp-20h中,也就是c中.此时eax的值就是函数的返回值,也就是将返回值赋给c了.

上述过程就是函数栈帧的创建和销毁,并且还有函数的调用和返回.


几个问题

局部变量是如何创建的?

函数栈帧开辟好后,为局部变量分配一些空间,供局部变量的使用和初始化.

为什么局部变量不初始化是随机值?

随机值是在进行函数栈帧初始化的时候放进去的.

函数如何传参?

在真正调用函数之前,先将形参进行压栈,当真正进入函数之后,在函数的栈帧里面,通过指针的偏移量在回头找到形参的值.

形参和实参的关系?

形参是在函数调用之前,进行压栈的时候得到的空间,它和实参在值上是相同的,但是空间上独立的.所以形参是实参的一份临时拷贝,改变形参的值并不会影响到实参.

函数调用是如何做的?

在调用函数之前,就把call的下一条执行的地址压栈了,同时将ebp指向的当前函数的首地址压栈了.

在函数执行完的时候,弹出元素,就能找到上一个函数ebp的指向了,在弹出一个元素,就能找到call指令的下一条指令的地址了.

返回值是通过寄存器的方式带回来的.

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值