函数栈帧的创建与销毁

本文详细解释了C语言编译过程中,函数栈帧如何在VS2013环境下通过汇编语言被创建和销毁,涉及栈帧的边界定义、寄存器的作用以及参数传递的内存操作过程。
摘要由CSDN通过智能技术生成

函数栈帧的创建与销毁

我们都知道计算机语言可以划分为机器语言,汇编语言,高级语言三大类。其中机器语言,汇编语言都是对计算机硬件的直接操作,而计算机硬件能直接理解的语言只有由‘1’‘0’所组成的二进制序列串,显然这对人类来说不是多么友好,因此机器语言,汇编语言被冠以“低级”之名,为了降低计算机语言的学习门槛,同时也为了使程序具有更高的可移植性,高级语言诞生了。高级语言与汇编语言有什么关联呢?与汇编语言实际上是把二进制串用某些特定英文字母,字符串代替不同,高级语言与汇编语言并不是单纯的替代关系,而是一种转化关系,你可以这样理解:我先用汇编语言写出一道程序,然后在这个程序中执行高级语言程序,汇编语言程序会把高级语言中的各种指令转化成汇编语言,然后,汇编语言又会被汇编程序1替换成二进制串,这样计算机就可以读懂这些指令了。

下面我们将通过VS20132去观察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;
}

我们知道,函数实际上就是实现各种功能的子程序,如同一个人想要做某些事需要各种各样的信息一样,函数实现某些功能也需要各种各样的信息,比如函数的局部变量,参数,地址等,这些信息存在哪里呢?它们被存在内存中一个叫栈区的地方,构成了一个名为函数栈帧的区域,函数栈帧为函数的执行提供了必要的环境。为了便于对栈区中各种信息的查找,有必要引入地址这一概念,每个地址都相当于一个小房子,是计算机存储空间的基本单位,其容量是一个字节;如果把栈区比作一个水桶,那函数的信息就相当于水,水进入水桶会到水桶的底部,而信息进入栈区也会到达栈区的底部,需要注意的是,栈区的底部是高地址,顶部则是低地址,栈区写入信息是从高地址往低地址写的。
为了明确函数栈帧的边界或者范围,我们把函数栈帧的始终地址储存在两个寄存器中,起始地址或者说高地址存储在名为“ebp”的寄存器中,终止地址或者说低地址存储在名为“esp”的寄存器中。
下面进入实操环节,首先在int main()前设置断点,将光标移到该行,按下F9,int main()前出现红点断点即设置成功,随后按下F5或者依次点击“调试”“启动调试”进入调试模式,再点击“调试”“窗口”“调用堆栈”,随后多按几下F10,直到光标移到main函数的最后一行,然后再按下F10,就会发现,进入了一个名为“__tmainCRTStartup”的堆栈帧,箭头指到“mainret = main(argc, argv, envp);”这一行,这意味着main函数的返回值会赋给mainret,观察调用堆栈窗口会发现函数__tmainCRTStartup又是被函数mainCRTStartup所调用的;我们一直说main函数是程序的入口,就像一个人去找某个很大公共场所的入口时需要指引一样,计算机找main函数也是需要指引的,而__tmainCRTStartup和mainCRTStartup这两个函数的功能就是去引导计算机找到main函数。
现在,多按几下F10,直到程序跳出“crtexe.c”文件,重新进入“main.c”文件,然后右键鼠标,点击“转到反汇编”,点击查看选项,取消“显示符号名”就可以看到由C语言转化而成的汇编语言:

