C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)

目录


函数栈帧的创建和销毁
1.寄存器
其中重点的两个寄存器:ebp,esp。这两个寄存器中存放的是地址,用来维护函数栈帧
首先我们要理解,每一个函数调用,都需要在栈区上开辟一块空间
进入反汇编
1.我们是创建变量的时候赋值,如果创建变量是没有赋值,默认里面放的就是CCCCCCCC
内存监视a
内存监视b
函数调用
进入函数内部(前几步的操作顺序跟前面main函数一样)

执行计算任务
执行加法,但是x,y在哪里?
加法完成,eax变成30
形参不是在Add函数内部创建的,而是回来找了压栈的空间 数据进行计算
我们经常说,形参是实参的一份零时拷贝,这句话完全正确,改变形参不影响实参
函数返回
大家有没有疑惑, return z返回的时候,z不是销毁了么,怎么能够把结果带回去
返回值是怎么带回来的?
是我们首先放到eax寄存器里面,当我们回到函数放到局部变量c里面去
回归最开始的问题
1.局部变量是怎么创建的
2.为什么局部变量不初始化的值是随机值
3.函数是怎么传参的?传参的顺序是怎样的?
4.形参和实参是什么关系?
5.函数调用是怎么做的?
6.函数调用的结果怎么返回的?


注:以下环境是基于vs2013进行展示
函数栈帧的创建和销毁
大家学习的过程中应该会有许多疑惑,举几个例子:
·局部变量是怎么创建的?
·为什么局部变量没初始化的值是随机值?
·函数是怎么传参的(传参的顺序是怎样的)?
·形参和实参是什么关系?
·函数调用是怎么做的?
·函数调用结束后是怎么返回的?
接下来就带大家把问题全过一遍
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


1.寄存器

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


电脑的存储体系,其中寄存器是集成到cpu上的
下面我们会遇到的寄存器有:eax,ebx,ecx,edx
其中重点的两个寄存器:ebp,esp。这两个寄存器中存放的是地址,用来维护函数栈帧

首先我们要理解,每一个函数调用,都需要在栈区上开辟一块空间
int Add(int x, int y) { return x + y; }int main() { int a = 10; int b = 20; int c = 0; c = Add(a, b); return 0; }

开始调用main函数,为main函数分配空间,调用哪个函数,ebp和esp就去维护那个函数的函数栈帧。
我要调用Add函数,ebp和esp跑去维护Add函数,ebp和esp之间的空间就是为Add函数调用所开辟的空间,就叫函数栈帧
由于栈区的使用习惯是先使用高地址,再使用低地址,放数据的时候,从顶上往下放数据,所以esp可以理解为栈顶指针,ebp理解为栈低指针。

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片




大家有没有疑惑过,main函数被调用起来了,但是main函数是被谁调用的?
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


往下走,将代码执行完,可以看见__tmainCRTStartup,这个函数内部调用了main函数 ,说明main函数也是被别人调用的,__tmainCRTStartup又是被tmainCRTStartup调用
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


在main函数之前也应该分配这两个函数的空间
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


大概轮廓是这样,具体是怎么调用的接下来研究
进入反汇编

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

main函数也是被别人调用,那调用main函数的__tmainCRTStartup已经被创造好了空间
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


反汇编第一句 (push压栈:给栈顶放一个元素)(pop出栈:从栈顶删除一个元素)
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


ebp存的是__tmainCRTStartup栈低的地址,push叫压栈,给栈里面放元素
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


push完后,esp的地址也应该改变
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

监视
观看esp和ebp,这是初始地址
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

当我们开始执行push后 ,地址减了4
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


内存
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

可以看见, ebp确实被push压进去
反汇编第二句话
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

mov指令是把后面的值赋到前面,把esp给ebp
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


反汇编第三句话

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

sub是减法,给esp减去0E4h,0E4h转化为十进制为228
监视(esp地址发生改变,为main函数开辟了一块很大的空间)

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片



C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


反汇编第四,五,六句话
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


反汇编第七,八,九,十句话,lea =load effective address(加载有效地址)
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

把后面有效地址加载到edi里面,这句话不太好观察,我们显示符号名后变成ebp-0E4h

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

mov把39h放到ecx里面去。 把0CCCCCCCCh的值放进eax里面
rep stos是指,要把刚刚从edi这个位置开始,向下的39h次这么多空间dword的数据内容,全部改成0CCCCCCCCh。
一个word是两个字节,dword是四个字节
从edi开始,到ebp停止,这么多空间内容全部初始化为CCCCCCCC
内存监视
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片



此时main函数栈帧已经开辟好了,下面正式开始执行代码
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

mov,把0Ah(十进制就是10)放到ebp-8的位置,我们认为CCCCCCCC为四个字节,为a开辟的空间就是ebp到ebp-8空间
1.我们是创建变量的时候赋值,如果创建变量是没有赋值,默认里面放的就是CCCCCCCC 打印烫烫烫的原因就是因为里面是随机值CCCCCCCC,不进行初始化,不同的编译器里面可能放的是不同的值

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


