【C】函数栈帧的创建和销毁

寄存器是用来存储数据的,如:eax,ebx,ecx ...... 还有两个存储地址的寄存器ebp,和esp,而存储的这两个地址是用来维护函数栈帧的。

函数要使用的话是要被调用的,而调用之后就会在栈区创建一片空间,主函数 main() 也是函数,所以它使用前也需要被调用,从调用堆栈中就可以看到调用情况。

(以下内容均以这段代码为例)

在堆区中使用内存是从高到低的,我们可以画出main () 所开辟的空间。而 main() 又是由 esp 和 ebp 维护的。

根据调用堆栈可以看到,main () 是被 _tmainCRTStartup() 调用的,而 _tmainCRTStartup() 又是被 mainCRTStartup() 调用的,以此类推,可以画出它们的空间。

 通常,我们把 esp 叫做 ‘栈顶指针’ ,把 ebp 叫做 ‘栈低指针’ 。

接下来,右键转到反汇编,我们来看看编译器是怎么一步步来调用函数的。

在调用 main() 之前,我们有 _tmainCRTStartup() 。

push ebp : 将 ebp 的值放到栈顶,也称 ‘压栈’ 。而因为 esp 维护的是栈顶,所以这时 esp 指向的地址也发生改变。

转到监视可以到看,esp 指向的地址减了4字节,因为地址是从高到低使用的。

 

mov ebp,esp :将 esp 指向的地址赋给 ebp 。

那么这个时候 esp 和 ebp 指向的应该是同一个地址。

 

 sub esp,0E4h:给esp 减去0E4h(十六进制数字,等于228,后面的h表示这是一个十六进制数) 。

 减去说明是往上走,而这时的 esp 和 ebp 维护着一块儿新的空间,就是为 main() 预先开辟的空间。

 

 push ebx : 用 ebx 进行压栈。

  push esi : 用 esi 进行压栈。

 

  push edi : 用 edi 进行压栈。

 

 

 lea   edi , [ebp-0E4h] :  将[ebp-0E4h]的地址放入 edi 中。(lea : 加载有效地址)

 

 mov   ecx,39h : 将39h放入 ecx 中。(39h:十六进制数,等于十进制57)

 mov   eax,0CCCCCCCCh :将0CCCCCCCCh 放入 eax 。

 rep stos   dword ptr es:[edi] :从 edi 指向的地址开始,后面的4个字节内容转换成 0CCCCCCCCh ,进行57次。    (word:单字,占2字节)   (dword:双字,占4字节)

就相当于把为 main() 开辟的空间内容全部初始化成 ‘0CCCCCCCCh’ 。

 

 

最后的结果就是将 mian() 中所有内容初始化成了 ‘ 0CCCCCCCCh ’ 。

 到这为止,main() 的空间就开辟完了。

 int a =10 对应的汇编代码为 : mov   dword ptr [ebp-8],0Ah

意思为:将 0Ah 放入 [ebp-8] 。(ebp-8为减去8个字节)(0Ah是十六进制数字,十进制为10)

 [ebp-8] 的位置被我们赋值成了 10 ,而 [ebp-4] 的位置我们没赋值,这时候里面放的就是 ‘0CCCCCCCCh’ ,这就是为什么我们有时候越界访问的时候会打印出乱码 ‘烫烫烫...’ 。

 int b = 20 

mov   dword ptr [ebp-14h] ,14h

 将 14h 放入 [ebp-14h] 中。(14h:十六进制数,等于十进制的20)(ebp-14h:减去20个字节)

 int c = 0

mov   dword ptr [ebp-20h] ,0

将 0 放入 [ebp-20h] 中。(20h:等于十进制的32)(ebp-20h:减去32个字节)

 

 这个时候就开始调用 Add() 了。

mov   eax , dword ptr [ebp-14h]

将 [ebp-14h] 中的值放入 eax 寄存器中。

 

 push   eax

使用 eax 进行压栈。

 

mov   ecx , dword ptr [ebp-8]

将 [ebp-8] 中的值放入 ecx 中。

 push   ecx

使用 ecx 进行压栈。

 到现在为止的四个步骤,其实就是在传参。

 

 call   000810D2

调用 Add() (call:调用)(000810D2:Add() 地址)

