关于函数栈帧

       函数是编程中非常重要的部分。那么在我们关于函数的学习中,肯定或多或少有一些疑问。例如:局部变量是如何创建的呢?形参和实参有什么关系呢?函数是怎么样传递参数的呢?函数调用是如何做的呢?

       诸如此类的问题,相信下面关于函数栈帧的知识可能帮到你,拭目以待吧!

目录

一、什么是函数

二、什么是函数栈帧 

2.1 内存分区

2.2 栈

2.3 函数栈帧 

 三、相关寄存器和汇编指令

3.1 相关寄存器

3.2 相关汇编指令

 

四、函数栈帧的创建和销毁

4.1 main 函数栈帧的创建

4.2 main 函数局部变量的创建

4.3 函数调用

4.4 Add函数栈帧的创建

4.5 Add函数的运算

4.6 Add函数栈帧的销毁

五、相关知识

一、什么是函数

首先我们来回顾一下函数的概念,维基百科对函数的定义为子程序:

在计算机科学中,子程序(Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。

一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软 件库。

二、什么是函数栈帧 

2.1 内存分区

 首先,当一个由C/C++编译的程序运行的时候占用内存的分区可以简化为下图(并不完整):

2.2 栈

栈(stack)被定义为一种特殊的容器,是一种数据结构,是只允许在栈顶进行数据压入栈中(入栈push),也可以将已经压入栈中的数据弹出(出栈,pop)的操作受限制的线性表。栈的特点是后进先出(Last In First Out,简称 LIFO)

2.3 函数栈帧 

 在 C 语言中,每个函数的每次调用,都有它自己独立的一个栈帧(stack frame),函数栈帧就是函数调用过程中在程序的调用栈(stack)所开辟的空间。

 三、相关寄存器和汇编指令

3.1 相关寄存器

esp栈顶寄存器(存放栈顶指针)
ebp栈底寄存器(存放栈底指针)
eax通用寄存器之一,可以用于存储整数、指针、地址等数据。在函数调用中,eax被用来存储计算结果。

  

3.2 相关汇编指令

push数据入栈,给栈顶放一个元素 ,栈顶寄存器esp所存储位置改变
pop数据弹出至指定位置,同时栈顶删除一个元素。栈底寄存器ebp所存储位置改变
movmov     A, B,即将数据 B 转移到 A 中
ret根据地址返回到地址指向的空间
subsub     A, B   将寄存器A中的数值减去B
addadd     A, B   将寄存器A中的数值加上B
call调用函数指令
leaload effective address ,加载有效地址

 

四、函数栈帧的创建和销毁

 这里我们用一段简单的C语言代码来探索函数栈帧是如何创建和销毁的

#include <stdio.h>

int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 10; 
	int b = 20;
	int c = 0;

	c = Add(a, b);

	printf("%d\n", c);
	
	return 0;
}

这段代码相信大家都能很容易的理解,在这段代码中,一共有主函数main()和自定义函数Add() 。下面一起来探究一下他们到底是如何创建和销毁的。

在vs2019里面按F10调试这段代码之后按照步骤点调试->窗口->调用堆栈,在调用堆栈页面右击鼠标勾选"显示外部代码",可以发现如下图所示:

这里我们可以看到函数的调用关系,这里Add函数被main函数调用,同时main函数也被一个叫做invoke_main()的函数调用,当然在这个函数之前,也会有其他函数调用invoke_main()函数。这里说明main()函数也是被调用的函数。

因此,main()函数也会在内存的栈空间上面有属于其自己的栈帧,且也会有销毁和创建的过程。

接下来我们将代码调试到 main 函数开始执行的第一行,右击鼠标转到反汇编

4.1 main 函数栈帧的创建

通过上面的推理我们可知,main()函数是由invoke_main()调用的,而我们知道invoke_main()函数也会有一个栈顶指针esp和栈底指针ebp维护,如下图所示:

push  ebp

当程序执行到第一步push  ebp时,这里指将ebp的值压栈,即将ebp的值压到esp上面的一块空间,这时esp的值也应该向上一块空间,如图所示:

mov   esp,ebp

 这里指将ebp的值传给esp,通过对esp的值的监视发现确实如此

执行前

执行后 

sub     esp,0e4h

这里指将esp的值减去十六进制的0E4h,故我们这里在监视窗口可以看到变化,如下图所示:

