0基础C语言自学教程——第九节 从底层汇编的角度简单理解函数栈帧的创建和销毁

 

写在前面:

你想知道为什么用vs在越界的时候会打出“烫烫烫烫烫”?

你想知道临时变量在函数调用结束后是怎么销毁的吗?

欢迎关注我♥,订阅专栏 0基础C语言保姆教学

就可以持续读到我的文章啦😀🐕~~~~

这里都是满满的干货,从零基础开始哦~,循序渐进😀,直至将C中知识基本全部学完🐂。

本文为第九节——从底层汇编的角度简单理解函数栈帧的创建和销毁  文末附前八张链接哟👉

我们在现在,其实已经比较清楚函数是怎么样运行的了,包括怎样传参 、函数调用等等。但是呢,这样也只是理解到了会用的地步,

其底层的原理是怎样的,到底是如何调用的?我们本节内容将会来做详细探讨。

首先,我们需要知道,函数栈帧的创建和销毁是在栈区中完成的。每一次地函数调用都有栈帧的创建和销毁。

而系统在栈区内使用地址时是从高地址往低地址使用。就是说,先使用高地址,再使用低地址。

我们简单地画一个图

 然后,我们需要了解这两个寄存器:ebp 和 esp

它们都是在函数创建栈帧的时候来去使用。用来维护函数栈帧。

其中,

ebp(栈底指针),存储着栈底的地址

esp(栈顶指针),存储着栈顶的地址

我们简单地来去写一下这么个程序:

为了便于理解,我将此代码拆分地足够细。 

ok,现在我们开始来看其底层到底是怎么实现的。

我们按住F10,让代码运行起来,然后转到反汇编,打开内存和监视。

 反正就看到这样一个乱七八糟的东西。

左边一行一行的实际上都是汇编代码,需要注意一下的是,第11行和12行我们暂不分析,因为这是vs2019自己弄的东西,进行了一些优化。如果用vs2013甚至更老的版本就基本不会出现。(不同的编译器、环境对函数栈帧创建销毁的过程大同小异)

要注意,首先需要为main函数创建栈帧,所以,我们前面的若干行都是在为main函数搞事情。

我们来一点一点分析:

前三行:

002617C0  push        ebp 

它的意思是压栈,将ebp压栈。

002617C1  mov         ebp,esp 

意思是将ebp的地址的那个值给esp。

此时,我们的栈区的图可以理解为这样:

 

 (此时的栈区图)

接着,

002617C3  sub         esp,0E4h

表示将esp的值减掉0E4h,0E4h是一个十六进制数字,代表的是0x00 00 00 e4

那么这个时候,这个图就变成了这样:

 我们可以让代码走起来,来看看ebp,esp的值是不是像我们所说的那样。

确实是这样。

我们接着往下看:

4-6行: 

002617C9  push        ebx  
002617CA  push        esi  
002617CB  push        edi 

它们的意思都是一样的。push...     就是将...压入栈中

他分别将ebx  esi   edi压入栈中(它们都是寄存器的类型)

那么此时,我们得到的栈区图就是这样:

 

 第七行到第十行是为了干一件事情,我们来看:

002617CC  lea         edi,[ebp-24h]  
002617CF  mov         ecx,9  
002617D4  mov         eax,0CCCCCCCCh  
002617D9  rep stos    dword ptr es:[edi]  

002617CC  lea         edi,[ebp-24h]   
//含义为读取ebp-24h到edi之间的地址,将ebp-24h赋给edi

mov         ecx,9  
//含义是将ecx的值变为9

同理,

mov         eax,0CCCCCCCCh
//含义是将eax的值变为0CCCCCCCCh

继续,

002617D9  rep stos    dword ptr es:[edi]  

//意为把从edi下ecx(0Ch)个数据
(或者说dword这么多次、这么多个数据)全部都改为eax的0CCCCCCCCh
然后让edi存储着ebp的值

另外注意,它这里是弄了9次,但每一次是一个dword,double word,4个字节。一个cc是一个字节。所以你应该看到的是36个cc。所以恰好是对应的次数关系,并没有多或者少。

接着,我们刚刚所画的栈区图就可以表示成了这个样子:

这也就解释了为什么我们有的时候越界会打出来“烫烫烫烫”,因为0xCCCCCCC所对应的就是“烫”字 

 

来看接下来的三行

    int a = 10;
002617E5  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
002617EC  mov         dword ptr [ebp-14h],14h  
	int c = 0;
002617F3  mov         dword ptr [ebp-20h],0  

 我们将我们的源代码也复制了上来。

还是一行一行来分析:

002617E5  mov         dword ptr [ebp-8],0Ah 
//意为将ebp - 8的位置dword(通俗来说就是赋值)成0Ah
(0Ah是一个十六进制数,就是0x00 00 00 0A,刚好是我们a的值10)