但在调用之前,还会有一步操作:用 call 下一条指令的地址进行压栈。(执行 call 时需要按F11才能进入到函数内部)

按下 F11 之后进入到函数内部,但这时可以发现 esp 发生了改变。

 经过对比可以发现,esp 中的值就是 call 的下一条指令的地址。

注:这里涉及到大小端存储,10 14 08 其实就是 08 14 10 只是存储方式不一样,后面也是。

 为什么要使用 call 的下一条指令地址进行压栈呢?这是为了在调用完函数之后可以直接继续执行后面的代码。

 继续按 F11,这次才是真正地进入到函数内部。

进入到函数内部我们发现,这些代码和前面为 mian() 开辟空间的时候很像,其实,这里就是在为Add () 开辟空间。

 

 push   ebp

使用 ebp 指向的值进行压栈。注意:这时的 ebp 正指向下面 main()  的底部,也就是说,把 main() 底部的地址拿来进行压栈。

 

 

 

 mov   ebp , esp

将 esp 的值赋给 ebp。

 

sub   esp , 0CCh

给 esp 减去 0CCh。(0CCh:十六进制数,等于十进制204)

这边减去的地址就是为 Add() 预开辟的空间。

push   ebx

使用 ebx 寄存器进行压栈。 

push   esi

使用 esi 寄存器进行压栈。 

push   edi

使用 edi 寄存器进行压栈。 

lea   edi,[ebp+FFFFFF34h]

将 [ebp+FFFFFF34h] 的地址放入 edi 。

mov   ecx , [33h]

将 [33h] 放入 ecx 。

mov   eax , 0CCCCCCCCh 

将 ‘0CCCCCCCCh’ 放入 eax 。

rep stos    dword ptr es : [edi]

从 edi 开始,往后四个字节,初始化为 ‘0CCCCCCCCh’ ,进行51次。

相当于把为 Add() 开辟的空间内容全部初始化成 ‘0CCCCCCCCh’ 。

 

 int z =0 

对应汇编代码:mov    dword ptr [ebp-8] , 0

将 [ebp-8] 的位置往后四个字节赋值为0。

到这里,就要开始我们函数内部的相加了。

 mov    eax , dword ptr [ebp+8]

将 [ebp+8] 里面的值放入 eax 中。注意,这里的 [ebp+8] 就是我们前面传进来的 a 的值,到这为止,我们已经开始使用参数了。

 

 mov    eax , dword ptr [ebp+0Ch]

将 [ebp+0Ch] 里面的值加入 eax 中。0Ch 等于 12 ,这里就找到了我们前面传进来的参数b 。

 mov    dword ptr [ebp-8] , eax

将 eax寄存器 中的值放入 [ebp-8] 的后面四个字节中。

 [ebp-8] 就是 z 的地址。

到这边为止,我们可以得出结论:当函数还没被调用的时候,参数就已经被压栈了,而函数传参的顺序是从右向左传参的。

 

 return z

对应代码:mov    eax , dword ptr [ebp-8]

将 [ebp-3] (z) 的值放入 eax寄存器 中。

 

这时函数调用完,就要开始回收空间了。

pop    edi

弹出 edi ,esp++

pop    esi

弹出 esi ,esp++

pop    ebx 

弹出 ebx ,esp++

 

 

 

 mov    esp , ebp

将 ebp 指向的地址赋给 esp 。

 

pop    ebp

弹出ebp,esp++。

注:这里的 ebp 放的是 main() 底部地址,当弹出之后,ebp 重新指向 main() 底部。

 到了这里,就又回到了 mian() ,继续由 esp,ebp 来维护这块空间。

我们是通过 call 来调用了 Add() ,那么调用完之后,就要从 call 的下一条指令继续执行。

这时,esp 指向的正好就是 call 指令的下一条指令的地址,这样就可以继续向后执行了。

我们可以看到,当调用完函数之后,esp 直接回到 call 的下一条指令。

add    esp , 8

给 esp 跳过8字节地址,相当于销毁了 a 和 b 的形参。

到了这里,我们就又知道了形参是什么时候销毁的。 

 这时,我们的 Add() 返回值放在了寄存器中。

<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` 恢复为原底部 | | 多级嵌套调用 | 逐层叠加,形成“增长”(向低地址延伸) |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值