@TOC
前言
之前学习C语言函数递归的时候,遇到了许多的疑惑和困难,所以今天就讲一讲函数递归的一些基本原理—==函数栈帧的创建与销毁==,来揭开函数递归的神秘面纱。
之前学习递归的时候产生的疑惑:
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机值?
- 函数是怎么传参的?传参的顺序又是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用结束后是怎么返回的?
而这些疑惑,今天我都会带着大家解开。并且今天的内容,建议使用的环境是==Visual Studio 2013==,建议不要使用太高级的编译器,因为我们在学习的过程中需要随时对我们的程序运行过程进行观测,而越是高级的编译器会自动优化,封装更加复杂,随之而来的就是越不容易学习和观察。
并且,在不同的编译器下,函数调用过程中栈帧的创建和销毁大体逻辑上是相同的,但细节上是略有差异的,具体细节取决于编译器的实现。
一、寄存器概念
在开始了解函数栈帧的创建与销毁之前,我们首先要了解一下计算机CPU内部的重要组成——==寄存器==。
寄存器百度百科简介:
寄存器是CPU内部用来==存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果==。其实寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储1位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。==寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址==。
寄存器最起码具备的4种功能
清除数码
:将寄存器里的原有数码清除。接收数码
:在接收脉冲作用下,将外输入数码存入寄存器中。存储数码
:在没有新的写入脉冲来之前,寄存器能保存原有数码不变。输出数码
:在输出脉冲作用下,才通过电路输出数码。
仅具有以上功能的寄存器称为数码寄存器;有的寄存器还具有移位功能,称为—移位寄存器
。
寄存器的类型
- 通用寄存器组
- 指针和变址寄存器
- 段寄存器
- 指令指针寄存器IP
- 标志寄存器FR
指针和变址寄存器
两个特别的寄存器
-
EBP:扩展基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈底。
-
ESP:扩展堆栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
二、函数栈帧的指令介绍
对于每一个函数在进行创建和使用时,都会在内存中创建一块临时空间,并在临时空间内进行函数的处理。
每一个函数被创建就会在栈上创建一块空间,并且会使EBP与ESP来进行维护,也就是说,ESP
与EBP
是专门维护新创立的函数的。
函数栈帧中的几个重要指令
- push—学过c++的stl的同学我感觉应该熟悉,push意思就是推、压,这里就是创建一块空间。
- sub—就是相减,作差。
- mov—就是赋值的意思。
- lea,mov,mov,rep stos的意思是,把lea这个为起始到reo stos这里之间的16进制数字,将第一个mov的值全部改写成第二个mov的值。
- pop—学过stl的其实知道就是减,其实应用这里就是释放栈帧其实就是销毁。
所用用到的指令
int main(){001C18B0 push ebp001C18B1 mov ebp,esp001C18B3 sub esp,0E4h001C18B9 push ebx001C18BA push esi001C18BB push edi001C18BC lea edi,[ebp-24h]001C18BF mov ecx,9001C18C4 mov eax,0CCCCCCCCh001C18C9 rep stos dword ptr es:[edi]001C18CB mov ecx,1CC008h001C18D0 call 001C131Bint a = 10;001C18D5 mov dword ptr [ebp-8],0Ahint b = 20;001C18DC mov dword ptr [ebp-14h],14hint c = Add(a, b);001C18E3 mov eax,dword ptr [ebp-14h]001C18E6 push eax001C18E7 mov ecx,dword ptr [ebp-8]001C18EA push ecx001C18EB call 001C10B4001C18F0 add esp,8001C18F3 mov dword ptr [ebp-20h],eaxprintf("%d + %d = %d\n", a, b, c);001C18F6 mov eax,dword ptr [ebp-20h]001C18F9 push eax001C18FA mov ecx,dword ptr [ebp-14h]001C18FD push ecx001C18FE mov edx,dword ptr [ebp-8]001C1901 push edx001C1902 push 1C7B30h001C1907 call 001C10D2001C190C add esp,10hreturn 0;001C190F xor eax,eax}001C1911 pop edi001C1912 pop esi001C1913 pop ebx001C1914 add esp,0E4h001C191A cmp ebp,esp001C191C call 001C1244001C1921 mov esp,ebp001C1923 pop ebp001C1924 retint main() { 001C18B0 push ebp 001C18B1 mov ebp,esp 001C18B3 sub esp,0E4h 001C18B9 push ebx 001C18BA push esi 001C18BB push edi 001C18BC lea edi,[ebp-24h] 001C18BF mov ecx,9 001C18C4 mov eax,0CCCCCCCCh 001C18C9 rep stos dword ptr es:[edi] 001C18CB mov ecx,1CC008h 001C18D0 call 001C131B int a = 10; 001C18D5 mov dword ptr [ebp-8],0Ah int b = 20; 001C18DC mov dword ptr [ebp-14h],14h int c = Add(a, b); 001C18E3 mov eax,dword ptr [ebp-14h] 001C18E6 push eax 001C18E7 mov ecx,dword ptr [ebp-8] 001C18EA push ecx 001C18EB call 001C10B4 001C18F0 add esp,8 001C18F3 mov dword ptr [ebp-20h],eax printf("%d + %d = %d\n", a, b, c); 001C18F6 mov eax,dword ptr [ebp-20h] 001C18F9 push eax 001C18FA mov ecx,dword ptr [ebp-14h] 001C18FD push ecx 001C18FE mov edx,dword ptr [ebp-8] 001C1901 push edx 001C1902 push 1C7B30h 001C1907 call 001C10D2 001C190C add esp,10h return 0; 001C190F xor eax,eax } 001C1911 pop edi 001C1912 pop esi 001C1913 pop ebx 001C1914 add esp,0E4h 001C191A cmp ebp,esp 001C191C call 001C1244 001C1921 mov esp,ebp 001C1923 pop ebp 001C1924 retint main() { 001C18B0 push ebp 001C18B1 mov ebp,esp 001C18B3 sub esp,0E4h 001C18B9 push ebx 001C18BA push esi 001C18BB push edi 001C18BC lea edi,[ebp-24h] 001C18BF mov ecx,9 001C18C4 mov eax,0CCCCCCCCh 001C18C9 rep stos dword ptr es:[edi] 001C18CB mov ecx,1CC008h 001C18D0 call 001C131B int a = 10; 001C18D5 mov dword ptr [ebp-8],0Ah int b = 20; 001C18DC mov dword ptr [ebp-14h],14h int c = Add(a, b); 001C18E3 mov eax,dword ptr [ebp-14h] 001C18E6 push eax 001C18E7 mov ecx,dword ptr [ebp-8] 001C18EA push ecx 001C18EB call 001C10B4 001C18F0 add esp,8 001C18F3 mov dword ptr [ebp-20h],eax printf("%d + %d = %d\n", a, b, c); 001C18F6 mov eax,dword ptr [ebp-20h] 001C18F9 push eax 001C18FA mov ecx,dword ptr [ebp-14h] 001C18FD push ecx 001C18FE mov edx,dword ptr [ebp-8] 001C1901 push edx 001C1902 push 1C7B30h 001C1907 call 001C10D2 001C190C add esp,10h return 0; 001C190F xor eax,eax } 001C1911 pop edi 001C1912 pop esi 001C1913 pop ebx 001C1914 add esp,0E4h 001C191A cmp ebp,esp 001C191C call 001C1244 001C1921 mov esp,ebp 001C1923 pop ebp 001C1924 ret
(一)这里先写一个程序以便于后续的讲解:
#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;}#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; }#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; }
(二)这里根据VS2013
就可以看到这里的指令
==这里是为了main函数做准备,所以下面就开始讲解函数栈帧的==
三、函数栈帧的创建
函数__tmainCRTStartup的建立
(一) 指令1.push
(二) 指令2.move
(三) 指令3.sub
(四) 指令4,5,6.push
前面指令的汇总:
(五) 指令7,8,9,10
PS:mov39h个内容,全部改成0cccccccch
dword是四个字节的意思
main函数建立
(一) 指令1.将0Ah赋值给[ebp-8]
(二) 指令2.将14h赋值给[ebp-14h]
(三) 指令3.将0赋值给[ebp-20h]
Add函数建立
这里没见过的 详细讲,见过的就不说了。比如push
指令1.把[ebp-14h]放到eax
指令2.把[ebp-8]放到ecx
指令3.call指令
这里由于前面讲解,并且指令简单,所以就直接给大家上图啦~
==以上是整理栈帧建立的知识图谱==
四、函数栈帧的销毁
==三连pop之后就是把我的ebp赋值给esp==
最后,执行最后一条ret
指令,而此时 ret 指令要回到的,又恰恰是此前 call 指令
存储在EBP弹出后变为栈顶的那个地址:00C21450
然后按照指令依次释放就算是销毁了之前的函数栈帧,直到主函数的return 0
;
总结
写到这里,函数栈帧的创建以及销毁
算是讲完了,但是奈不住我讲的不好或者同学们没听懂,我的建议是给我提意见或者仔细多看几遍,这样才能学明白函数栈帧。
我是夏目浅石,希望和你一起学习进步,刷题无数!!!希望各位大佬
==能一键三连==支持一下博主
,hhhh~我们下期见喽
✨
?
⭐️
✏️