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

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

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


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


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


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


我们简单地画一个图

image.png



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


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


其中,


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


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


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

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

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

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



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


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


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


我们来一点一点分析:



前三行:

002617C0  push        ebp 
它的意思是压栈,将ebp压栈。
002617C1  mov         ebp,esp 
意思是将ebp的地址的那个值给esp。


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

微信图片_20221208184657.png


(此时的栈区图)


接着,

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


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

image.png



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



确实是这样。


我们接着往下看:


4-6行:

002617C9  push        ebx  
002617CA  push        esi  
002617CB  push        edi 
它们的意思都是一样的。push...     就是将...压入栈中
他分别将ebx  esi   edi压入栈中(它们都是寄存器的类型)


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


image.png



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


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。所以恰好是对应的次数关系,并没有多或者少。


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

image.png



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

微信图片_20221208185159.png


来看接下来的三行

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)


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


微信图片_20221208185250.png


下面的两行就同理了:

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


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


image.png


好的,接下来我们来看一看如何调用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压栈


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

image.png




继续往下,然后我们需要按住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的值。

image.png

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

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的值,


就是这样:

微信图片_20221208185756.png



然后

00262618  mov         esp,ebp  
把ebp给esp


就变成了这样


微信图片_20221208185817.png


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


就变成了这样。

微信图片_20221208185900.png



然后

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函数其实也是被其他函数调用的。


这个我们可以不用管了。


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


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

目录
相关文章
|
2月前
|
C语言 开发者
C语言实现猜数字小游戏(详细教程)
C语言实现猜数字小游戏(详细教程)
|
2月前
|
编译器 C语言 C++
VSCode安装配置C语言(保姆级教程)
VSCode安装配置C语言(保姆级教程)
|
2月前
|
Linux C语言 iOS开发
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
48 0
|
4月前
|
存储 C语言
【C语言】——函数栈帧的创建与销毁
【C语言】——函数栈帧的创建与销毁
|
4月前
|
Linux C# C语言
C 语言与嵌入汇编
C 语言与嵌入汇编
33 0
|
6月前
|
IDE 编译器 开发工具
C语言教程:如何进行环境搭建
C语言教程:如何进行环境搭建
|
6月前
|
存储 机器学习/深度学习 编译器
C语言基础简单教程
C语言基础简单教程
|
6月前
|
搜索推荐 编译器 C语言
C语言的简单教程
摘要: 了解C语言中的注释,包括//单行和/*多行*/注释,注意不要嵌套。使用快捷键Ctrl + K, Ctrl + C/U处理注释。C语言的关键字如'int'和'return'有特定含义,通常小写且高亮显示。常量是不可变的,分为不同类型。练习涉及识别不同类型的常量,如整型、实型和字符型。使用printf通过占位符输出常量,例如`printf("%d", 10)`。作业包括根据占位符输出个性化信息,如学校名字、高考分数和女朋友的详细信息,并掌握在不同情况下使用\n进行换行。
|
7月前
|
存储 算法 程序员
零基础C语言“函数”教程,有手就行
零基础C语言“函数”教程,有手就行
|
7月前
|
C语言
【用C语言轻松实现】- 扫雷【超详细教程】
【用C语言轻松实现】- 扫雷【超详细教程】