内存监视a
0a 00 00 00是因为此编译器是小端存储
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

内存监视b
14h是十六进制,转化为10进制是20,空了两个整形的位置


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


内存监视c
又是相差两个整形的位置
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

函数调用

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

函数调用要传参,下面这两个动作是在传参么?
mov,把ebp-14h(也就是b)放到eax里面。push->eax,压栈
mov,把ebp-8(也就是a)放到ecx里面。push->ecx,压栈
答案确实在传参
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

call指令是调用函数,现在我们记住call指令的地址
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

执行完call指令,AA8地址放的是00 c2 14 50
压栈了call指令的下一个地址 ,为什么要记住这个地址?不要着急
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

call一调用后马上调用Add函数,Add函数调用完后需要返回,我需要回到call指令的下一条指令(call指令执行完)

进入函数内部(前几步的操作顺序跟前面main函数一样) 为Add函数准备栈帧

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


首先push ebp(此时ebp还在维护main函数栈低) 把ebp值压到顶上
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


内存监视(ebp压栈在call指令下一条指令地址)
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

把esp的值给ebp
esp,0CCh是在为Add这次调用分配函数栈帧空间

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


函数加载地址
lea 把[ebp+FFFFFF34h]地址加载到edi里面,mov把33h 放到ecx里面,再把0XCCCCCCCCh的值放到eax里面
rep ->从edi开始向下到ebp之间所有位置初始化为0XCCCCCCCCh
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片



执行计算任务
把0放到ebp-8的位置


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片





执行加法,但是x,y在哪里?
把ebp+8的值放到eax里面,ebp+8找到了ecx
ecx是压栈压来的,我们可以叫他a‘,eax就是b'
把ebp+8的值放到eax里,再add把ebp+0ch的值加到eax里面
加法完成,eax变成30
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片



C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


形参是怎么来的?我们有主动创建形参么?
没有,是因为我们在下面刚开始调用函数的时候就把参数传过来了
通过mov push mov push指令传参
push压栈先压b,再压a。c=Add(a,b)先传的b,在传的a,参数是从右向左传的
形参不是在Add函数内部创建的,而是回来找了压栈的空间 数据进行计算

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


我们经常说,形参是实参的一份零时拷贝,这句话完全正确,改变形参不影响实参
函数返回
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

大家有没有疑惑, return z返回的时候,z不是销毁了么,怎么能够把结果带回去
return z的意思是把ebp-8的值放到eax里面,eax是寄存器。寄存器是不会程序退出销毁的
ebp-8位置是z
pop三句话,弹出,把栈顶元素弹出放到edi里面,每次弹出esp就++往下走
mov 把ebp赋给esp,esp没有指向栈顶了,指向ebp所在的位置(回收Add函数的栈帧)
pop一下,之前存了ebp-main函数的栈低地址(为了防止函数销毁后找不到栈低指针),ebp返回到main函数栈低地址
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


此时pop弹出后,ebp回归到main函数栈低
ret指令,esp回到了00C21450的地址,此地址是call指令下一条指令的地址,继续从call指令下一条开始执行(不仅要走的出去,还要回的来)
给esp加8,跳过形参a,b,形参a,b销毁
把eax的值放到ebp-20h,ebp-20h位置就是c的空间 ,eax值是和:30
返回值是怎么带回来的? 是我们首先放到eax寄存器里面,当我们回到函数放到局部变量c里面去
C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片


Add函数销毁知道了,main函数销毁流程也差不多,这就是函数栈帧创建销毁的过程
回归最开始的问题

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

1.局部变量是怎么创建的
首先为函数分配好栈帧空间,栈帧空间初始化好一部分空间以后,给我的局部变量在栈帧里面分配一点空间
2.为什么局部变量不初始化的值是随机值
因为随机值是编译器放进去的,例如CCCCCCCC,如果初始化便把随机值覆盖了
3.函数是怎么传参的?传参的顺序是怎样的?
当我们还没有去调用函数的时候,便已经push从右向左压栈压进去,当我们进入形参函数的时候,在Add函数里面,通过指针的偏移量回来找到了我们形参。
4.形参和实参是什么关系?
形参实在压栈的时候开辟的空间,和实参值是相同的,空间是独立的,形参是实参的一份零时拷贝,这句话完全正确,改变形参不影响实参
5.函数调用是怎么做的?
上面总结清楚了
6.函数调用的结果怎么返回的?
调用之前就把call指令下一条指令的地址记住,把ebp调用函数的上一个函数的栈帧存进去,当函数调用完后返回的时候,弹出ebp,就能找到原始上一个函数的ebp,指针往下走就能找到esp地址
返回值是通过寄存器带回来的

C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)
文章图片

【C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)】函数的内部所创建的静态变量是在全局开辟的,今天我们画的都是在栈区上开辟的

    推荐阅读