入门C语言第二话:函数(下)之心法篇,带你修炼内功,从内存的角度理解函数调用与销毁(函数栈帧),

前言:

在这里插入图片描述

概念引入

  在这个资源共享的时代,共享单车等带给我们极大的便利,当你使用共享单车后,你是不是要把共享单车放在停车地点?想象一下,把使用共享单车看做内存的一部分,把操作系统看做停车场,那么,使用者占用内存,就是你在骑共享单车的那一段时间,放回共享单车就是把内存还给操作系统,像这样动态化,在栈区合理地使用内存,就是我们所说的函数的栈帧了!

栈帧

 定义:一个函数执行的环境——就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
1.函数参数和函数返回值
2.临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
3.保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

那什么是栈?什么又是帧呢?

栈:计算机的一块内存,我们临时变量和函数一般在栈区开辟空间。
帧:函数和临时变量所使用的空间就是帧。
把这两个词合起来:在栈区为函数运行开辟的一块空间就是栈帧了!

到这里我们第一个目标就完成了。

注意:栈区的使用是从高地址到低地址使用的,就像堆得很高的一层书一样,先从最高处拿,再从低处拿。
在这里插入图片描述

寄存器

定义:CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。

我们这篇文章所用的寄存器:
ebp:栈底寄存器
esp:栈顶寄存器
功能:维护当前正在使用的函数空间
eax:通用寄存器,保留临时数据,常用于返回值,也用于赋值。
ebx:通用寄存器,保留临时数据——通常是用于初始化用户栈
eip: 指令寄存器,保存当前指令的下一条指令的地址,确保上一条指令能够继续运行下去。
反汇编指令:
mov:数据转移指令,也就是赋值操作
push:数据入栈,同时esp栈顶寄存器也要发生改变,向低地址处移动。
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变,向高地址处移动。
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令

rep:重复前缀指令,不可独立使用。
用途:重复执行一条指令。
形式:位于 stos、lod、ins、outs等传送指令之前,如 rep stosd edi。
运行机制:rep指令是重复执行该指令后面的汇编代码,执行次数由寄存器ecx控制。

stos:传送指令,可独立使用。
用途:将eax(ax,al)寄存器的值传送到指定的内存单元
形式:stos edi,其中edi为CPU寄存器,该寄存器存有目标内存的地址,为寄存器寻址。
运行机制: 将寄存器eax中的内容传输到寄存器EDI所指向的内存单元中。

lod:load effective address加载有效地址
图解:
在这里插入图片描述
在这里插入图片描述

恭喜你!到这里第二个目标也解决了。
知识准备工作已经完成,下面我们来举个例子详细理解

栈帧的调用

举例函数

#include <stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 3;
	int b = 5;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

准备阶段:

为了让我们研究函数栈帧的过程足够清晰,不要太多干扰,我们可以关闭下面的选项,让汇编代码中排
除一些编译器附加的代码:
要执行以下操作:
在这里插入图片描述
先调用main函数,那么如何调用main函数呢?
在这里插入图片描述
从这里我们可以看出main函数是由invoke_main 来调用的,那么我们有理由推断invoke_main 也是由一个函数调用的,并且每一部分都有自己的ebp和esp来维护自己的栈帧,这里就不多说了。

到这里第三个问题就解决了。

调用阶段

开始转到反汇编
那么我们会看到如下代码:

int main()
{
	//函数栈帧的创建
	00BE1820 push ebp
	00BE1821 mov ebp,esp
	00BE1823 sub esp,0E4h
	00BE1829 push ebx
	00BE182A push esi
	00BE182B push edi
	00BE182C lea edi,[ebp-24h]
	00BE182F mov ecx,9
	00BE1834 mov eax,0CCCCCCCCh
	00BE1839 rep stos dword ptr es:[edi]


	//main函数中的核心代码
	int a = 3;
	00BE183B mov dword ptr [ebp-8],3
	int b = 5;
	00BE1842 mov dword ptr [ebp-14h],5
	int ret = 0;
	00BE1849 mov dword ptr [ebp-20h],0

   
	ret = Add(a, b);
	00BE1850 mov eax,dword ptr [ebp-14h]
	00BE1853 push eax
	00BE1854 mov ecx,dword ptr [ebp-8]
	00BE1857 push ecx
	00BE1858 call 00BE10B4
	00BE185D add esp,8
	00BE1860 mov dword ptr [ebp-20h],eax


	printf("%d\n", ret);
	00BE1863 mov eax,dword ptr [ebp-20h]
	00BE1866 push eax
	00BE1867 push 0BE7B30h
	00BE186C call 00BE10D2
	00BE1871 add esp,8
	return 0;
	00BE1874 xor eax,eax
}

下面一句一句的拆解代码:

1.栈帧的创建

起点从上一步开始——也就是invoke_main 调用结束:

00BE1820 push ebp//将ebp(一个整形)压入栈顶也就是下图esp指向元素的上一个元素,然后将esp在向上移动四个字节指向ebp(由于是低地址所以要减去四个字节),这个过程就叫做压栈,此时的ebp为目前ebp的值也就是invoke_main栈底的地址。

在这里插入图片描述

	
	00BE1821 mov ebp,esp
	//将esp的值赋值给ebp,esp的值不变,这时,esp就会指向原来esp指向的元素,也就是ebp这时的ebp为main函数的栈底
	00BE1823 sub esp,0E4h
	//然后,esp减上个0E4h(十六进制值为228)这么大的空间作为main函数的栈帧 (空间)
	00BE1829 push ebx
	//将ebx放在main函数的栈顶然后esp再减去4个字节
	00BE182A push esi
	//再将esi的值放在main函数的栈顶然后esp再减去4个字节
	00BE182B push edi
	//再将edi的值放在main函数的栈顶然后esp再减去4个字节
	00BE182C lea edi,[ebp-24h]
	//将ebp(main函数的栈底)减去24h(36)字节大小
	00BE182F mov ecx,9
	//将9的值放在ecx中。为啥是9?因为9*4(整形所占的空间)=36(24h)是为了将ebp到adi的值改为目标值,也就是下一句话。
	00BE1834 mov eax,0CCCCCCCCh/
	/0CCCCCCCCh的值放在eax中
	00BE1839 rep stos dword ptr es:[edi]
	//重点:
	//这句话的意思是从edi处开始,将eax的值放入从edi指向的值中,执行过后,edi减去四个字节(dword————双字(一个字占两个字节))重复9次操作(ecx的值存的是操作次数)
	// 操作类似循环,下面用伪代码辅助理解

伪代码理解:

	edi = ebp - 0x24;
	ecx = 9;
	eax = 0xCCCCCCCC;
	for (; --ecx; edi += 4)
	{
		*(int*)edi = eax;
	}

到这里赋值后会使用0xccccc的空间为变量的空间,如果字符只定义不初始化那么就会出现烫烫的文字,0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,因此会打印烫字。
到这第四个问题就解决了。

2.变量的空间创建

重点代码:

	int a = 3;
	00BE183B mov dword ptr [ebp-8],3
	//将3放在ebp减8个字节所指地址的值处,这里存放的就是a变量
	int b = 5;
	00BE1842 mov dword ptr [ebp-14h],5
	//将5放在ebp减20个字节所指地址的值处,这里存放的就是b变量
	int ret = 0;
	00BE1849 mov dword ptr [ebp-20h],0
	//将0放在ebp减20个字节所指地址的值处,这里存放的就是ret变量
