C语言内功修炼? 还得是函数栈帧的创建和销毁

369 阅读8分钟

1、为main()函数开辟栈帧

1.1 main()函数被调用

main()函数,也就是我们所说的主函数
mian()函数也是一个函数,它的使用也是需要调用的,那么main()函数是被谁调用的呢?


F10 进行调试,直到主函数return 0被返回,即可出现以下界面

image.png

image.png


可以看到 main() 函数是被 __tmainCRTStartup() 函数调用的
__tmainCRTStartup() 函数是被 mainCRTStartup() 函数调用的。

image.png

1.2 寄存器和栈区

image.png


⚠️ 注意:esp和ebp这两个寄存器存放的是地址,是用来维护函数栈帧的。
正在调用哪个函数,espebp维护的就是哪个函数的函数栈帧


⚠️ 注意:栈区的使用习惯是先消耗高地址再消耗低地址

image.png

image.png


在栈区放入一个元素叫压栈,删除栈区的元素叫出栈。


⚠️ 注意:只能从栈顶进行压栈或出栈操作,所以就造成了先进的后出,后进的先出(进指的是压栈,出指的是出栈)

1.3 main()函数栈帧的开辟

push ebp

将 ebp 的值放到 __tmainCRTStartup() 函数的函数栈帧的上面 (ebp 的值为该函数栈帧的栈底指针

image.png


执行 push 操作后栈顶指针 esp 就会往上移。因为上面是低地址,esp往上移动后值会减小

 

⚠️注意:每一次执行push操作之后,esp都会向上移动(低地址)

image.png

image.png

move  ebp, esp


esp 的值赋给 ebp,ebp 就会向上移动指向 esp 所指的位置

image.png

image.png

sub     esp, 0E4h


esp - 0E4h (八进制数,转换为十进制值为:228),将esp减去一个228

image.png

image.png

image.png

push ebx

push esi

push edi

⚠️注意:

这三个push压栈压了三个值,具体是什么不需要知道
我们只需要知道每一次压栈esp(栈顶指针) 都会向上移动一次
压栈三次也就移动了三次。

image.png

image.png

根据图可以看出来,三次push之后确实是把ebx、esi、edi的值压进去了。
当执行完push edi之后,esp就会指向edi的值。

lea     edi , [ ebp-0E4h ]

[ ebp-0E4h ] 加载到 edi 中(将 [ ebp-0E4h ] 这个值放到 edi 里面)。

image.png

image.png

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个字节。

image.png

image.png

到这里main()函数的函数栈帧已经开辟完毕了。

1.4 动态演示图

2.在main()函数中创建变量

2.1 变量的创建

mov    dword ptr [ebp-8],0Ah

0Ah(0Ah 转化为十进制就是 10)放到 ebp - 8 6这个位置。
因为在定义变量a的同时赋初值为10,所以执行 int a = 10; 会将10放到变量 a 所在的内存中。

⚠️注意:

如果在定义变量时没有赋初值,那么变量a所在的内存中存放的就是cc cc cc cc
所以在定义变量的时候不赋初值打印出来的就是随机值。cc cc cc cc 这个值是由编译器存放的,不同编译器存放的值不同)。

image.png

image.png

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

将 14h(14h 转化为十进制就是 20)放到 ebp - 14h 这个位置。

此时ebp 的值为 0x012FFAA0 所以 ebp - 14h 的值为 0x012FFA8C
可以发现编译器为变量 b 开辟的空间距离变量 a 是跳过8个字节。

⚠️ 注意: 具体是跳过几个字节主要取决于编译器,有些编译器是紧挨着开辟空间的。

image.png

image.png

mov     dword ptr [ebp-20h],0

将 0 放到 ebp - 20h 这个位置。 

 此时ebp 的值为 0x00AFF9D8  所以 ebp - 20h 的值为
可以发现编译器为变量 c 开辟的空间距离变量 b 是跳过8个字节。

image.png

image.png

2.2动态演示图

3.调用Add()函数前的准备

3.1 调用Add()函数

mov     eax,dword ptr [ebp-14h]

ebp-14h 中存放的值放到 eax 中。ebp-14h 是变量 b 所在空间的地址,也就是将 20 放到 eax 中。

push     eax

将 eax 放到 esp 指向的地址的上面,esp ( 栈顶指针) 向上移动指向 eax 。

image.png

image.png

mov     ecx,dword ptr [ebp-8]

ebp-8 中存放的值放到 ecx 中。ebp-8 是变量 a 所在空间的地址,也就是将 10 放到 ecx 中。

push ecx

将 ecx 放到 esp 指向的地址的上面,esp ( 栈顶指针) 向上移动指向 ecx 。

image.png

image.png

⚠️注意:

执行 call 指令以后会将 call 指令的下一条指令的地址压栈。这里将地址压栈是为了记住 call 指令的下一条指令的地址,记住这个地址是为了调用完 Add函数后返回到这个地址,再从这个地址继续往下执行。