int main()
{
00461410  push        ebp  
00461411  mov         ebp,esp  
00461413  sub         esp,0E4h  
00461419  push        ebx  
0046141A  push        esi  
0046141B  push        edi  
0046141C  lea         edi,[ebp+FFFFFF1Ch]  
00461422  mov         ecx,39h  
00461427  mov         eax,0CCCCCCCCh  
0046142C  rep stos    dword ptr es:[edi]  
    int a = 10;
0046142E  mov         dword ptr [ebp-8],0Ah  
    int b = 20;
00461435  mov         dword ptr [ebp-14h],14h  
    int c = 0;
0046143C  mov         dword ptr [ebp-20h],0  

在对其进行逐个说明之前,我们先要明确一下当前"ebp"和“esp”这两个指针寄存器所指向的地址,之前我们就已经说过,“esp”和“ebp”是用来明确函数栈帧的边界和范围的,而main函数又是被名为__tmainCRTStartup的函数所调用的,所以当前ebp和esp所指向的地址应该是这样的:

在这里插入图片描述

然后我们就可以看汇编指令了:
第一行:push ebp,意思就是把寄存器ebp中储存的地址放置在现在调用的函数(也就是__tmainCRTStartup)栈帧之上,这个操作叫做“压栈”;压栈之后函数栈帧的范围就扩大了,esp所指向地址的地址就会自动发生变化,把地址ebp也给囊括进来。变成这样:

在这里插入图片描述
在这里插入图片描述

如果你想看的再清楚一点,那就打开“调试”“窗口”“监视”监视1“,再右键鼠标选中十六进制显示,对esp和ebp进行监视,现在箭头指向的是push ebp,我们按下F10,观察esp是否发生变化:

push ebp 执行前:
在这里插入图片描述

push ebp 执行后:
在这里插入图片描述

我们观察到,寄存器esp中储存的值由0x007df9f4变为了0x007df9f0,要注意,栈区中写入信息是从高地址往低地址写的,所以现象是符合预测的。

或者也可以打开“调试”“窗口”“内存”“内存1”实际看一下内存情况,在查找中输入esp回车,就可以看到esp所指向的地址中确实储存了ebp中的地址信息:
在这里插入图片描述

下一行是 mov ebp esp,意思是把esp中储存的地址赋给ebp。让我们观察一下监视窗口:
mov ebp,esp 执行前:
在这里插入图片描述

mov ebp,esp 执行后:
在这里插入图片描述
在这里插入图片描述

下一行是 sub esp,0E4h 意思就是把esp中存储的地址减去一个0E4h。
sub esp,0E4h 执行前:
在这里插入图片描述

sub esp,0E4h 执行后:
在这里插入图片描述
在这里插入图片描述

这个新创建的空间其实就是main函数的栈帧,而0E4h则是计算机通过一些算法计算出的,具体多少要看main函数。
下一行是push ebx 意思与上面的push ebp 相似,这里的ebx指的是寄存器ebx中存储的数据。push esi和push edi 于此类似,都是“压栈”操作。

push ebx:

在这里插入图片描述

push esi:

在这里插入图片描述

push edi:
在这里插入图片描述

接下来,为了方便观察,我们将“显示符号名”再次勾选上,汇编就变成了:

0046141C  lea         edi,[ebp-0E4h]  
00461422  mov         ecx,39h  
00461427  mov         eax,0CCCCCCCCh  
0046142C  rep stos    dword ptr es:[edi]   

lea 的全称是“load effective address” ,它与mov类似,区别是mov edi,[ebp-0E4h]指的是把地址为ebp-0E4h的数据赋给edi,而lea edi,[ebp-0E4h]是直接把ebp-0E4h这个地址赋给edi;mov ecx,39h 是把39h赋给ecx; mov eax,0CCCCCCCCh 是把0CCCCCCCCh 赋给eax;rep是重复的意思,重复干什么呢?重复stos,stos又是什么呢?stos是赋值的意思,怎么赋值呢?是把eax中的数据(也就是0CCCCCCCCh ) 赋给从地址为edi开始(es:[edi])的双字节数据(dword ptr),重复39h次,每重复一次,edi所指向的地址就变化一次,如果设置了direction flag, 那么edi所指向的地址会减小, 如果没有设置direction flag, 那么edi所指向的地址会增大,这里没有设置direction flag,所以是增大,我们来对比一下:
rep stos dword ptr es:[edi]执行前
在这里插入图片描述

rep stos dword ptr es:[edi]执行后
在这里插入图片描述

可以看到,有一大片地址所储存的数据都变成了 0CCCCCCCCh,而在执行后,edi的值也等于ebp了。
还是为了观察,再取消“显示符号名”:

    int a = 10;
0046142E  mov         dword ptr [ebp-8],0Ah  
    int b = 20;
00461435  mov         dword ptr [ebp-14h],14h  
    int c = 0;
0046143C  mov         dword ptr [ebp-20h],0  

下一行是mov dword ptr [ebp-8],0Ah,意思就是把0Ah(也就是10),赋给ebp-8处的(dword ptr)双字节数据,这就是对局部变量a进行初始化,所以你现在知道为什么要对局部变量初始化了吧,如果不初始化,现在的a==0CCCCCCCCh;mov dword ptr [ebp-14h],mov dword ptr[ebp-20h],0 都是一样的,都是给某个地址处的数据赋值,你可以观察内存窗口,获得更直观的理解。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

c = Add(a, b);
00461443  mov         eax,dword ptr [ebp-14h]  
00461446  push        eax  
00461447  mov         ecx,dword ptr [ebp-8]  
0046144A  push        ecx  
0046144B  call        004610E1 
00461450  add         esp,8  
00461453  mov         dword ptr [ebp-20h],eax  

mov eax,dword ptr [ebp-14h] 的意思是把ebp-14h处的数据(也就是20)赋给寄存器eax;push eax是压栈。
压栈前:

在这里插入图片描述

压栈后:

在这里插入图片描述

push ecx mov ecx,dword ptr [ebp-8]也是同样道理:
压栈后:

在这里插入图片描述

这两步实际上就是对函数Add准备进行传参。

call 004610E1 可以细分为两步:
第一步,跳转到Add处,第二步,对下一个指令的地址(也就是00461450)进行压栈处理。

执行前:
在这里插入图片描述

按下F11,而不是F10,执行call 004610E1:

在这里插入图片描述

在这里插入图片描述

再次按下F11,正式进入Add函数:

在这里插入图片描述

int Add(int x, int y)
{
004613C0  push        ebp  
004613C1  mov         ebp,esp  
004613C3  sub         esp,0CCh  
004613C9  push        ebx  
004613CA  push        esi  
004613CB  push        edi  
004613CC  lea         edi,[ebp+FFFFFF34h]  
004613D2  mov         ecx,33h  
004613D7  mov         eax,0CCCCCCCCh  
004613DC  rep stos    dword ptr es:[edi] 

这些指令和上面都是一样的,就不说了。

push ebp:

在这里插入图片描述

mov ebp,esp

在这里插入图片描述

sub esp,0CCh
在这里插入图片描述

push ebx

在这里插入图片描述

push esi

在这里插入图片描述

push edi

在这里插入图片描述

勾选“显示符号名”

004613CC  lea         edi,[ebp-0CCh]  
004613D2  mov         ecx,33h  
004613D7  mov         eax,0CCCCCCCCh  
004613DC  rep stos    dword ptr es:[edi]  

也是把Add函数的栈帧中的数据初始化成0CCCCCCCCh。

初始化前:
在这里插入图片描述

初始化后:
在这里插入图片描述

然后取消“显示符号”:

int z = 0;
004613DE  mov         dword ptr [ebp-8],0  
    z = x + y;
004613E5  mov         eax,dword ptr [ebp+8]  
004613E8  add         eax,dword ptr [ebp+0Ch]  
004613EB  mov         dword ptr [ebp-8],eax  
    return z;
004613EE  mov         eax,dword ptr [ebp-8]  
}

mov dword ptr [ebp-8],0 的意思是,把储存在ebp-8处的数据赋为0(也就是z)。

mov eax,dword ptr [ebp+8] 的意思是,把ebp+8处地址的数据赋给eax,我们之前不是说有两步是对函数Add准备传参吗?那ebp+8处的数据不就是ecx(即10)吗。

add eax,dword ptr [ebp+0Ch] 意思是把地址为ebp+0Ch(0Ch就是12)的数据eax(20)加到(add的意思)eax中,10+20等于30,所以现在eax==30。

mov dword ptr [ebp-8],eax 就是把eax中的数据赋给地址为ebp-8处的数据(即z)。

moveax,dword ptr [ebp-8] 就是把地址为ebp-8处的数据(z)赋给eax.

004613F1  pop         edi  
004613F2  pop         esi  
004613F3  pop         ebx  
004613F4  mov         esp,ebp  
004613F6  pop         ebp  
004613F7  ret    

然后是pop edi ,pop与push相对,push是压栈,而pop是出栈:并且把栈的数据赋给edi

在这里插入图片描述

pop esi :

在这里插入图片描述

pop ebx :

在这里插入图片描述

mov esp,ebp 就是把esp指向的地址变为ebp指向的地址,就变成了这样:

在这里插入图片描述

pop ebp: ebp所指向的地址变为main的ebp:

在这里插入图片描述

ret 就是跳转到00461450,并弹出00461450 :当初储存00461450就是为了方便回来。
弹出前:

在这里插入图片描述

弹出后:

在这里插入图片描述
在这里插入图片描述

c = Add(a, b);
00461450  add         esp,8  
00461453  mov         dword ptr [ebp-20h],eax   

add esp,8 :把esp所指向的地址+8 如下:

在这里插入图片描述

mov dword ptr [ebp-20h],eax :把eax中的值赋给地址为ebp-20h的数据,要注意eax中的值是Add的返回值。所以你现在知道这个返回值是怎么返回的吧。


  1. 这里的汇编程序与之前提到的汇编语言程序并不是同一概念,汇编语言程序指的是用汇编语言写的程序,而汇编程序是用机器语言写的,它的作用是把那些特定英文字母,字符串重新翻译成二进制串。 ↩︎

  2. VS各版本下载链接:https://pan.baidu.com/s/1Jm7z5jNUzVVA0C0CSIN-hg 提取码:1k58! ↩︎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值