函数栈帧的创建与销毁底层逻辑简述

目录

前言:

一、寄存器

二、栈顶、栈底指针

三、C语言程序运行大致过程

四、一个简单程序运行的剖析

(1)main函数的调用

(2)main函数调用其中的函数

六、程序的最后

总结


前言:

在vs2013的环境下,通过从内存、反汇编代码的角度对程序运行进行观察,来探究一个函数的栈帧的创建于销毁的过程


一、寄存器

C语言中用来存放一些临时变量的地方

C语言编译器(VS)下大多命名诸如eax,ebx,ecx,edx
(eax, ebx, ecx, edx, esi, edi, ebp, esp
等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。)

二、栈顶、栈底指针

ebp、esp寄存器
这两个寄存器是专门用于存放地址,这两个地址是用来分别维护函数栈帧的

ebp和esp是用于维护当前正在调用的函数的栈地址
ebp保存栈底位置的地址,esp用于保存栈顶位置的地址

关于栈空间:
在内存中栈顶是低地址,栈底是高地址,ebp为栈底指针,esp为栈顶指针
栈空间的使用是由高地址向低地址增长

函数调用时:
每一个函数调用都要在栈上创建一个空间
ebp与esp则是分别存放(指向)该空间的始末位置


三、C语言程序运行大致过程

程序的运行由main函数开始
但是编译器进行编译时并不是由main函数开始,大致如下:

首先是main函数的调用:
在vs2013中,main函数也是由其它函数调用的
mainCRTStartup函数调用__tmainCRTStartup函数,然后再由__tmainCRTStartup调用main函数

函数的调用
每次函数的调用都会为该函数创建栈帧

具体检验的过程:
在vs2013中通过调试代码,监视内存的变化以及观察反汇编代码的执行来进行检验,不同的编译器可能会有一些地方有些许出入,不过整体逻辑是相似的

栈帧的创建
从内存和反汇编上观察栈帧的创建

反汇编代码的一些语句
push a即将a压栈,mov ebp,esp将esp的值赋给ebp,sub a,b用a减b等

栈帧创建
栈帧创建时,创建空间的栈顶位置的地址由esp指向,栈底由ebp指向


四、一个简单程序运行的剖析

(1)main函数的调用

1.初始
mainCRTStartup函数调用__tmainCRTStartup,此处的mainCRTStartup由编译器调用,再由该函数调用__tmainCRTStartup函数,内存中系统自动为其开辟空间

2.ebp与esp
最开始调用__tmainCRTStartup函数时,系统为其开辟的栈空间的起始地址则由ebp与esp进行管理

3.开始调用main函数
(每次压栈(push)的操作后esp都会自动往低地址(当前函数栈帧空间的栈顶)移动)
​先将ebp的拷贝压入到栈中(为了函数调用完毕后出栈做准备),esp则往低地址移动(__tmainCRTStartup函数的栈帧空间增长)

4.更改ebp,esp的指向
1、将esp指向ebp的位置(准备进行main函数的栈帧空间开辟)
2、esp则往低地址移动一定的距离(为main函数预开辟一块栈帧空间
3、此时ebp与esp则分别维护了一块新的空间,这块空间即为了main函数开辟的栈帧空间

将三个寄存器压入占中
1、ebx;2、esi;3、edi
tips:ebx是"基地址"(base)寄存器, 在内存寻址时存放基地址
esi/edi分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串(其实这个
不了解也没事儿,每次函数调用栈帧的创建都会将该三个寄存器压入栈中)

5.将开辟的空间进行初始化
这也是为什么如果局部变量不初始化就会是随机值的原因

汇编代码:
(1)lea edi,[ebp-0E4h] 即将中括号里的地址放入edi中
​(2)move ecx,39h 即将39h放入ecx寄存器中;
(3)move eax,0CCCCCCCCh 即将该值放入eax中
(4)rep stos dword ptr es:[edi]
即意为从edi开始向下的39h个空间(直到ebp)都更改成eax里的内容(初始化)
tips:lea即load effictive address,初始化的值由编译器设计时决定

6.此时才真正进入main函数
然后将main函数中的变量在为main函数开辟的栈帧空间中创建(按顺序依次压入main函数的栈帧区域中)

(2)main函数调用其中的函数

1.先将所调用的函数的参数先进行拷贝并压入栈中
形参是自右向左依次进行拷贝(所以形参传递表现出来即是自右向左进行传递),拷贝时都是通过先用寄存器保存局部变量的值并压入栈中
​tips:main函数的栈帧区域进行相应的扩展(esp则往低地址再次移动)

2.调用函数
利用call指令进行对函数的调用,在执行call指令时,会先将call指令的下一条指令的地址压入栈中,再跳转到调用的函数中
(便于函数调用完毕后回到函数调用的地方继续运行程序)

3、创建被调用函数的栈帧空间
过程与调用main函数时的整体过程是一样的,创建完毕后,esp与ebp则会分别指向该栈帧空间的栈顶与栈底

4.临时变量的创建以及形参的使用
临时变量的创建也会在该被调用函数的栈帧空间中进行创建;
​对于形参的调用即通过ebp(栈底指针)的偏移找到先前调用时创建的实参的拷贝的地址并进行使用
tips:函数的形参在调用函数之前就已经被创建在了main函数的栈帧空间中了(步骤1)

5.调用函数完毕后的返回
有返回值时,会将返回值先保存到寄存器中,再销毁该被调函数的栈帧空间
(tips:所以尽管函数调用完毕栈空间被销毁了也仍然能收到返回值)
即利用pop将栈顶的数据弹出,esp依次往底偏移,之后再将ebp的值赋予esp(将esp指向ebp指向的位置(被调函数创建的栈帧空间的栈底
再将main函数创建的栈帧空间的栈底位置弹入到ebp中,此时ebp则会指向main函数的栈帧空间的栈底
再利用ret返回到main函数中调用函数的位置
(此时是通过pop掉先前执行call指令时压入栈中的下一条指令的地址实现的)


六、程序的最后

1.返回到main函数的栈帧空间
2.此时才将先前为调用函数创建的实参的拷贝进行销毁
(esp往下移动,即将形参变量的空间返还给操作系统)
3.此时再将保存着返回值的寄存器里的值放到接收变量的空间中(如果没有变量将函数的返回值接收则就无了)

4.main函数调用完毕
调用完毕后返回的方式与上方的过程是一样的


总结

本篇对C语言中,函数调用时,其底层的一些剖析。

具体对局部变量是如何创建的为什么局部变量的值未初始化是随机值函数是如何传参其传参顺序是怎样的形参与实参的底层关系函数调用是怎么做的以及函数调用结束后是如何返回的这些问题进行了一些分析,希望能让大家更加了解C语言~


tips:以上函数调用过程都是基于vs2013的编译器下进行,具体实践时不同的编译器可能会有些许出入,不过整体上都是大同小异的。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

c.Coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值