文章目录
前言
提示:这里采用的是vs2013的编译器,越高级的编译器函数的栈帧越不容易观察与学习。本文意在让初学者快速理解函数栈帧而不做过于深入而无意义的工作,同时,不同编译器下函数调用函数栈帧的创建是略有差异的,希望学习完本文能对读者有所帮助!
引子:
要了解函数栈帧,我们必须先知道ebp,esp这两个寄存器中存放的是地址,该地址是用来维护函数栈帧的。
因为每一个函数调用,我们都要在栈区创建一个空间,我们下面以add函数为例
ps:ebp(又称栈低指针),esp(栈顶指针),栈区使用是从高地址到低地址
#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 = add(a, b);
printf("%d", c);
return 0;
}
我们在调用这个main函数的时候会在栈区开辟一块空间给mian函数,该空间称为main函数的函数栈帧,那这片空间怎么维护?会有两个寄存器分别为ebp和esp存图示地址,也就是main函数栈帧的一头一尾(ps:栈区由下往上是地址从高到低),在调用add函数时,ebp和esp就会去为add函数维护,其存放地址也是add的一头一尾。那么显而易见,我们可以知道,ebp和esp这两个指针就是用来维护函数栈帧的,你调用哪个函数,ebp和esp就来维护哪两个。
ps:main函数也是被其他函数调用的,具体调用关系为:mainCRTStartup调用__tmianCRTStartup,
__tmianCRTStartup调用main函数,也就是说,main函数所在区域前还有两块区域分配给了mainCRTStartup和__tmianCRTStartup
一、局部变量是如何创建的?
在完成上述操作后,会经理三次压栈(给栈顶放一个元素)操作,在main函数的栈帧顶上放入edi,esi,ebx(了解即可),压栈完成后,esp也要移动到栈顶,如图
ps:压栈是给栈顶放一个元素
出栈是给栈顶移开一个元素
随后计算机会将main函数的栈帧里的内容都初始化为0CCCCCCCCh,到这里main函数栈帧的开辟就完成了,接下来就是正式的代码
当我们int a=10时,从ebp-8这个地址开始(前面还有一个ebp-4的地址,因为是地址,每间隔一个地址是4个字节,但是ebp-4并没有使用),创建了变量a,里面的内容由 CCCCCCCC变成了10 00 00 00。往后在ebp-14h(与ebp-8差2个整型)这个地址创建变量b,里面内容由CCCCCCCC变成20 00 00 00。后面创建变量依旧差两个整型,以此类推。。。
二、为什么局部变量的值是随机值?
由一可知,在main函数的函数栈帧中,我们没有提前放值的时候,栈帧里的内容是CCCCCCCC也就是我们非常头疼的“烫烫烫!!!”
三、函数是如何传参的?传参的顺序呢?
程序再往下走,到c=add(a,b).我们会再经历一次压栈操作,将eax放入(其值为20),并将esp(栈顶指针)移动到栈顶,如图一。继续一次压栈操作,将ecx放入(其值为10),并将esp(栈顶指针)移动到栈顶,如图二。
图一:
图二:
以上两步就是函数的传参,分别为20与10。
四、形参与实参的关系?函数调用是如何实现的?
随后我们会在ecx上边压入call指令的下一条指令的地址
很好理解,我们在调用call完要调用add函数嘛,你调用完add你是不是要回来?你回来的地址就是call指令下一条指令的地址,再往下调试,我们会发现,接下来的很多步骤和main函数一样,都是为函数准备栈帧,最后实现效果如图:
创建变量z,在ebp-8的位置将CCCCCCCC初始化为00 00 00 00,将ebp+8的地址里的值放入eax中,由图可知ebx+8也就是ecx对应的10,也就是传过来的实参10,然后把ebp+12地址里的20加上去,随后得到30放到ebp-8这个地址里,也就是z
由上可知,我们在调用函数时,是调用之前地址内传过来的值,也就是b‘和a’,先传b,push压进去,再压a。c=add(a,b)可知参数是从右向左传的。而真正进入函数内部时,int add(int x, int y),形参x,y并不是在add函数内部创建的,而是我们回头找了我们调用时,传参传的空间即b‘(被认为y),a’(被认为x)
五、函数调用后是如何返回的?
到了return z这一步,我们由五知,z所在地址为ebp-8,电脑会先把ebp-8这个地址里的值放入eax这个寄存器中(因为出add,z就销毁了),再往下会进行3次pop(弹出操作),也就是把栈顶元素移除并且,esp指向地址向下移动,具体效果如下图
随后把ebp赋给esp,那么esp就指向(main函数的)ebp指向的地址,再执行pop把ebp弹出,使得它回去main函数“头”那里,但是要注意的是,你ebp被弹走了,esp是不是要往下了,也就又回到了main函数中。到这里,main函数再次由ebp和esp正式维护
在回到main函数后,我们是不是还应该从call指令的下一条指令开始执行啊?这时,我们在前面埋下的伏笔就起到了作用,我们在之前ebp+8上面一个地址安插了一个call指令的下一条指令的地址,这时程序完成了从main函数到add函数,再回到main函数
随后把栈顶地址弹出,esp+8到了eax(形参y)下面,完成了形参x和形参y的销毁,其空间得以释放。
总结
本文详细解释了函数栈帧的创建与销毁,其重难点是理解指针的变换,在函数创建时也要考虑返回和参数的销毁,深入理解非常有益读者对函数本质的理解,希望本文能对读者有所帮助!