当前位置: 首页 > news >正文

纯干货:C语言中函数栈帧的介绍

函数栈帧的创建和销毁

  • 压栈和出栈
  • 问题引入
  • 寄存器及栈区的使用方式
  • main函数其实也是被其他函数调用的
  • F10之后 转到反汇编开始观察
  • S1:push ebp 压栈
  • S2:mov ebp,esp
  • S3:sub esp,0E4h->开辟main函数的函数栈帧
  • S4:三次push
  • S5:lea加载有效地址
  • S6:默认初始化main函数的栈帧空间
  • S7:开始给a分配空间(知道为什么局部变量不初始化是随机值了)
  • S8:继续给b c分配空间
  • 总结1:怎么创建局部变量的?
  • 开始观察Add()函数
  • A1:先传参b
  • A2:再传参a
  • 总结2:怎么传参的?(怎么和想象中不太一样?)
  • A3:call-调用函数(理解函数是怎么返回的)
  • 总结3:可以认为形参是在main函数的栈帧里创建的
  • A4:进入Add之后 类似main 先开辟栈帧 然后初始化
  • A5:给Add函数的局部变量z开辟空间
  • A6:计算z = x + y
  • A7:临时保存计算结果
  • A8:三次pop
  • A9:回收Add函数的栈帧
  • A10:继续往下执行(之前存放的call指令下一条指令的地址派上用场了)
  • A11:销毁形参
  • A12:把A7的计算结果真正给到c
  • 总结4:形参并不是是在Add函数内部创建的
  • 总结5 Add()函数的整个调用流程
  • 总结6:形参确实是实参的临时拷贝
  • 总结7:什么叫销毁?
  • 最后的总结

压栈和出栈

● push 压栈 在栈顶放一个元素
● pop esh 出栈 把栈顶的元素赋给esh
在这里插入图片描述

问题引入

在学习前面的内容 可能仍然存在很多困惑:

  • 局部变量是如何创建的?
  • 为什么局部变量的值不初始化就是随机值?
  • 函数是怎么传参的?传参的顺序?
  • 一直说形参是实参的临时拷贝 具体怎么实现的?
  • 调用一个函数 具体过程是什么?
  • 函数调用完 是怎么返回的?

在深入了解函数栈帧的创建和销毁之后 以上问题都会迎刃而解

注意:
在这里插入图片描述

本小节使用如下代码讲解:
在这里插入图片描述

寄存器及栈区的使用方式

  • 寄存器 是集成在CPU上的 和硬盘 内存之间是独立的
  • ebp esp看作指针变量 里面放的是地址
  • 正在调用哪个函数 ebp和esp维护的就是该函数的函数栈帧
    在这里插入图片描述
  • main函数被调用执行 也需要main函数的栈帧空间
  • 栈区的使用方式:先使用高地址 再使用地地址(仿佛从栈顶往里东西 沉到栈底了)
  • esp:栈顶指针
  • ebp:栈底指针
    在这里插入图片描述

main函数其实也是被其他函数调用的

  • 然后下面就要调用Add()函数了
    在这里插入图片描述

F10之后 转到反汇编开始观察

在这里插入图片描述

下面的Sn 就是在观察这部分代码怎么一步一步走的
可以对比观察
在这里插入图片描述

S1:push ebp 压栈

刚开始的样子:
在这里插入图片描述

push ebp:把ebp这个指针变量压栈 放在栈顶
与此同时 由于esp永远都指向栈顶
当ebp被压栈之后 esp也要往上走(也就是地址值会变低/变小) 直到他指向栈顶
在这里插入图片描述

S2:mov ebp,esp

也就是把esp的值赋给ebp 也就说这俩指针变量暂时指向同一块空间
在这里插入图片描述

S3:sub esp,0E4h->开辟main函数的函数栈帧

也就是esp = esp - 0E4h(八进制的228)
地址变小了 在图上 就是往上移动了 在这里插入图片描述

于是又维护了一个全新的函数栈帧 这其实就是main函数的栈帧
这其实就相当于:在栈区为main函数预开辟了一块空间
在这里插入图片描述

S4:三次push

之前说过了 esp永远指向栈顶
所以每次push一个元素到栈顶 esp就会往上移(其实是地址值变小)
在这里插入图片描述

S5:lea加载有效地址

先看看这个地址是啥 勾选上这个选项
在这里插入图片描述

原来是这样 下图圈起来的不是同一个意思吗?

在这里插入图片描述

就是把S3esp指向的地址赋给edi
在这里插入图片描述

S6:默认初始化main函数的栈帧空间

从edi这个位置开始(刚好到ebp结束)
把39h这么多个 dword(相当于四个字节)
全都初始化成CCCCCCCCh
在这里插入图片描述

相当于把main预开辟的空间都改成0xCCCCCCCh
在这里插入图片描述

S7:开始给a分配空间(知道为什么局部变量不初始化是随机值了)

这表明把0Ah这个值 也就是10么 放到ebp-8的位置
在这里插入图片描述
在这里插入图片描述

