前言
在前面的学习中,可能会出现许多疑惑:
1、局部变量是怎么创建的?
2、函数是怎么传参的?
3、函数调用是怎么做到的?
4、函数调用结束后是怎么返回的?
…
希望读者在看完小编的文章,对一系列问题会有所掌握
观图有感
你去野外烧烤,并为此创建了一个待办事项清单——一叠便条。
将想到的烧烤食物写在便条上,一个食材一个便条,最先想到的食材写在便条上后,放在最下面,依次往上放,最后想到的写在便条上后,放在最上面。
之后,在烧烤的时候,从上往下拿,拿出来的表示你已经在烧烤了,可以将它删去。
一叠便条要简单得多:插入的待办事项放在清单的最前面;读取待办事项时,你只读取最上面的那个,并将其删除。因此这个待办事项清单只有两种操作:压入(插入)和弹出(删除并读取)。
这种数据结构称为栈。栈是一种简单的数据结构,之前学函数的时候我们一直在使用它,却没有意识到!
一、概述
函数栈帧是在内存中的栈区为被调函数开辟的一块空间,里面用来存放该函数中定义的变量等东西,当函数运行完毕栈帧将被销毁。
可以想象成洗盘子,最先吃完的人将盘子放在最下面,后面吃完的人依次将盘子叠放在前一个的上面。于是,最后吃完的人的盘子就在最上面,也就是最先洗。
Push(入栈):为栈增加一个元素
Pop (出栈): 从栈中取出一个元素
二、寄存器
寄存器是中央处理器内用来暂存指令、数据和地址的电脑存储器。寄存器的存贮容量有限,读写速度非常快。在计算机体系结构里,寄存器存储在已知时间点所作计算的中间结果,通过快速地访问数据来加速计算机程序的执行。
–百科
Name | Function |
eax | “累加器”, 用来存放函数的返回值 |
ebx | "基地址"寄存器,可作为储存器指针来使用, 在内存寻址时存放基地址 |
ecx | 计数器, 在循环和指针操作时,要用它来控制循环次数 |
edx | "数据寄存器’,在进行乘、除法运算时,可作为默认的操作数参数参与运算 |
esp | 栈指针寄存器,存放函数栈顶地址 |
ebp | 帧指针寄存器,存放函数栈底地址 |
esp和ebp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的
在本节中,主要了解这俩寄存器
三、汇编指令
Directives | Function |
push x | 将x压入栈中 |
pop x | 将x弹出栈中 |
mov a, b | 将b赋值给a,即b指向a |
sub a | num a的值减去num,即a向低地址移动 |
lea(load effective adress) | 加载有效地址(在示例中理解) |
四、函数栈帧的创建
所有函数的调用都会在内存里面的栈区创建函数栈帧,包括main函数。
以下面一个详细的代码,描述函数栈帧的创建
本次代码是在 vs 2013 里面实现的,版本越低,可以更好展示
#include <stdio.h> int Add(int x, int y) { int z = x + y; return z; } int main() { int a = 10; int b = 20; int c = Add(a, b); return 0; }
按F10,进行调试
4.1 main函数栈帧的创建
C语言所对应的汇编代码
int main() { push ebp //将ebp压入栈中 mov ebp,esp //将esp赋值给ebp,即将esp移动到ebp的位置 sub esp,0E4h //将esp向低地址移动0E4h个字节的位置 push ebx //(我们不要管) push esi //(我们不要管) push edi //(我们不要管) lea edi,[ebp-24h] //将[ebp-24h]存入edi中 mov ecx,9 //将9存入ecx中 mov eax,0CCCCCCCCh //将0CCCCCCCCh存入eax中 rep stos dword ptr es:[edi] //将edi的值对应的地址处开始,将高于该地址共ecx个单位的值置为0CCCCCCCCh int a = 10; mov dword ptr [ebp-8],0Ah int b = 20; mov dword ptr [ebp-14h],14h int c = Add(a, b); mov eax,dword ptr [ebp-14h] push eax mov ecx,dword ptr [ebp-8] push ecx call 011C10B4 add esp,8 mov dword ptr [ebp-20h],eax return 0; xor eax,eax } pop edi pop esi pop ebx add esp,0E4h cmp ebp,esp call 011C1235 mov esp,ebp pop ebp ret
看着有点麻烦,不过对着汇编语言,可以仔细研究一番。
首先看main函数
栈使用空间是由高地址到低地址
正在调用哪个函数,esp和ebp就维护哪个函数,在这里,我们调用的是main函数,那么就维护main函数。
通过 __tmainCRTStartup 函数调用main函数,所以要创建好__tmainCRTStartup 的栈帧
push ebp
push ebp就是把__mainCRTStartup 函数栈底的地址压栈,ebp的值压入后,esp指针会上移一位
mov ebp,esp
sub esp,0E4h
push ebx / esi /edi
lea edi,[ebp-24h] 、mov ecx,9、mov eax,0CCCCCCCCh、rep stos dword ptr es:[edi]
main函数中变量的创建
4.2 在main函数中调用Add函数
int a = 10; mov dword ptr [ebp-8],0Ah int b = 20; mov dword ptr [ebp-14h],14h int c = Add(a, b); mov eax,dword ptr [ebp-14h] //把ebp-14h这个地址里面存放的值(也就是20)赋值给eax push eax //push(压栈)eax mov ecx,dword ptr [ebp-8] //把ebp-8这个地址里面存的值(也就是10)赋值给ecx push ecx //push(压栈)ecx call 011C10B4 add esp,8 mov dword ptr [ebp-20h],eax
传参
经过这两次压栈后,esp指针指向的地址减小8个字节(一次减小4个字节,也就是1个整型)
进入Add函数
call 011C10B4 执行这条语句,在执行这条语句时,我们需要按键盘上的F11(笔记本电脑可能需要按Fn+F11)。
在按完F11后,我们就进入了Add函数内部,而且还会发现,esp指针还向上移动了4个字节
003F1450 是call指令的下一条指令的地址。
为什么要将call指令的下一条指令的地址存起来呢??
是因为在Add函数调用结束的时候,需要返回继续执行call指令的下一条指令,所以在执行call指令的时候要将call下一条指令的地址存起来,在Add函数调用结束的时候,就能根据存的地址找到call指令的下一条指令,从而让程序可以继续执行。
紧接着继续按F11,就真正来到了Add函数里面
剩下的过程其实和在调用main函数的动画演示是一样的,不再做过多演示。
Add函数中变量Z的创建
此过程和main函数中变量a,b,c创建的过程是一样的
z=x+y
int z = x + y; mov eax,dword ptr [ebp+8] //把ebp+8这个地址里面存储的值放到eax里 add eax,dword ptr [ebp+0Ch] //把ebp+0Ch这个地址里面存储的值加到eax里面去 mov dword ptr [ebp-8],eax //把eax里面的值存到ebp-8这个地址里面(变量z的地址) return z; mov eax,dword ptr [ebp-8]
五、函数栈帧的销毁
pop edi pop esi pop ebx add esp,0CCh
会发现pop指令,代表出栈
将edi,esi和ebx弹出栈
add esp,8这条指令,该指令的执行结果是让esp指针指向的地址加8
mov dword ptr [ebp-20h],eax指令。执行结果是:把eax里面存的值(30)赋值给ebp-20h所指向的这块空间,其实就是变量c的存储空间
执行这条指令之前ebp-20h所指向的这块空间存的值是0,在执行完这条指令之后,ebp-20h所指向的这块空间里面存的就是1e(十进制下的30)。并且可以看出ebp-20h和&c的值相同,说明他们指向同一块空间——变量c的存储空间.
总结
本节涉及一些数据结构的内容,但是为了解决心中的疑惑,我们可以了解一下。
如有误,欢迎指正!!