你看不见的函数栈帧的创建与销毁(C语言)

五千字从反汇编一步步详细讲述函数栈帧的创建与销毁

这篇文章带你深入了解函数参数的传递和销毁过程,形参实参的关系,和局部变量的创建等等。
------------------------------------------------------------文章较长,满满干货---------------------------------------------------------------
在这里插入图片描述


文章所用环境:windows x64 VS2013


预备知识

1 . 内存四区

在介绍函数栈帧开辟之前先草草画一个内存四区的图方便后续理解:
在这里插入图片描述
我们知道堆区和栈区是相对着使用空间的,堆区从低地址到高地址,而栈区从高地址到低地址

2 . 栈帧-----摘自百度百科

栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
---------------------------------说白了就是用来记录的。

3 . 寄存器

在我们了解了栈区的性质之后,接下来需要大概说明一下计算机至关重要的元件:寄存器
这里想要具体了解的可以去看看百度百科https://baike.baidu.com/item/%E5%AF%84%E5%AD%98%E5%99%A8

以下都是寄存器的一些种类:
在这里插入图片描述

其中最重要的就是ebp和esp
重要的事情说三遍!!

ebp和esp这两个寄存器是存放函数地址的 , 专门来维护栈帧

ebp和esp这两个寄存器是存放函数地址的 , 专门来维护栈帧
ebp和esp这两个寄存器是存放函数地址的 , 专门来维护栈帧

而栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。

寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

那么接下来我们通过一段代码和对应的汇编语言来说明

#include<stdio.h>
int add(int a, int b);
int main()
{
	int a = 10;
	int b = 20;
	int ret = add(a, b);
	printf("%d", ret);
	return 0;
}
int add(int a, int b)
{
	return a + b;
}
int main()
{
00C61400  push        ebp  
00C61401  mov         ebp,esp  
00C61403  sub         esp,0E4h  
00C61409  push        ebx  
00C6140A  push        esi  
00C6140B  push        edi  
00C6140C  lea         edi,[ebp+FFFFFF1Ch]  
00C61412  mov         ecx,39h  
00C61417  mov         eax,0CCCCCCCCh  
00C6141C  rep stos    dword ptr es:[edi]  
	int a = 10;
00C6141E  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00C61425  mov         dword ptr [ebp-14h],14h  
	int c = add(a, b);
00C6142C  mov         eax,dword ptr [ebp-14h]  
00C6142F  push        eax  
00C61430  mov         ecx,dword ptr [ebp-8]  
00C61433  push        ecx  
00C61434  call        00C61096  
00C61439  add         esp,8  
00C6143C  mov         dword ptr [ebp-20h],eax  
	printf("%d", c);
00C6143F  mov         esi,esp  
00C61441  mov         eax,dword ptr [ebp-20h]  
00C61444  push        eax  
00C61445  push        0C65858h  
00C6144A  call        dword ptr ds:[00C69114h]  
00C61450  add         esp,8  
00C61453  cmp         esi,esp  
00C61455  call        00C6113B  
	return 0;
00C6145A  xor         eax,eax  
}
00C6145C  pop         edi  
}
00C6145D  pop         esi  
00C6145E  pop         ebx  
00C6145F  add         esp,0E4h  
00C61465  cmp         ebp,esp  
00C61467  call        00C6113B  
00C6146C  mov         esp,ebp  
00C6146E  pop         ebp  
00C6146F  ret

这里想要了解汇编指令可以了解这篇文章:
https://blog.csdn.net/sinat_27382047/article/details/72810788反汇编指令


一、main函数栈帧的开辟

1) 地基:main函数也是被其他函数调用的

  • c语言中的main函数是被_start函数调用的,
  • 函数都是在栈上开辟栈帧的, 那么有栈帧必然需要ebp和esp去维护栈帧,
    ebp指向栈帧底部(高地址),esp指向栈帧顶部(低地址)

![在这里插入图片描述](https://img-blog.csdnimg.cn/6a26e8cd19c24106bd866a213cd45730.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MTQ4NDc4MA==,size_16,color_FFFFFF,t_70

2) 压栈:从已有地基打起从高地址往低地址盖大楼

008C1400  push        ebp  
008C1401  mov         ebp,esp  
008C1403  sub         esp,0E4h  
008C1409  push        ebx  
008C140A  push        esi  
008C140B  push        edi  
008C140C  lea         edi,[ebp-0E4h]  
008C1412  mov         ecx,39h  
008C1417  mov         eax,0CCCCCCCCh  
008C141C  rep stos    dword ptr es:[edi]  

PUSH 指令:首先减少ESP的值,再将源操作数复制到堆栈。操作数是16位的,则ESP减2(字节),操作数是32位的,则 ESP减4(字节),esp通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与Windows下的堆栈是不同的,请参考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多,esp也就越来越小;

  • 首先看第一段进行push指令压栈操作,从高地址往低地址把ebp压栈
    在这里插入图片描述
    并且esp调整指向栈帧顶部(也就是减少ESP的值,16位的,则ESP减2字节,32位的,则 ESP减4字节)
    在这里插入图片描述

接下来是move指令:mov ebp,esp , 就是把第二个参数拷贝到第一个参数。

即:把ebp的地址指向改为esp的地址,esp,ebp指向同一块
在这里插入图片描述

										sub   esp,0E4h

(这里sub是减,0E4h 是十六进制E4,也就是esp — E4,esp存放的地址减E4)
那么此时esp和ebp之间就分配了一定空间,即main函数的栈帧.

在这里插入图片描述

										 push ebx
										 push esi
										 push edi
				这三个都是寄存器,依然压栈操作,并且esp减少指向栈顶(低地址)