3.函数的传参
	ret = Add(a, b);
	00BE1850 mov eax,dword ptr [ebp-14h]
	//将ebp减去20字节地址处的值,也就是b放在eax中
	00BE1853 push eax
	//将eax放在栈顶处,esp再减去四个字节指向eax
	00BE1854 mov ecx,dword ptr [ebp-8]
	//将ebp减去8字节地址处的值,也就是a放在ecx中
	00BE1857 push ecx
	//将ecx放在栈顶处,esp再减去四个字节指向eax
	//这几句汇编代码其实就是将实参复制作为形参,放在栈顶处供所调用函数的使用。
	//第五个问题就解决了
4.函数的调用与销毁

call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方(也就是记住你要回去的地址),继续往后执行。(知道要去哪,并且要知道咋回去,要不然就回不去了)

	00BE1858 call 00BE10B4
	//调用add函数的指令跳到add函数里
int Add(int x, int y)
{
00BE1760 push ebp //将main函数栈帧的ebp的值放在栈顶,esp减4字节
00BE1761 mov ebp,esp //将main函数的esp的值赋值给ebp,ebp现在是Add函数的ebp
00BE1763 sub esp,0CCh //给esp减0xCC,这是Add函数的esp
00BE1769 push ebx //将ebx的值放在栈顶,esp减4字节
00BE176A push esi //将esi的值放在栈顶,esp减4字节
00BE176B push edi //将edi的值放在栈顶,esp减4字节
int z = 0;
00BE176C mov dword ptr [ebp-8],0 //将0放在ebp-8的地址所指向的值处,其实就是变量z的创建
z = x + y;
//接下来计算的是x+y,结果保存到z中
00BE1773 mov eax,dword ptr [ebp+8] //将ebp加8字节地址处所存的数字存储到eax中
00BE1776 add eax,dword ptr [ebp+0Ch] //将ebp加12字节地址处所存的数字加到eax寄存器中
00BE1779 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp减8字节的地址所指向的值处,其实就是放到z中
return z;
00BE177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。因为寄存器是集成在cpu上的不会随着函数的销毁而销毁
}
//到这第六个问题就解决了。
00BE177F pop edi
//将此时栈顶所存的值赋给edi,esp加上四个字节
00BE1780 pop esi
//将此时栈顶所存的值赋给esi,esp加上四个字节
00BE1781 pop ebx
将此时栈顶所存的值赋给esi,esp加上四个字节
00BE1782 mov esp,ebp
//将此时ebp的值赋给esp,这里的esp就为main函数的esp
00BE1784 pop ebp
//将此时栈顶也就是main函数的ebp(mian函数的ebp)所存的值赋给ebp(add函数的ebp),esp加上四个字节
00BE1785 ret
//此时返回call指令的下一条指令的地址,然后接着执行,并且将地址弹出,esp加上四个字节
5.函数的返回
//返回到这个地址继续往下执行
	00BE185D add esp,8
	//将8加到esp上,也就是指向edi
	00BE1860 mov dword ptr [ebp-20h],eax
	//将eax也就是求和的值,放在ebp-20h所指向的值处,也就是ret里面

那么到这就是函数的调用与销毁的全过程了,至于main函数的销毁也大同小异,就交给你了。
整体的流程图:
在这里插入图片描述

总结

尝试回答下列问题:如果对答如流,说明你的内功已经炉火纯青了,恭喜恭喜!如果没有还需要加油哦!
在这里插入图片描述

第一个问题:函数执行的环境——为函数运行所开辟的一块空间。
第二个问题:存放函数的参数返回值,临时变量,保存上下文信息。
第三个问题:参考目录。
第四个问题:invoke_main 来调用的
第五个问题:两个0xccc转换成字符即为汉字“烫”
第五个问题:参考整体的流程图:
在这里插入图片描述
第六个问题:返回值存在eax返回,eax为寄存器集成在cpu上
不会随着函数的销毁而销毁。(可以把eax看成全局变量)

尾序

如果能认真看到这里,我坚信你能收获很多很多!也希望这篇文章能帮助到你,如果觉得不错,请点击一下不要钱的赞,如果有误请温柔的指出,在这里感谢大家了!

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值