• 函数栈帧的创建和销毁


    “总有人间一两风,

    填我十万八千梦”

    🍑作者:小赛毛


    目录

    函数栈帧解决了什么问题?

    什么是栈?

    什么是寄存器?

      函数栈帧的创建和销毁

    预热知识准备:

     函数的调用堆栈

    转到反汇编

    函数栈帧的创建

    补充说明:烫烫烫~

    调用Add函数:

    函数栈帧的销毁


    大家学了这么久C语言有没有产生一些问题与疑惑哩?

    好家伙,你问俺啥问题,那我只能说6哈哈哈哈

    比如噻

    函数栈帧解决了什么问题?

    • 局部变量是怎么创建的?
    • 为什么局部变量的值是随机值?
    • 函数是怎么传参的?传参的顺序是怎样的?
    • 形参和实参是什么关系?
    • 函数调用是怎么做的?
    • 函数调用是结束后怎么返回的?

    老铁这会儿可能又要说,你小子,哪来这么多问题?

     其实哩,老铁们呢只要知道函数栈帧的创建和销毁,学会了就明白这些问题了,内功呢自然也就修炼了~

    我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。
    那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
    函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:函数参数和函数返回值
    临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
    保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

    什么是栈?

    栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函
    数,没有局部变量,也就没有我们如今看到的所有的计算机语言。

    在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可
    以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出
    栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。

    在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据
    从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。

    在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
    在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的


    要给大家讲明白这一点呢,就要给大家讲一下寄存器

    什么是寄存器?

    • eax:通用寄存器,保留临时数据,常用于返回值
    • ebx:通用寄存器,保留临时数据
    • ebp:栈底寄存器
    • esp:栈顶寄存器
    • eip:指令寄存器,保存当前指令的下一条指令的地址

    ebp

    esp

    这两个呢是今天的重点,要理解函数栈帧,就必须理解这两个寄存器。

    ebp,esp中存放的是地址,这2个地址是用来维护函数栈帧的。

    同时我们也要知道相关汇编命令

    • mov:数据转移指令
    • push:数据入栈,同时esp栈顶寄存器也要发生改变
    • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
    • sub:减法命令
    • add:加法命令
    • call:函数调用,1. 压入返回地址 2. 转入目标函数
    • jump:通过修改eip,转入目标函数,进行调用
    • ret:恢复返回地址,压入eip,类似pop eip命令

      函数栈帧的创建和销毁

    预热知识准备:

    在开始之前呢,再给大家补充一下知识:

     每一个函数调用,都要在栈区创建一个空间每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间

    • push压栈:给栈顶放一个元素
    • pop出栈:从栈顶删除一个元素

    我们知道每一个函数调用都会在栈区上开辟一块空间,

    如果下面是高地址,上面是低地址

     这块空间由ebp esp所维护,在正在调用哪个函数,ebp和esp就去维护哪块函数空间的函数栈帧,比如说此时调用Add函数,那esp ebp就去维护Add函数空间的栈帧,通常我们把ebp叫做栈底指针,esp叫做栈顶指针。 

     函数的调用堆栈

     在vs2013中,main函数也是被其他函数调用的

    __tmainCRTStartup

    mainCRTStartup


     接下来我们继续:

    转到反汇编

    首先f10调试到main函数开始执行的第一行,右击鼠标转到反汇编

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int Add(int x, int y)
    4. {
    5. int z = 0;
    6. z = x + y;
    7. return z;
    8. }
    9. int main()
    10. {
    11. int a = 10;
    12. int b = 20;
    13. int c = 0;
    14. c = Add(a, b);
    15. printf("%d\n", c);
    16. return 0;
    17. }
    1. int main()
    2. {
    3. 00C418B0 push ebp //把ebp寄存器中的值进行压栈
    4. 00C418B1 mov ebp,esp //move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp //sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp
    5. 00C418B3 sub esp,0E4h
    6. 00C418B9 push ebx
    7. 00C418BA push esi
    8. 00C418BB push edi
    9. //下面的代码是在初始化main函数的栈帧空间。
    10. //1. 先把ebp-24h的地址,放在edi中
    11. //2. 把9放在ecx中
    12. //3. 把0xCCCCCCCC放在eax中
    13. //4. 将从edp-0x2h到ebp这一段的内存的每个字节都改为cccccccc
    14. 00C418BC lea edi,[ebp-24h]
    15. 00C418BF mov ecx,9
    16. 00C418C4 mov eax,0CCCCCCCCh
    17. 00C418C9 rep stos dword ptr es:[edi]
    18. 00C418CB mov ecx,0C4C008h
    19. 00C418D0 call 00C4131B
    20. int a = 10;
    21. 00C418D5 mov dword ptr [ebp-8],0Ah
    22. int b = 20;
    23. 00C418DC mov dword ptr [ebp-14h],14h
    24. int c = 0;
    25. 00C418E3 mov dword ptr [ebp-20h],0
    26. c = Add(a, b);
    27. 00C418EA mov eax,dword ptr [ebp-14h]
    28. 00C418ED push eax
    29. 00C418EE mov ecx,dword ptr [ebp-8]
    30. 00C418F1 push ecx
    31. 00C418F2 call 00C410B4
    32. 00C418F7 add esp,8
    33. 00C418FA mov dword ptr [ebp-20h],eax
    34. printf("%d\n", c);
    35. 00C418FD mov eax,dword ptr [ebp-20h]
    36. 00C41900 push eax
    37. 00C41901 push 0C47B30h
    38. 00C41906 call 00C410D2
    39. 00C4190B add esp,8
    40. return 0;
    41. 00C4190E xor eax,eax
    42. }


    函数栈帧的创建

     我们先来push压栈

    操作前:

     我们看执行完:

    这就是给大家讲的先把ebp压进去

    ebp压进去以后再怎么办呢?


    接下里是move

    move是把esp给ebp

     


     下一步sub,sub是解码嘛

     我们注意到esp变了,这意味着什么呢?

    esp的地址往上走了~


    紧接着下来三次push ,其实是给顶上压进去了三个元素,这三个元素呢,我们不需要去管


     接下来lea

    lea:load effective address

    给edi里面放了一个地址

    将从edp-0x2h到ebp这一段的内存的每个字节都改为cccccccc


    补充说明:烫烫烫~

    之所以程序输出“烫”这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一
    个字节都被初始化为cccccccc,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,cccccccc的汉字编码就是“烫”,所以cccccccc被当作文本就是“烫”。


    a,b,c创建:

    1. int a = 10;
    2. 00C418D5 mov dword ptr [ebp-8],0Ah //将10存储到ebp-8的地址处,ebp-8的位置其实就是a变量
    3. int b = 20;
    4. 00C418DC mov dword ptr [ebp-14h],14h //将20存储到ebp-14h的地址处,ebp-14h的位置其实是b变量
    5. int c = 0;
    6. 00C418E3 mov dword ptr [ebp-20h],0 //将0存储到ebp-20h的地址处,ebp-20h的位置其实是c变量

     以上汇编代码表示的变量a,b,c的创建和初始化,这就是局部的变量的创建和初始化 ,其实是局部变量的创建时在局部变量所在函数的栈帧空间中创建的


    当a,b,c创建好后,我们就该:

    调用Add函数:

    1. c = Add(a, b);
    2. 00C418EA mov eax,dword ptr [ebp-14h] //把b的值20放入eax
    3. 00C418ED push eax //栈顶压入
    4. 00C418EE mov ecx,dword ptr [ebp-8] //把a的值10放入ecx
    5. 00C418F1 push ecx //栈顶压入
    6. 00C418F2 call 00C410B4 //调用
    7. 00C418F7 add esp,8
    8. 00C418FA mov dword ptr [ebp-20h],eax
    9. printf("%d\n", c);
    10. 00C418FD mov eax,dword ptr [ebp-20h]
    11. 00C41900 push eax
    12. 00C41901 push 0C47B30h
    13. 00C41906 call 00C410D2
    14. 00C4190B add esp,8
    15. return 0;
    16. 00C4190E xor eax,eax
    17. }
    18. 00C41910 pop edi
    19. 00C41911 pop esi
    20. 00C41912 pop ebx
    21. 00C41913 add esp,0E4h
    22. 00C41919 cmp ebp,esp
    23. 00C4191B call 00C41244
    24. 00C41920 mov esp,ebp
    25. 00C41922 pop ebp
    26. 00C41923 ret
    1. 00C418EA mov eax,dword ptr [ebp-14h] //把b的值20放入eax
    2. 00C418ED push eax //栈顶压入
    3. 00C418EE mov ecx,dword ptr [ebp-8]
    4. 00C418F1 push ecx

    以上两个动作就是在进行传参

    1. int Add(int x, int y)
    2. {
    3. 00C41770 push ebp //将main函数栈帧的ebp保存,esp-4
    4. 00C41771 mov ebp,esp //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
    5. 00C41773 sub esp,0CCh //给esp-0xCC,求出Add函数的esp
    6. 00C41779 push ebx //将ebx的值压栈,esp-4
    7. 00C4177A push esi //将esi的值压栈,esp-4
    8. 00C4177B push edi //将edi的值压栈,esp-4
    9. 00C4177C lea edi,[ebp-0Ch]
    10. 00C4177F mov ecx,3
    11. 00C41784 mov eax,0CCCCCCCCh
    12. 00C41789 rep stos dword ptr es:[edi]
    13. 00C4178B mov ecx,0C4C008h
    14. 00C41790 call 00C4131B
    15. int z = 0;
    16. 00C41795 mov dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z
    17. z = x + y;//接下来计算的是x+y,结果保存到z中
    18. 00C4179C mov eax,dword ptr [ebp+8] //将ebp+8地址处的数字存储到eax中
    19. 00C4179F add eax,dword ptr [ebp+0Ch] //将ebp+12地址处的数字加到eax寄存中
    20. 00C417A2 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp-8的地址处,其实就是放到z中
    21. return z;
    22. 00C417A5 mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
    23. }
    24. 00C417A8 pop edi
    25. 00C417A9 pop esi
    26. 00C417AA pop ebx
    27. 00C417AB add esp,0CCh
    28. 00C417B1 cmp ebp,esp
    29. 00C417B3 call 00C41244
    30. 00C417B8 mov esp,ebp
    31. 00C417BA pop ebp
    32. 00C417BB ret

     图片中的 a’ 和 b’ 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。 


    函数栈帧的销毁

    函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。
    那具体是怎么销毁的呢?我们看一下反汇编代码:

    1. 00C417A8 pop edi //在栈顶弹出一个值,存放到edi中,esp+4
    2. 00C417A9 pop esi //在栈顶弹出一个值,存放到esi中,esp+4
    3. 00C417AA pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4
    4. 00C417AB add esp,0CCh //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈帧空间

     各位老铁们,本章知识到此就要说再见啦,我们接下来的知识,下一篇见,小赛毛与你不见不散!

    加油啦,小比特~

    记得一键三连嗷!三连!!三连!!!

  • 相关阅读:
    【LeetCode】891.子序列宽度之和
    Java老码农心得:卷了这么多年,您真的卷会了吗?
    Git常见命令
    002数据安全传输-多端协议传输平台:配置Oracle数据库-19c及导入数据信息
    Java 8 stream的详细用法
    一文读懂 BizDevOps:数字化转型下的技术破局
    查找算法【二叉查找树】 - 二叉查找树的插入
    GOM传奇引擎登录器商业版与免费版的区别
    【Maven教程】(五)仓库:解析Maven仓库—布局、分类和配置,远程仓库的认证与部署,快照版本,依赖解析机制,镜像和搜索服务 ~
    PIL学习
  • 原文地址:https://blog.csdn.net/m0_58232983/article/details/127949515