在这里插入图片描述

lea:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。
然而lea也同样可以实现mov的操作,例如:lea edi,[ebp-24h]

方括号表示存储单元,也就是提取方括号中的数据所指向的内容,然而lea提取内容的地址,这样就实现了把地址(ebp-24h)放入到了edi
lea edi,[ebp-E4h]


008C140C lea edi,[ebp-0E4h]
008C1412 mov ecx,39h
008C1417 mov eax,0CCCCCCCCh
008C141C rep stos dword ptres:[edi]

  • 这段的作用就是把ebp - 0E4h的地址赋给edi,39(十六进制)赋给ecx,把CCCCCCCC(十六进制)赋给eax
    并且从edi开始的以下39h个4字节都变成eax也就是CCCCCCCC
    注意这里39h = 57,57个4字节也就是57 × 4 = 228转化成十六进制就是0E4h,也就是从ebp到ebp - 0E4h的那段空间全都变成CCCCCCCC
    而CCCCCCCC也就是烫所对应的乱码
    在这里插入图片描述
							int a = 10;
							008C141E  mov         dword ptr [a],0Ah  
							int b = 20;
							008C1425  mov         dword ptr [b],14h  

那么我们关闭反汇编中的符号显示:

							int a = 10;
							008C141E  mov         dword ptr [ebp-8],0Ah  
							int b = 20;
							008C1425  mov         dword ptr [ebp-14h],14h 

接下来把十六进制0A也就是10拷贝给a,十六进制14也就是16+4=20拷贝给b
而a 的地址就是ebp - 8h,b的地址就是ebp - 14h也就是ebp - 20

在这里插入图片描述

二、代码中add函数栈帧的开辟

int ret = add(a, b);
00951943  mov         eax,dword ptr [b]  
00951946  push        eax  
00951947  mov         ecx,dword ptr [a]    
0095194A  push        ecx  
0095194B  call        009513B1  
00951950  add         esp,8  
00951953  mov         dword ptr [ebp-20h],eax  

1 ) 传参

							00951943  mov         eax,dword ptr [b]  
							00951946  push        eax
							00951947  mov         ecx,dword ptr [a]    
							0095194A  push        ecx   			

方括号表示提取方括号中的数据所指向的内容.
把b的值拷贝给eax,并且把eax压栈,同理也把a的值拷贝给ecx,并压栈
在这里插入图片描述

									> 008C1434  call        008C1096
									> call指令进行调用函数并保存地址

在这里插入图片描述

2 ) add函数:

代码如下(示例):

int add(int a,int b)
{
00C613C0  push        ebp  
00C613C1  mov         ebp,esp  
00C613C3  sub         esp,0C0h  
00C613C9  push        ebx  
00C613CA  push        esi  
00C613CB  push        edi  
00C613CC  lea         edi,[ebp+FFFFFF40h]  
00C613D2  mov         ecx,30h  
00C613D7  mov         eax,0CCCCCCCCh  
00C613DC  rep stos    dword ptr es:[edi]  
	return a + b;
00C613DE  mov         eax,dword ptr [ebp+8]  
00C613E1  add         eax,dword ptr [ebp+0Ch]  
}
00C613E4  pop         edi  
00C613E5  pop         esi  
00C613E6  pop         ebx  
00C613E7  mov         esp,ebp  
00C613E9  pop         ebp  
00C613EA  ret  
  • 还是一样进行add函数栈帧的开辟

00C613C0 push ebp
00C613C1 mov ebp,esp
00C613C3 sub esp,0C0h
00C613C9 push ebx
00C613CA push esi
00C613CB push edi
00C613CC lea edi,[ebp+FFFFFF40h]
00C613D2 mov ecx,30h
00C613D7 mov eax,0CCCCCCCCh
00C613DC rep stos dword ptr es:[edi]
同main函数一样不再赘述,这里注意存放ebp的是main的栈帧底部,并且此时esp ebp用来维护add函数的栈帧
在这里插入图片描述

					return a + b;
					00C613DE  mov         eax,dword ptr [ebp+8]  
					00C613E1  add         eax,dword ptr [ebp+0Ch]  
  • 接下来把ebp+ 8 那块地址所对应的值(如图也就是10)拷贝给寄存器eax
  • 并且把ebp +0Ch 的地址所对应 的值(也就是20)加到 eax

在这里插入图片描述

三、add函数栈帧的销毁

								00C613E4  pop         edi  
								00C613E5  pop         esi  
								00C613E6  pop         ebx  
								00C613E7  mov         esp,ebp  
								00C613E9  pop         ebp  
								00C613EA  ret 

pop:与push相反,esp每次加4(字节),数据出栈并放到后边寄存器里。
在这里插入图片描述
这里依次弹出也就是edi放到edi里,esi放到esi,ebx放到ebx

  • 此时edi,esi,ebx出栈,并把ebp拷贝给esp
    在这里插入图片描述
    在这里插入图片描述
  • 接着继续出栈,把栈顶弹出放到ebp里边。而此时的栈顶就是之前存放的Main函数的ebp
  									00C613E9     pop   bp 

在这里插入图片描述

  • pop之后ebp指向main函数的栈底
    在这里插入图片描述

ret:跳转会调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回值,则放入eax中

此时弹出栈顶(call指令下一条地址)并返回到该地址
在这里插入图片描述

形参的销毁

								00C61439  add         esp,8  
  • 紧接着esp往下加8个字节在这里插入图片描述
    在这里插入图片描述
  • 此刻形参销毁

流程图

在这里插入图片描述

评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李 天 真

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值