概述
目录
⌚一、为main()函数开辟栈帧
⛳1.main()函数被调用
⛳2.寄存器和栈区
⛳3.main()函数栈帧的开辟
⛳4.动态演示图
⏰ 二、在main()函数中创建变量
⛳1.变量的创建
⛳2.动态演示图
⚾️三、调用Add()函数前的准备
⛳1.调用Add()函数
⛳2.动态演示图
⚽ 四、为Add()函数开辟栈帧
⛳1.Add函数栈帧的开辟
⛳ 2.动态演示图
✏️ 五、在Add()函数中创建变量并运算
⛳1.变量的创建并运算
⛳2.总结
✒️ 六、Add()栈帧的销毁
⛳1.销毁
⛳2.动态演示图
⌚ 一、为main()函数开辟栈帧
1.main()函数被调用
main()函数,也就是我们所说的主函数,mian()函数也是一个函数,它的使用也是需要调用的,那么main()函数是被谁调用的呢?
按F10进行调试,直到主函数return 0被返回,即可出现以下界面
可以看到main() 函数是被 __tmainCRTStartup() 函数调用的;__tmainCRTStartup() 函数是被 mainCRTStartup() 函数调用的。
2.寄存器和栈区
⚠️
注意:esp和ebp这两个寄存器存放的是地址,是用来维护函数栈帧的。
正在调用哪个函数,esp和ebp维护的就是哪个函数的函数栈帧
⚠️
注意:栈区的使用习惯是先消耗高地址再消耗低地址
在栈区放入一个元素叫压栈,删除栈区的元素叫出栈。
⚠️
注意:只能从栈顶进行压栈或出栈操作,所以就造成了先进的后出,后进的先出(进指的是压栈,出指的是出栈)
3.main()函数栈帧的开辟
push ebp
将 ebp 的值放到 __tmainCRTStartup() 函数的函数栈帧的上面 (ebp 的值为该函数栈帧的栈底指针)
执行 push 操作后栈顶指针 esp 就会往上移。因为上面是低地址,esp往上移动后值会减小。
⚠️
注意:每一次执行push操作之后,esp都会向上移动(低地址)
move ebp , esp
将 esp 的值赋给 ebp,ebp 就会向上移动指向 esp 所指的位置
sub esp , 0E4h
esp - 0E4h (八进制数,转换为十进制值为:228),将esp减去一个228
push ebx
push esi
push edi
⚠️
注意:
这三个push压栈压了三个值,具体是什么不需要知道,我们只需要知道每一次压栈esp(栈顶指针)都会向上移动一次,压栈三次也就移动了三次。
根据图可以看出来,三次push之后确实是把ebx、esi、edi的值压进去了。
当执行完push edi之后,esp就会指向edi的值。
lea edi , [ ebp-0E4h ]
将 [ ebp-0E4h ] 加载到 edi 中(将 [ ebp-0E4h ]这个值放到 edi 里面)。
ebp-0E4h的值为三次push前esp指向的值,三次push前esp指向的是main()函数的函数栈帧的栈顶;所以ebp-0E4h的值为main()函数的函数栈帧的栈顶的值,即edi中存放的的值为main()函数的函数栈帧的栈顶的值
mov ecx,39h: 将 39h 放到 ecx 中。
mov eax,0CCCCCCCCh:将 0CCCCCCCCh 放到 eax 中
rep stos dword ptr es:[edi]:从edi ( 此时 edi 中存放的是main函数栈帧栈顶处的值 ) 开始向下将39h这么大的空间中的值全部初始化为0CCCCCCCCh(eax)。
⚠️
注意:dword:double word,word表示两个字节,double word就表示4个字节。
到这里main()函数的函数栈帧已经开辟完毕了。
4.动态演示图
⏰ 二、在main()函数中创建变量
1.变量的创建
mov dword ptr [ebp-8],0Ah
将 0Ah(0Ah 转化为十进制就是 10)放到 ebp - 8 这个位置。
因为在定义变量a的同时赋初值为10,所以执行 int a = 10; 会将10放到变量 a 所在的内存中。
⚠️注意:
如果在定义变量的时候没有赋初值,那么变量 a 所在的内存中存放的就是cc cc cc cc,所以在定义变量的时候不赋初值打印出来的就是随机值。(cc cc cc cc这个值是由编译器存放的,不同编译器存放的值不同)
mov dword ptr [ebp-14h],14h
将 14h(14h 转化为十进制就是 20)放到 ebp - 14h 这个位置。
此时ebp 的值为 0x012FFAA0 所以 ebp - 14h 的值为 0x012FFA8C,可以发现编译器为变量 b 开辟的空间距离变量 a 是跳过8个字节。
⚠️
注意:具体是跳过几个字节主要取决于编译器,有些编译器是紧挨着开辟空间的。
mov dword ptr [ebp-20h],0
将 0 放到 ebp - 20h 这个位置。
此时ebp 的值为 0x00AFF9D8 所以 ebp - 20h 的值为 ,可以发现编译器为变量 c 开辟的空间距离变量 b 是跳过8个字节。
a,b,c三个变量创建完毕,开始调用Add()函数
2.动态演示图
⚾️三、调用Add()函数前的准备
1.调用Add()函数
mov eax,dword ptr [ebp-14h]
将 ebp-14h 中存放的值放到 eax 中。ebp-14h 是变量 b 所在空间的地址,也就是将 20 放到 eax 中。
push eax
将 eax 放到 esp 指向的地址的上面,esp ( 栈顶指针) 向上移动指向 eax 。
mov ecx,dword ptr [ebp-8]
将 ebp-8 中存放的值放到 ecx 中。ebp-8 是变量 a 所在空间的地址,也就是将 10 放到 ecx 中。
push ecx
将 ecx 放到 esp 指向的地址的上面,esp ( 栈顶指针) 向上移动指向 ecx 。
⚠️注意:
执行 call 指令以后会将 call 指令的下一条指令的地址压栈。这里将地址压栈是为了记住 call 指令的下一条指令的地址,记住这个地址是为了调用完 Add函数后返回到这个地址,再从这个地址继续往下执行。
2.动态演示图
⚽ 四、为Add()函数开辟栈帧
1.Add函数栈帧的开辟
与 main 函数栈帧的创建相同,先为 Add 函数栈帧做准备工作,再执行C语言代码。
push ebp
将 ebp 的值放到 esp 指向的地址的上面,esp ( 栈顶指针) 向上移动指向 ebp(此时ebp 中存放的是 main 函数栈帧栈底的地址)。
mov ebp , esp
将 esp 的值赋给 ebp,ebp 就会向上移动指向 esp 所指的位置。
sub esp,0CCh
esp - 0CCh(八进制数,转换为十进制值为:204)。
push ebx
push esi
push edi
lea edi,[ ebp-0CCh ]
将 [ ebp-0CCh ] 加载到 edi 中(将 [ ebp-0CCh ]这个值放到 edi 里面)。
mov ecx,33h:将 33h 放到 ecx 中
mov eax,0CCCCCCCCh:将 0CCCCCCCCh 放到 eax 中
rep stos dword ptr es:[edi]:从edi ( 此时 edi 中存放的是Add函数栈帧栈顶处的值 ) 开始向下将空间中的值初始化为0CCCCCCCCh(eax)(每次操作4个字节),总共初始化 33h(ecx) 次。
mov dword ptr [ebp-8],0
将 0 放到 ebp - 8 这个位置。
2.动态演示图
✏️ 五、在Add()函数中创建变量并运算
1.变量的创建并运算
mov eax,dword ptr [ebp+8]
将 ebp+8 里面的值放到 eax 中,ebp+8存放的是变量 a 传参(形参 x)的值,也就是将 10 放到 eax(寄存器)中。
add eax,dword ptr [ebp+0Ch]
将 ebp+0Ch 里面的值与 eax 中的值相加然后存放到eax中。
⚠️注意:
ebp+0Ch 存放的是变量b传参(形参 y)的值,eax 中的值是10,就是将 20 + 10 的结果存放到 eax 中。
mov dword ptr [ebp-8],eax
将 eax 中的值放到 ebp-8 的位置上。
2.总结
函数在调用计算的时候并没有创建形参,是main函数在调用Add函数之前将参数传过去,在传参的过程中将形参 a 与形参 b 进行压栈。压栈是先 push b 再 push a, 所以传参是先传 b 再传 a,所以参数是从右向左传的。
在函数内部进行计算的时候,形参并没有在 Add 函数中被创建,而是通过寻找传参传过去的空间(压栈压进去的空间),再对里面的值进行计算。计算后的结果放变量 z 中。
形参是实参的一份临时拷贝。通过这个按例可以很清楚的知道改变形参并不会影响实参。
3.动态演示图
✒️ 六、Add()栈帧的销毁
1.销毁
mov eax,dword ptr [ebp-8]
将 ebp-8 里面的值放到 eax 中。
⚠️注意:
执行 return z; 以后,变量 z 的空间就会被销毁,在销毁之前将里面的值放到 eax(寄存器)中,这样 Add 函数计算后值就可以返回到 main 函数中。
pop edi
pop esi
pop ebx
⚠️注意:每执行一次 pop 指令就会弹出一个值,esp 的值就会向下移动一次,执行完三次 pop 指令后,esp 就会向下移动三次。
mov esp,ebp
将 ebp 的值赋给 esp,esp 就会向下移动指向 ebp 所指的位置
pop ebp
将栈顶 ebp 的值弹出。当前 ebp 存放的是 main 函数栈帧栈底的地址。执行 pop ebp 指令后,ebp 就会指向 main 函数栈帧的栈底。
⚠️注意:
将main函数栈底的值提前保存是为了Add 函数调用结束后,随着Add栈帧的销毁,main 函数的栈顶是很容易找到的,但是main 函数的栈底不容易找到,所以将main函数的栈底地址提前保存。
ret
返回到 main 函数。执行 ret 指令后会从栈顶弹出 call 指令的下一条指令的地址,回到 main 函数的函数栈帧中,继续从 call 指令的下一条指令开始执行。
add esp,8
esp+8,esp 向下移动,当esp向下移动指向 esp+8 的地址时,形参 x 、y 的空间被释放。
mov dword ptr [ebp-20h],eax
将 eax 中存放的值放到 ebp-20h (转换成十进制:32) 中。
2.动态演示图
最后
以上就是阔达戒指为你收集整理的C语言内功修炼【函数栈帧的创建和销毁】的全部内容,希望文章能够帮你解决C语言内功修炼【函数栈帧的创建和销毁】所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复