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语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第1张图片

1.寄存器

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第2张图片

电脑的存储体系,其中寄存器是集成到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语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第3张图片

大家有没有疑惑过,main函数被调用起来了,但是main函数是被谁调用的?

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第4张图片

往下走,将代码执行完,可以看见__tmainCRTStartup,这个函数内部调用了main函数 ,说明main函数也是被别人调用的,__tmainCRTStartup又是被tmainCRTStartup调用

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第5张图片

在main函数之前也应该分配这两个函数的空间 

 C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第6张图片

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


进入反汇编

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第7张图片

main函数也是被别人调用,那调用main函数的__tmainCRTStartup已经被创造好了空间

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第8张图片


反汇编第一句 (push压栈:给栈顶放一个元素)(pop出栈:从栈顶删除一个元素)

ebp存的是__tmainCRTStartup栈低的地址,push叫压栈,给栈里面放元素

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第9张图片

push完后,esp的地址也应该改变

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第10张图片

 监视

观看esp和ebp,这是初始地址

当我们开始执行push后 ,地址减了4

内存

可以看见, ebp确实被push压进去


反汇编第二句话

 mov指令是把后面的值赋到前面,把esp给ebp

 C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第11张图片


反汇编第三句话

 sub是减法,给esp减去0E4h,0E4h转化为十进制为228

监视(esp地址发生改变,为main函数开辟了一块很大的空间)

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第12张图片

        

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第13张图片


反汇编第四,五,六句话

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第14张图片

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第15张图片


 反汇编第七,八,九,十句话,lea =load effective address(加载有效地址)

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

 

 mov  把39h放到ecx里面去。 把0CCCCCCCCh的值放进eax里面

rep stos是指,要把刚刚从edi这个位置开始,向下的39h次这么多空间dword的数据内容,全部改成0CCCCCCCCh。

一个word是两个字节,dword是四个字节

从edi开始,到ebp停止,这么多空间内容全部初始化为CCCCCCCC

内存监视

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第16张图片

 C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第17张图片


此时main函数栈帧已经开辟好了,下面正式开始执行代码

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第18张图片

mov,把0Ah(十进制就是10)放到ebp-8的位置,我们认为CCCCCCCC为四个字节,为a开辟的空间就是ebp到ebp-8空间

1.我们是创建变量的时候赋值,如果创建变量是没有赋值,默认里面放的就是CCCCCCCC

打印烫烫烫的原因就是因为里面是随机值  CCCCCCCC,不进行初始化,不同的编译器里面可能放的是不同的值

 C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第19张图片

内存监视a

0a 00 00 00是因为此编译器是小端存储

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第20张图片

 内存监视b

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

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第21张图片

内存监视c

又是相差两个整形的位置

 
C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第22张图片


函数调用

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第23张图片

 函数调用要传参,下面这两个动作是在传参么?

mov,把ebp-14h(也就是b)放到eax里面。push->eax,压栈

mov,把ebp-8(也就是a)放到ecx里面。push->ecx,压栈

答案确实在传参

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第24张图片

 call指令是调用函数,现在我们记住call指令的地址

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第25张图片

执行完call指令,AA8地址放的是00 c2 14 50

压栈了call指令的下一个地址 ,为什么要记住这个地址?不要着急

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第26张图片

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第27张图片

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


进入函数内部(前几步的操作顺序跟前面main函数一样)

为Add函数准备栈帧

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第28张图片

首先push ebp(此时ebp还在维护main函数栈低) 把ebp值压到顶上 

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第29张图片

内存监视(ebp压栈在call指令下一条指令地址)

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第30张图片

把esp的值给ebp

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

 C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第31张图片

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第32张图片


函数加载地址

lea 把[ebp+FFFFFF34h]地址加载到edi里面,mov把33h 放到ecx里面,再把0XCCCCCCCCh的值放到eax里面

rep ->从edi开始向下到ebp之间所有位置初始化为0XCCCCCCCCh

 C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第33张图片

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第34张图片


执行计算任务

 把0放到ebp-8的位置

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第35张图片

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第36张图片


执行加法,但是x,y在哪里?

把ebp+8的值放到eax里面,ebp+8找到了ecx

ecx是压栈压来的,我们可以叫他a‘,eax就是b'

把ebp+8的值放到eax里,再add把ebp+0ch的值加到eax里面

加法完成,eax变成30

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第37张图片

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第38张图片


形参是怎么来的?我们有主动创建形参么?

没有,是因为我们在下面刚开始调用函数的时候就把参数传过来了

通过mov push mov push指令传参

push压栈先压b,再压a。c=Add(a,b)先传的b,在传的a,参数是从右向左传的

形参不是在Add函数内部创建的,而是回来找了压栈的空间 数据进行计算

我们经常说,形参是实参的一份零时拷贝,这句话完全正确,改变形参不影响实参


函数返回

 C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第39张图片

大家有没有疑惑, 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语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第40张图片

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第41张图片


此时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语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第42张图片


Add函数销毁知道了,main函数销毁流程也差不多,这就是函数栈帧创建销毁的过程

回归最开始的问题

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第43张图片

 1.局部变量是怎么创建的

首先为函数分配好栈帧空间,栈帧空间初始化好一部分空间以后,给我的局部变量在栈帧里面分配一点空间

2.为什么局部变量不初始化的值是随机值

因为随机值是编译器放进去的,例如CCCCCCCC,如果初始化便把随机值覆盖了

3.函数是怎么传参的?传参的顺序是怎样的?

当我们还没有去调用函数的时候,便已经push从右向左压栈压进去,当我们进入形参函数的时候,在Add函数里面,通过指针的偏移量回来找到了我们形参。

4.形参和实参是什么关系?

形参实在压栈的时候开辟的空间,和实参值是相同的,空间是独立的,形参是实参的一份零时拷贝,这句话完全正确,改变形参不影响实参

5.函数调用是怎么做的?

上面总结清楚了

6.函数调用的结果怎么返回的?

调用之前就把call指令下一条指令的地址记住,把ebp调用函数的上一个函数的栈帧存进去,当函数调用完后返回的时候,弹出ebp,就能找到原始上一个函数的ebp,指针往下走就能找到esp地址

返回值是通过寄存器带回来的

C语言拯救者 番外篇 (函数栈帧的创建和销毁讲解)_第44张图片

函数的内部所创建的静态变量是在全局开辟的,今天我们画的都是在栈区上开辟的

你可能感兴趣的:(C语言拯救者,c语言,开发语言,后端)