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的博客-优快云博客

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

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

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

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

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

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

<think>好的,用户想了解函数的概念以及比特鹏哥的相关教程。首先,我需要回忆一下函数的基本知识。函数是在程序运行时为每个函数调用分配的内存区域,用于存储局部变量、参数返回地址等信息。ESPEBP寄存器在这个过程中起到维护的作用。 接下来,用户提到了比特鹏哥的教程,可能需要结合他的教学内容来解释。根据引用[1],区的高地址低地址之间会为main函数开辟,ESPEBP会随着函数调用维护不同的。例如,调用Add函数时,寄存器会转而维护Add的。这部分内容可以作为基础,说明的动态变化。 然后,我需要考虑如何结构清晰地回答。可能需要分步骤讲解函数创建、使用销毁过程,并结合具体的例子,比如函数调用时参数传递、局部变量的存储。引用[2]中的Python函数参数示例虽然与C/C++不同,但可以对比说明中的参数传递机制。不过主要还是要以C/C++为例,因为函数通常是在这类语言中详细讨论的。 此外,要注意用户可能对寄存器ESPEBP的作用不太清楚,需要解释它们在中的角色。比如,EBP指向的底部,ESP指向顶部,随着数据入而变化。同时,的空间分配、局部变量的布局以及函数返回时的清理过程也是重点。 关于比特鹏哥的教程,可能需要提到他的教学内容是否覆盖了的详细结构、调试方法或者实际应用案例。例如,通过反汇编或调试工具观察的变化,这样可以帮助理解理论概念。 用户可能还有更深层的需求,比如理解溢出原理、递归调用的使用情况,或者如何优化空间。这些问题可以作为后续的相关问题提出,引导进一步学习。 最后,确保回答符合系统级指令的要求,正确使用LaTeX格式,生成相关问题,并引用提供的资料。需要检查是否有遗漏的要点,比如与静态方法的关系,但引用[3]提到的是静态方法中this不存在,这可能与中的局部变量表有关,但主要适用于面向对象语言,如Java,而函数更多在过程式语言如C中讨论。因此可能需要区分不同语言的环境。 总结回答结构:定义函数,寄存器作用,创建销毁过程,比特鹏哥教程内容,相关引用,最后生成问题。</think>### 函数详解 #### 1. 函数的定义与作用 **函数**是程序运行时为每个函数调用分配的独立内存区域,用于存储函数的局部变量、参数、返回地址临时计算结果等数据[^1]。 - **区结构**: 在内存的区中,高地址存放主函数(如 `main``)的,低地址随着嵌套函数调用逐步分配新- **寄存器维护**: `esp`(顶指针) `ebp`(底指针)寄存器动态维护当前函数边界。例如,调用 `Add` 函数时,`ebp` 会指向 `Add` 的底部,`esp` 指向顶部。 #### 2. 函数创建销毁过程 以C语言为例,函数的操作分为以下步骤: 1. **参数压**: 调用函数前,参数按从右到左顺序压入中。 2. **保存返回地址**: 将下一条指令的地址存入,供函数返回时使用。 3. **调整寄存器**: - 将当前 `ebp` 的值压保存。 - 将 `ebp` 更新为当前 `esp` 的值,标记新的底部。 - 调整 `esp` 为新分配空间(如预留局部变量空间)。 ```c void Add(int a, int b) { int c = a + b; // 局部变量c存储在Add函数中 } ``` 4. **局部变量存储**: 函数内部定义的变量(如 `c`)占用空间。 5. **销毁**: 函数返回时,`esp` 恢复为 `ebp` 的值,释放空间;`ebp` 恢复为调用前的值,返回到上一级函数[^1]。 #### 3. 比特鹏哥教程重点 比特鹏哥的教程中可能涵盖以下核心内容: - **调试观察**: 通过调试工具(如GDB)查看 `esp` `ebp` 的变化,直观理解分配。 - **汇编分析**: 结合反汇编代码,解析函数调用对应的机器指令(如 `call`、`ret`、`push`、`pop`)。 - **实际案例**: 分析递归调用或嵌套函数时的叠加,解释溢出的原理。 #### 4. 关键知识点对比 | 场景 | 变化 | |--------------------|--------------------------------------------------------------------------| | 函数调用时 | `ebp` 指向新底部,`esp` 指向顶部 | | 函数返回时 | `esp` 回退到调用前的顶,`ebp` 恢复为原底部 | | 多级嵌套调用 | 逐层叠加,形成“增长”(向低地址延伸) |
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

jxwd

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

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

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

打赏作者

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

抵扣说明:

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

余额充值