也就是说 不赋值的话 a那块空间放的就是S6默认初始化的CCCCCCC
CCCCCCCvs2013的初始化方式
不同的编译器可能不一样
所以我们说是 “随机值”
在这里插入图片描述

S8:继续给b c分配空间

有可能有的编译器就是连着分配的 vs2013中间就隔了八个字节
在这里插入图片描述
在这里插入图片描述

如下图:
在这里插入图片描述

总结1:怎么创建局部变量的?

调用函数
先给函数栈帧开辟空间
然后给这部分栈帧全都初始化成CCCCCCCCC
然后再给函数里的局部变量分配空间 如果不初始化 就是CCCCCCCC

开始观察Add()函数

类似前面F10的操作 下面开始观察下图的过程
在这里插入图片描述

A1:先传参b

把ebp-14h里放的值(也就是十进制的20) 赋给eax
在这里插入图片描述

然后push eax 也就是把eax压栈
从栈顶放一个eax 也就是20
同时esp往上移动
在这里插入图片描述

A2:再传参a

和A1同理
把ebp-8这个位置的值 也就是十进制的10 也就是a了 赋给ecx
在这里插入图片描述

然后push exc
把10压栈
同时esp往上移动
在这里插入图片描述

总结2:怎么传参的?(怎么和想象中不太一样?)

A1 A2 就是在传参!
而且好像是b先传 a后传 也就是从右向左传!
和我们想象中的好像不太一样?
没错!! 继续往下看!!
在这里插入图片描述

A3:call-调用函数(理解函数是怎么返回的)

请记住下面这个地址

在这里插入图片描述

然后按F11
下图是按F11之前esp的值:
在这里插入图片描述
按完F11:
在这里插入图片描述

还记得前面让记住一个地址了吗
所以我按完F11 也就是执行call指令
不仅跳进去Add函数了 而且把call指令的下一条指令的地址 给压栈了
压栈之后 esp肯定要继续往上移的
在这里插入图片描述

思考:F11跳到Add函数里 执行完怎么回来?
所以才会把call执行下一条指令的地址先压栈
待会就通过它 等函数执行完毕 可以找回来
然后继续往下执行

总结3:可以认为形参是在main函数的栈帧里创建的

其实是先push
下图的exc eax 就可以看做形参
然后push完 esp又跑到上面去了
这个时候仍然可以认为esp和ebp维护的还是main的栈帧
所以main的栈帧貌似扩大了一点(从ebp到esp)
形参是在main的栈帧里的
可以这么理解 但是也可以认为形参是独立的空间
总之明白形参本质上不是在Add()函数的栈帧里创建的就对了! 因为这个时候还没调用Add呢!!
在这里插入图片描述

A4:进入Add之后 类似main 先开辟栈帧 然后初始化

在这里插入图片描述

此时epb还在维护main的栈底呢
所以一上来先push 把main函数的ebp压栈
同时esp肯定要继续往上移
这个动作就很像:我要开始开辟全新的栈帧了!!
在这里插入图片描述

后续的过程不再详述 和前面S1~S6完全一致 就是给Add函数开辟栈帧 完成初始化的
最终如下图:
在这里插入图片描述

A5:给Add函数的局部变量z开辟空间

下面的步骤和main函数里一开始int a b c是十分类似的
同样都是在ebp-8的位置开始开辟
不过这个时候的ebp已经被更新过了 现在维护的是Add函数的栈帧
都是在各种的空间开辟局部变量的内存空间

把ebp-8这个空间初始化成0 也就是z了 在这里插入图片描述
在这里插入图片描述

A6:计算z = x + y

x y是什么呢?? 先回看A1和A2
结合A1 A2 和 A5的图
ebp+8和ebp+12 就是往下找 正好找到了A1 A2的exc和eax

  1. 找到ebp+8 也就是A2的a(也就是10) 把10给eax
  2. 然后再给eax add上ebp+0Ch里的东西(也就是ebp+12) 也就找到了A1的b 也就是12
  3. 然后再把eax的值放到ebp-8里去 其实就是把相加的结果放到了abp-8(其实就是z的位置) 在这里插入图片描述
    最终如下图:
    z已经放好了计算结果30
    在这里插入图片描述

A7:临时保存计算结果

前面已经把eax里的计算结果放到ebp-8里了 其实就是z
然后又把ebp-8的值 也就是z的值(30)
临时放到了eax寄存器里(全局的寄存器) 在这里插入图片描述

A8:三次pop

注意pop出栈之后
esp由于一直要指向栈顶
也会一直往下移(其实是地址值增大了)
在这里插入图片描述
在这里插入图片描述

A9:回收Add函数的栈帧

在这里插入图片描述

执行完mov:
把ebp的值赋给了esp 这样Add的esp和ebp都指向了main之前的ebp
在这里插入图片描述

执行完pop:
也就是把栈顶的元素(main之前的ebp) 赋给了Add的ebp
每次pop完之后 esp也要下移
这样一来
Add的ebp走到一开始main的ebp的位置了
Add的esp恰好也是一开始main的esp的位置
总结来说就是 现在的esp和ebp 维护的又是之前main函数的栈帧了
在这里插入图片描述