我们此时的栈区的图可以画成这样: 

 下面的两行就同理了:

002617EC  mov         dword ptr [ebp-14h],14h 
//将ebp - 14h的位置赋值为14h
002617F3  mov         dword ptr [ebp-20h],0  
//将ebp-20h的位置赋值0

那么这个图再添加两个变量值

 好的,接下来我们来看一看如何调用Add函数呢?

我们两行两行来看:

002617FA  mov         eax,dword ptr [ebp-14h]  
002617FD  push        eax  
//将 ebp -14h 位置的值赋值给到eax里
  然后让eax压栈

 注意到,ebp - 14h恰好就是我们要传的参数b的位置。

下面两行同理:

002617FE  mov         ecx,dword ptr [ebp-8]  
00261801  push        ecx
将 ebp -8 位置的值赋值给到ecx里
  然后让ecx压栈

那么,现在的栈区图就可以理解成这样:

继续往下,然后我们需要按住F11        

call  002613BB

表示调用函数,并记住call下面的一行指令的地址(也就是00261807)

 F11按进去,

call 002613BB

我们便来到了指令为002613BB的这么一行汇编代码

jmp         002625D0

意为跳转到002625D0

继续按住F11

 此时正式进入自定义函数中。

由刚刚的jump 002625D0,我们便跳转到这么一行汇编指令上去。

从这一行开始分析。

 我们可以看到,从第一行到第十行与在main函数的如出一辙

同样,让ebp的值压栈;

然后将ebp的值给esp;

让esp减去0CCh

压栈ebx、esi、edi;

将ebp-0Ch赋给edi,然后向下读出3个dword,赋值成eax的0CCCCCCh;

然后再将edi变成ebp的值。

 

 接下来,又是同样的配方,

    int z = 0;
002625F5  mov         dword ptr [ebp-8],0 
//让ebp-8位置的值变成0
	z = x + y;
002625FC  mov         eax,dword ptr [ebp+8]  
//将ebp+8的位置的值复制到eax中
002625FF  add         eax,dword ptr [ebp+0Ch]  
//再将ebp+0Ch的位置的值和eax相加,存放到eax中
//ebp+8和ebp+0Ch恰好一个是x,一个是y
00262602  mov         dword ptr [ebp-8],eax    
//将eax里的值赋值到ebp-8(就是z)中

 继续来看:

 00262605  mov         eax,dword ptr [ebp-8]

将ebp-8的位置的值再赋给eax;
00262608  pop         edi  
00262609  pop         esi  
0026260A  pop         ebx 

pop三次,弹出三次,就是说弹出edi、esi、ebx的值,

就是这样:

 然后

00262618  mov         esp,ebp  
把ebp给esp

就变成了这样

 

0026261A  pop         ebp  
//弹出ebp(注意,弹出时会将ebp的值还给原先存储的值)

就变成了这样。

 然后

0026261B  ret 
返回原先记住的call指令下面的地址
继续执行

 回来后继续:

00261807  add         esp,8  
//将esp加8  具体作用尚不清楚
0026180A  mov         dword ptr [ebp-20h],eax 
//将eax的值给ebp-20h。
//就是将刚刚算的z的值给ebp-20h的位置,也就是c。

 然后就是返回0.

下面还是熟悉的配方,

pop三次;

将ebp的值给esp;

弹出esp;

返回,结束本函数。

我们执行下去,会发现

 其还会调用到别的地方。

这是因为main函数其实也是被其他函数调用的。

这个我们可以不用管了。

剩下的是编译器自己的事情了,我们简单地理解至此就可以了。

好啦,到此为止,我们函数栈帧有关的知识就结束了。

欢迎各位看官关注我@jxwd,订阅专栏,就能持续看到我的文章啦😀😀

0基础C语言自学教程——第八节 函数指针数组的各种关系_jxwd的博客-CSDN博客

0基础C语言自学教程——第七节 初始指针_jxwd的博客-CSDN博客

0基础C语言保姆教程——第六节 操作符、表达式和语句_jxwd的博客-CSDN博客

0基础C语言保姆教学——第五节 数组_jxwd的博客-CSDN博客

0基础C语言保姆教程——第4节 函数_jxwd的博客-CSDN博客

0基础C语言自学教程——第三节 分支与循环_jxwd的博客-CSDN博客

0基础C保姆自学 第二节——初步认识C语言的全部知识框架_jxwd的博客-CSDN博客_c语言全部框架
C语言自学保姆教程——第一节--编译准备与第一个C程序_jxwd的博客-CSDN博客
 

  • 22
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

jxwd

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

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

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

打赏作者

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

抵扣说明:

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

余额充值