我们知道,由于在计算器中我们是由高地址到低地址储存的,所以当esp减去一个数之后,esp的值便会指向低地址的某一块空间,这里的esp和ebp之间的空间就是为main()函数开辟的 ,如图所示:

push    ebx
push    esi
push    edi

接下来看下面这三行汇编代码。这三行汇编代码即将这三个值分别压栈到内存中。

第一步push   ebx

压栈之前

 压栈之后

 

可以从压栈前后观察到,压栈后esp的地址比压栈前esp的地址多了4个字节,而ebx的值也被压入了esp指向的地址所存放的空间。

当执行完这三条汇编指令之后我们可以观察到ebx ,esi ,edi三个的值均被压栈压入了esp和ebp所在的空间,而最后压入的edi所在的地址就是esp指向的空间。 

 详情如图所示:

 

lea   edi[ebp-FFFFFF1Ch]

 lea即加载有效地址,就是把后面的地址加载到edi里面。那么这里的[ebp-FFFFFF1Ch]是什么意思呢,这里我们右键找到显示符号名的一行并勾选,可以看到如下图所示:

这里的[ebp+FFFFFF1Ch]变成了[ebp-0E4h],根据上面我们知道[ebp-0E4h]所指向的空间如下图所示:

故这段汇编代码的意思为将这个地址放在edi中

mov          ecx,39h
mov          eax,0CCCCCCCCh
rep stos     dword ptr es:[edi]

mov   ecx,39h   即将39h的值放到ecx中;

mov   eax,0CCCCCCCCh  即将0CCCCCCCCh这个值放入eax中;

而真正产生效果的是第三行 :dword即double word,这句汇编代码加上面的三句汇编代码合起来的意思即为将从[ebp-0E4h]以后的内存的单位为四个字节一共39块空间都初始化为0CCCCCCCCh

执行前:

执行后: 

 到这里为main函数栈帧的创建就完成了

4.2 main 函数局部变量的创建

	int a = 10;
0043183B  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00431842  mov         dword ptr [ebp-14h],14h  
	int c = 0;
00431849  mov         dword ptr [ebp-20h],0 

mov         dword ptr [ebp-8],0Ah     即将 0xa(10) 存储到 ebp - 8 的地址处

mov         dword ptr [ebp-14h],14h    即将 0x14(20) 存储到 ebp - 0x14 的地址处

mov         dword ptr [ebp-20h],0     即将 0 存储到 exp - 0x20 的地址处

 

4.3 函数调用

ret = add(a, b);
mov        eax,dword ptr [ebp-14h]  
push       eax  
mov        ecx,dword ptr [ebp-8] 
push       ecx 
  

 这四句汇编代码意思分别为:

mov        eax,dword ptr [ebp-14h]    将 ebp - 0x14处存放的 20 放到 eax 寄存器中;

push       eax    将 eax 的值压栈;

mov        ecx,dword ptr [ebp-8]     将 ebp -0x 8 处存放的 10 放到 ecx 寄存器中;

push       ecx     将 ecx 的值压栈

如图所示:

00C2144B   call       00C210E1

这里的call是调用函数的意思,即调用Add函数,执行完call指令之后就会进入Add函数里面。当我们执行这条指令的时候会发现一个很重要的事情:

执行前

执行之后

我们发现执行前后红框中的数值发生了改变,esp下一块空间的内容被压入了一个地址,那么这个地址含义是上面呢?

 观察汇编语言之后我们发现这个00C21450就是call指令下一条指令的地址,其实这里是为了在调用完Add函数之后可以回到这里执行下面的内容。

 

 4.4 Add函数栈帧的创建