A10:继续往下执行(之前存放的call指令下一条指令的地址派上用场了)

ret这个指令会:

  1. 先pop一下(也就是把栈顶的元素弹出 栈顶那个元素 就是call指令的下一条指令的地址) 同时esp下移
  2. 然后跳到刚刚pop的地址去 也就跳到call下面的执行 开始继续执行了
  3. 也就是说Add函数调用完毕 还能找到回去的路 继续往下执行
    在这里插入图片描述
    请看A9的图 栈顶上正放着call的下一条指令的地址呢!
    在这里插入图片描述

A11:销毁形参

找回call 继续往下执行
把esp+8
也就是在A9的图的基础上 esp往下移动两个元素 正好跳过了两个形参
相当于把形参的空间还给操作系统了
在这里插入图片描述
在这里插入图片描述

A12:把A7的计算结果真正给到c

  • 请回头看A7 计算结果 临时放到了eax寄存器里(全局的寄存器)
  • 然后这里把eax的结果 又给了ebp-20h 其实就是c!
    在这里插入图片描述

总结4:形参并不是是在Add函数内部创建的

从总结3可以看出
a和b应该属于扩大之后的main函数的栈帧
而Add()函数在计算z = x + y的时候
是直接去访问到下面的a和b进行计算的 所以形参并不是在Add的函数栈帧创建的
`

不难看出:
Add的栈帧里只创建了一个z
然后利用扩大之后的main函数的栈帧里的a和b
计算出x+y的值 赋给z
再把z临时保存在寄存器eax中
在这里插入图片描述

总结5 Add()函数的整个调用流程

再来看这个代码

  1. 开辟main栈帧
  2. 分配 abc的空间 完成初始化
  3. 在进入Add之前 已经把a和b压栈了 而且先压的是b 再压的a
  4. 进入Add之后 开辟add的栈帧 初始化局部变量z
  5. 计算x+y的时候 往下找到之前已经压栈的a b 利用他们的值 计算出x+y的值(a给x用 b给y用 ) 然后赋给z
  6. 然后把z里的计算结果 临时保存在全局寄存器eax之中
  7. 至此 就要开始销毁Add函数的栈帧了 Add函数调用结束
  8. 继续找到call的下一条指令继续执行
  9. 先销毁形参 然后把eax中的计算结果赋给c
  10. 再往后就是打印c 然后开始销毁main函数的栈帧 和Add的过程非常相似 在这里插入图片描述

总结6:形参确实是实参的临时拷贝

虽然a’ b’不是在Add内部创建的 但是确实是给了形参x y使用的
所以就可以把a' b'看做a b的形参
显然就是一份临时拷贝

而且a' b' 和 a b完全是独立的空间
所以修改a’ b’肯定不会影响a b
在这里插入图片描述

总结7:什么叫销毁?

所以销毁不是真的给他毁了啊! 叫做操作系统回收更好!
计算机的销毁就是:这块内存空间 可以被别人使用/覆盖了
就在这里而言:如果不再被esp和ebp维护 就可能会被分配给别人使用 就算是被销毁/回收了

最后的总结

让我们回到一开始的问题

  • 局部变量是如何创建的?
    先给函数分配完栈帧空间 然后初始化这块空间(这里是都初始化成CCCCCCCC) 然后给局部变量分配空间 如果不初始化 那就是随机值
  • 为什么局部变量的值不初始化就是随机值?
    上面已经回答
  • 函数是怎么传参的?传参的顺序?
    A1 A2的两次push 其实就是在传参(在创建两个临时变量) 而且是从右往左push的
  • 一直说形参是实参的临时拷贝 具体怎么实现的?
    看总结6
  • 调用一个函数 具体过程是什么?
    看总结5
  • 函数调用完 是怎么返回的? 返回值怎么带回来的?
    因为一开始就记住了call执行的下一条指令
    所以执行ret指令的时候 pop一下就可以找回去 继续往下执行
    而返回值其实是利用eax全局寄存器返回的

http://www.mrgr.cn/news/52277.html

相关文章:

  • FFmpeg源码:avformat_new_stream函数分析
  • 1.项目初始化
  • Spring Boot 的执行器是什么?
  • Linux Debian12基于ImageMagick图像处理工具编写shell脚本用于常见图片png、jpg、jpeg、tiff格式批量转webp格式
  • 42. 将数值保留两位小数
  • springboot 对接Telegram发送消息
  • 数据结构--链表
  • web APIs
  • 视频格式转换软件哪个好用?7款可靠的视频转换软件测评
  • HubSpot客户平台那些超好用的工具,你get了吗?
  • c高级day4
  • Python干货:良心整理出来Python15个超级库,学习python的小伙伴千万不要错过
  • element-ui 的el-calendar日历组件样式修改
  • 【升华】人工智能10大常用算法与及代码实现(汇总)
  • QTableView 接口详情
  • C语言小游戏--猜数字
  • 安达发|机械零件APS生产排程系统的多种排序规则
  • 文件IO(Linux文件IO,目录操作函数)
  • centos ping能通但是wget超时-解决
  • 【随时随地学算法】本地部署hello-algo结合内网穿透远程学习新体验