目录
函数栈帧的创建和销毁
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进行展示函数栈帧的创建和销毁
大家学习的过程中应该会有许多疑惑,举几个例子:接下来就带大家把问题全过一遍
·局部变量是怎么创建的?
·为什么局部变量没初始化的值是随机值?
·函数是怎么传参的(传参的顺序是怎样的)?
·形参和实参是什么关系?
·函数调用是怎么做的?
·函数调用结束后是怎么返回的?
文章图片
1.寄存器
文章图片
电脑的存储体系,其中寄存器是集成到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理解为栈低指针。
文章图片
大家有没有疑惑过,main函数被调用起来了,但是main函数是被谁调用的?
文章图片
往下走,将代码执行完,可以看见__tmainCRTStartup,这个函数内部调用了main函数 ,说明main函数也是被别人调用的,__tmainCRTStartup又是被tmainCRTStartup调用
文章图片
在main函数之前也应该分配这两个函数的空间
文章图片
大概轮廓是这样,具体是怎么调用的接下来研究
进入反汇编
文章图片
main函数也是被别人调用,那调用main函数的__tmainCRTStartup已经被创造好了空间
文章图片
反汇编第一句 (push压栈:给栈顶放一个元素)(pop出栈:从栈顶删除一个元素)
文章图片
ebp存的是__tmainCRTStartup栈低的地址,push叫压栈,给栈里面放元素
文章图片
push完后,esp的地址也应该改变
文章图片
监视
观看esp和ebp,这是初始地址
文章图片
当我们开始执行push后 ,地址减了4
文章图片
内存
文章图片
可以看见, ebp确实被push压进去
反汇编第二句话
文章图片
mov指令是把后面的值赋到前面,把esp给ebp
文章图片
反汇编第三句话
文章图片
sub是减法,给esp减去0E4h,0E4h转化为十进制为228
监视(esp地址发生改变,为main函数开辟了一块很大的空间)
文章图片
文章图片
反汇编第四,五,六句话
文章图片
文章图片
反汇编第七,八,九,十句话,lea =load effective address(加载有效地址)
文章图片
把后面有效地址加载到edi里面,这句话不太好观察,我们显示符号名后变成ebp-0E4h
文章图片
mov把39h放到ecx里面去。 把0CCCCCCCCh的值放进eax里面
rep stos是指,要把刚刚从edi这个位置开始,向下的39h次这么多空间dword的数据内容,全部改成0CCCCCCCCh。
一个word是两个字节,dword是四个字节
从edi开始,到ebp停止,这么多空间内容全部初始化为CCCCCCCC
内存监视
文章图片
文章图片
此时main函数栈帧已经开辟好了,下面正式开始执行代码
文章图片
mov,把0Ah(十进制就是10)放到ebp-8的位置,我们认为CCCCCCCC为四个字节,为a开辟的空间就是ebp到ebp-8空间
1.我们是创建变量的时候赋值,如果创建变量是没有赋值,默认里面放的就是CCCCCCCC 打印烫烫烫的原因就是因为里面是随机值CCCCCCCC,不进行初始化,不同的编译器里面可能放的是不同的值
文章图片
内存监视a
0a 00 00 00是因为此编译器是小端存储
文章图片
内存监视b
14h是十六进制,转化为10进制是20,空了两个整形的位置
文章图片
文章图片
内存监视c
又是相差两个整形的位置
文章图片
文章图片
函数调用
文章图片
函数调用要传参,下面这两个动作是在传参么?
mov,把ebp-14h(也就是b)放到eax里面。push->eax,压栈
mov,把ebp-8(也就是a)放到ecx里面。push->ecx,压栈
答案确实在传参
文章图片
call指令是调用函数,现在我们记住call指令的地址
文章图片
执行完call指令,AA8地址放的是00 c2 14 50
压栈了call指令的下一个地址 ,为什么要记住这个地址?不要着急
文章图片
文章图片
call一调用后马上调用Add函数,Add函数调用完后需要返回,我需要回到call指令的下一条指令(call指令执行完)
进入函数内部(前几步的操作顺序跟前面main函数一样) 为Add函数准备栈帧
文章图片
首先push ebp(此时ebp还在维护main函数栈低) 把ebp值压到顶上
文章图片
内存监视(ebp压栈在call指令下一条指令地址)
文章图片
把esp的值给ebp
esp,0CCh是在为Add这次调用分配函数栈帧空间
文章图片
文章图片
函数加载地址
lea 把[ebp+FFFFFF34h]地址加载到edi里面,mov把33h 放到ecx里面,再把0XCCCCCCCCh的值放到eax里面
rep ->从edi开始向下到ebp之间所有位置初始化为0XCCCCCCCCh
文章图片
文章图片
执行计算任务
把0放到ebp-8的位置
文章图片
文章图片
执行加法,但是x,y在哪里?
把ebp+8的值放到eax里面,ebp+8找到了ecx
ecx是压栈压来的,我们可以叫他a‘,eax就是b'
把ebp+8的值放到eax里,再add把ebp+0ch的值加到eax里面
加法完成,eax变成30
文章图片
文章图片
文章图片
形参是怎么来的?我们有主动创建形参么?
没有,是因为我们在下面刚开始调用函数的时候就把参数传过来了
通过mov push mov push指令传参
push压栈先压b,再压a。c=Add(a,b)先传的b,在传的a,参数是从右向左传的
形参不是在Add函数内部创建的,而是回来找了压栈的空间 数据进行计算
文章图片
我们经常说,形参是实参的一份零时拷贝,这句话完全正确,改变形参不影响实参
函数返回
文章图片
大家有没有疑惑, 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函数栈低地址
文章图片
文章图片
文章图片
此时pop弹出后,ebp回归到main函数栈低
ret指令,esp回到了00C21450的地址,此地址是call指令下一条指令的地址,继续从call指令下一条开始执行(不仅要走的出去,还要回的来)
给esp加8,跳过形参a,b,形参a,b销毁
把eax的值放到ebp-20h,ebp-20h位置就是c的空间 ,eax值是和:30
返回值是怎么带回来的? 是我们首先放到eax寄存器里面,当我们回到函数放到局部变量c里面去
文章图片
文章图片
Add函数销毁知道了,main函数销毁流程也差不多,这就是函数栈帧创建销毁的过程
回归最开始的问题
文章图片
1.局部变量是怎么创建的首先为函数分配好栈帧空间,栈帧空间初始化好一部分空间以后,给我的局部变量在栈帧里面分配一点空间
2.为什么局部变量不初始化的值是随机值因为随机值是编译器放进去的,例如CCCCCCCC,如果初始化便把随机值覆盖了
3.函数是怎么传参的?传参的顺序是怎样的?当我们还没有去调用函数的时候,便已经push从右向左压栈压进去,当我们进入形参函数的时候,在Add函数里面,通过指针的偏移量回来找到了我们形参。
4.形参和实参是什么关系?形参实在压栈的时候开辟的空间,和实参值是相同的,空间是独立的,形参是实参的一份零时拷贝,这句话完全正确,改变形参不影响实参
5.函数调用是怎么做的?上面总结清楚了
6.函数调用的结果怎么返回的?调用之前就把call指令下一条指令的地址记住,把ebp调用函数的上一个函数的栈帧存进去,当函数调用完后返回的时候,弹出ebp,就能找到原始上一个函数的ebp,指针往下走就能找到esp地址
返回值是通过寄存器带回来的
文章图片
【C语言拯救者|C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)】函数的内部所创建的静态变量是在全局开辟的,今天我们画的都是在栈区上开辟的
推荐阅读
- C语言|【C语言】#define 定义常量和宏
- C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
- C语言基础|【C语言】函数栈帧的创建与销毁
- C语言|C语言实现扫雷游戏+优化(递归)
- c语言|初识c语言5——12.#define定义常量和宏、13.指针、14.结构体(struct关键字)
- c语言|链表刷题笔记(较难篇) (c实现)(跑路人笔记)
- c语言|(C语言底层逻辑)函数栈帧的创建和销毁讲解
- C语言|vscode配置C语言环境
- YY|【C语言】 扫雷游戏(保姆级的实现过程)