int Add(int x, int y)
{
push       ebp
mov        ebp,esp
sub        esp,0CCh
push       ebx
push       esi
push       edi
les        edi,[ebp+FFFFFF34h]
mov        ecx,33h
mov        eax,0CCCCCCCCh
rep stos   dword ptr es:[edi]

有没有发现这里的代码与我们开辟main函数的栈帧时候的代码非常相似,没错,这里就是在为Add函数开辟栈帧。

好了我们一步一步看这里的内容

push    ebp   将ebp的值压入栈中

可以看到左边ebp的值0x008ffba4已经被压入栈中,而通过右图我们可以看到其所在的地址就是esp里面的值

mov   ebp,esp    这里是指将esp的值压入ebp中,下图就是执行之后的结果

sub  esp,0CCh   即将esp的值减去0CCh,这里我们知道是在为Add函数开辟栈帧

push       ebx
push       esi
push       edi

这三行分别为三个寄存器开辟栈帧

从这里可以看到esp的值从0x008ffaa4变成了0x008ff9cc,而这个地址指向空间里面的值也是寄存器ebx的值

les        edi,[ebp+FFFFFF34h]
mov        ecx,33h
mov        eax,0CCCCCCCCh
rep stos   dword ptr es:[edi]

这四句代码就是为了初始化为Add函数开辟的这一块栈帧,与上面的main函数初始化相同

注意看,这里初始化范围正好是ebp所指向的地址上面到edi所指向地址中间的部分

 4.5 Add函数的运算

 首先mov   dword ptr [ebp-8],0 即将z的值0放入ebp-8的位置

然后我们来看一看系统怎么执行z=x+y的操作吧!

mov  eax,dword ptr [ebp+8]  将ebp+8的值放入eax里面

add   eax,dword ptr [ebp+0ch]   将ebp+0ch的值和eax里面刚才放入的值相加

mov   dword ptr [ebp-8],eax    将eax的值放入ebp-8的位置

这里我们用画图来清晰的看一下

 从内存中我们也可以看出来

总结一下这里的操作就是:首先Add函数先在其栈帧内开辟了一块空间储存z,又将之前为形参开辟好的空间里面的值传给eax寄存器,在eax寄存器里面完成加法之后又将得到的结果存到形参z所在的位置

有木有发现这里两个形参的值都是在Add函数栈帧开辟号之前开辟好的,而且这里之前传参的时候是先传形参b,也就是说main()函数传参的时候是从右往左传参的

我们都知道的是函数的临时变量出了函数就会被销毁,而这里也不是传址调用,那系统是如何将结果返回到main函数的呢?

4.6 Add函数栈帧的销毁

下一步是return  z,我们看一下是怎么返回的吧

mov   eax,dword ptr [ebp-8]

即将[ebp-8]中的值传到eax寄存器里面,原来在函数返回之前我们已经将刚刚计算成功的值放在了eax寄存器里面,而寄存器里面的值不会随着Add函数栈帧的销毁而消失,就是说函数最后的返回值就是由eax寄存器带回主函数的

pop      edi
pop      esi
pop      ebx
mov      esp,ebp
pop      ebp
ret

pop      edi    在栈顶弹出一个值,存放到edi中,同时esp+4

pop      esi    在栈顶弹出一个值,存放到esi中,同时esp+4

pop      ebx   在栈顶弹出一个值,存放到ebx中,同时esp+4

执行之后如图所示:

mov      esp,ebp   将ebp的值传给esp

pop      ebp    在栈顶弹出一个值,存放到ebp中,同时esp+4

此时,esp中的地址回到刚刚ebp的位置,而下一步弹出的值就是刚刚我们存放的main函数栈底位置,同时esp+4。此时esp和ebp所指向的这一块空间就是我们刚刚为main函数开辟的栈帧。

ret     根据地址返回到main函数里面

我们注意到,当我们刚刚在main函数中使用call调用Add函数的时候,我们将call指令下一条指令的地址压入了栈中,也就是此时esp中地址所指向的那一块空间,当我们这里的ret执行完之后便会返回到那条指令的位置

  

add   esp,8         esp中的值加8,如图所示:

mov     dword ptr [ebp-20],eax       将eax的值放入[ebp-20]的位置

eax的值就是刚刚Add函数计算后的结果,而 [ebp-20]的位置就是main函数存储局部变量c的值的位置,如图所示:

 最后就是main函数栈帧的销毁,与Add函数一样,main函数栈帧的销毁也是一样的步骤

五、相关知识

1、寄存器时集成到CPU上面的,他不属于内存空间,函数代码执行的时候会调用寄存器

2、局部变量不初始化内容随机的原因是:函数栈帧创建后会自动将空间中存储的值全部初始化为一个特定值,且编译器不同值也不同。 

3、栈帧的创建与销毁是通过对ebp和esp存储的地址的改变来实现的,在ebp和esp之间的空间才是系统分配的空间,其他的都是无权限的空间

That's it.

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

coderMiLong

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

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

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

打赏作者

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

抵扣说明:

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

余额充值