image.png

image.png

3.2 动态演示图

4.为Add()函数开辟栈帧

4.1 Add函数栈帧的开辟

与 main 函数栈帧的创建相同,先为 Add 函数栈帧做准备工作,再执行C语言代码。

push     ebp 

将 ebp 的值放到 esp 指向的地址的上面,esp ( 栈顶指针) 向上移动指向 ebp(此时ebp 中存放的是 main 函数栈帧栈底的地址)。

image.png

image.png

mov     ebp , esp

将 esp 的值赋给 ebp,ebp 就会向上移动指向 esp 所指的位置。

image.png

image.png

sub     esp,0CCh

esp - 0CCh(八进制数,转换为十进制值为:204)。

image.png

image.png

push ebx
push esi
push edi

image.png

image.png

lea     edi,[ ebp-0CCh ]

将 [ ebp-0CCh ] 加载到 edi 中(将 [ ebp-0CCh ]这个值放到 edi 里面)。

image.png

image.png

mov     ecx,33h:将 33h 放到 ecx 中
mov     eax,0CCCCCCCCh:将 0CCCCCCCCh 放到 eax 中
rep stos     dword ptr es:[edi]:
从edi ( 此时 edi 中存放的是Add函数栈帧栈顶处的值 ) 开始向下将空间中的值初始化为0CCCCCCCCh(eax)(每次操作4个字节),总共初始化 33h(ecx) 次。

image.png

image.png

mov     dword ptr [ebp-8],0

将 0 放到 ebp - 8 这个位置。

image.png

image.png

4.2 动态演示图

5.在Add()函数中创建变量并运算

5.1 变量的创建并运算

mov   eax,dword ptr [ebp+8]

将 ebp+8 里面的值放到 eax 中,ebp+8存放的是变量 a 传参(形参 x)的值,也就是将 10 放到 eax(寄存器)中。

image.png

image.png

add     eax,dword ptr [ebp+0Ch]

将 ebp+0Ch 里面的值与 eax 中的值相加然后存放到eax中。


⚠️注意:

ebp+0Ch 存放的是变量b传参(形参 y)的值,eax 中的值是10,就是将 20 + 10 的结果存放到 eax 中。

image.png

mov     dword ptr [ebp-8],eax

将 eax 中的值放到 ebp-8 的位置上。

5.2.总结

函数在调用计算的时候并没有创建形参,是main函数在调用Add函数之前将参数传过去,在传参的过程中将形参 a 与形参 b 进行压栈。压栈是先 push b 再 push a, 所以传参是先传 b 再传 a,所以参数是从右向左传的。

在函数内部进行计算的时候,形参并没有在 Add 函数中被创建,而是通过寻找传参传过去的空间(压栈压进去的空间),再对里面的值进行计算。计算后的结果放变量 z 中。

形参是实参的一份临时拷贝。通过这个按例可以很清楚的知道改变形参并不会影响实参。


5.3.动态演示图

​**

6.Add()栈帧的销毁

6.1 销毁

mov     eax,dword ptr [ebp-8]

将 ebp-8 里面的值放到 eax 中。

image.png

⚠️注意:

 执行 return z; 以后,变量 z 的空间就会被销毁,在销毁之前将里面的值放到 eax(寄存器)中,这样 Add 函数计算后值就可以返回到 main 函数中。

pop edi
pop esi
pop ebx

⚠️注意:每执行一次 pop 指令就会弹出一个值,esp 的值就会向下移动一次,执行完三次 pop 指令后,esp 就会向下移动三次。

image.png

image.png

mov     esp,ebp

将 ebp 的值赋给 esp,esp 就会向下移动指向 ebp 所指的位置

image.png

image.png

pop     ebp

将栈顶 ebp 的值弹出。当前 ebp 存放的是 main 函数栈帧栈底的地址。执行 pop     ebp 指令后,ebp 就会指向 main 函数栈帧的栈底。


⚠️注意:

将main函数栈底的值提前保存是为了Add 函数调用结束后,随着Add栈帧的销毁,main 函数的栈顶是很容易找到的,但是main 函数的栈底不容易找到,所以将main函数的栈底地址提前保存。

image.png

image.png

ret

返回到 main 函数。执行 ret 指令后会从栈顶弹出 call 指令的下一条指令的地址,回到 main 函数的函数栈帧中,继续从 call 指令的下一条指令开始执行。

image.png

image.png

add     esp,8

esp+8,esp 向下移动,当esp向下移动指向 esp+8 的地址时,形参 x 、y 的空间被释放。

image.png

image.png

mov     dword ptr [ebp-20h],eax

将 eax 中存放的值放到 ebp-20h (转换成十进制:32) 中。

image.png

image.png

6.2 动态演示图