在上一篇博客中说道了递归过深会导致栈溢出,而这与函数栈帧有关这篇中就说说函数栈帧.
在这里使用环境是vs2013,不要用太高级编译器,高级编译器不好观察函数栈帧创建和销毁过程.且在不同的编译器下栈帧创建是有差异的,具体的要看编译器实现.
说道函数栈帧,就要谈到寄存器,今天主要要用ebp,esp两个寄存器.这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的.我们也知道每一个函数的函数调用都会在栈上开辟一块空间.(栈使用地址是由高到低的)
如上图所示上图的代码这个程序main函数就会在栈上开辟一块空间,ebp为栈底指针,esp为栈顶指针,去维护当前在使用的函数栈帧.
注:在VS2013中可以看到main函数也是被其他函数调用的.
在这部分要用到汇编代码去理解函数栈帧的创建和销毁.
首先,因为我们已知main函数是其他函数调用的,所以在调用main之前栈上就应该有一段栈帧.如下图_tmainCRTStartup所示.
下面让我们来看下main函数的汇编,第一行,push是压栈在栈上压入ebp的值,同时esp作为栈顶指针要一直指向栈顶,在每次压栈后都会向上移动.最后栈的结果就大概如下图所示.
第二行,mov指令是让ebp移动到esp的位置上.在这行指令后ebp,esp暂时都指向栈顶.如下图
第三行是让esp减去16进制下的0E4这个数字,因为栈是从高地址向低地址开空间,所以这行实际上是为main函数开辟了空间.如下所示:
下三行继续是压栈.前面已经说过类似的操作了,经过下面三次压栈,栈区大概会变成下图所示的样子.
下面那行后面开起来很乱其实是下面第一行这样.lea指令是计算某部分地址并将地址存到寄存器中.所以下面指令就是计算ebp到ebp-0E4h的地址并存到edi中.结合他下面的三行指令,他们一起将新开辟的(上面空白的)空间给上随机的初始值0CCCCCCCh.(这就是如果不进行初始化然后直接打印,会打印出超级的大数的原因,这里的初始随机值是编译器随即设定的,编译器之间可能会有不同)执行之后的结果大概如下图.
再下面的三行,就是在已经开好的函数栈帧中给inta,b,c分配存储他们的空间.并且按照初始化的值给对应的空间初始化.大概如下所示(以及这里这三个int是不是挨着的完全取决于编译器)
接下来我们继续看下面的汇编指令.
第一行是指将ebp-14h这个位置上的值放入eax寄存器中,根据上面已经讲解过的指令可以得知ebp-14h这个位置就是存储b的位置,紧接着的指令是将eax压栈.所以这两句指令是指将b的值拷贝一份后压入栈顶,这两行的下两行与这两行相似,是将a的值拷贝一份后压入栈顶.进行之后的栈类似于下面图片.(这个大步骤就是函数调用时的传参,由这个进行的步骤也可以知道函数传参是从右向左传参的)
下面就到了call指令,call指令是调用函数的指令.call指令执行之后会跳转到call后面的地址所示的指令,并且将call后面紧跟着的指令地址压栈(方便表用后的返回).
在call之后就进入到了Add函数.
既然到了一个新函数中,那就要建立这个函数的函数栈帧,上面的大端和main函数建立栈帧基本一致,同样是开辟函数栈帧并且将空间内的内容全部用随机值处理.处理后大概如下图(图中空白部分应为0CCCCCCCh.
下面就是给z分配空间并且给空间初始化为0.
在z=x+y处,使ebp+8的值放到eax中,这里会发先ebp+8位置上放的正是实参a传参时的拷贝.下一行是将ebp+12位置上的值加到eax中,ebp+12位置上放的正是实参b传参时的拷贝.下面再将eax中算好的加法结果放到z的位置上,就此完成z=x+y.如下图:
下面我们来看最后函数的返回:
第一行把z中的结果放入寄存器中(因为z为局部参数,出了Add函数的作用域就会销毁,所以要借助寄存器返回).
下面几句pop就是把之前压入栈顶的元素弹出,同时esp栈顶指针也会向下移.
mov指令将Add函数栈帧空间释放使得ebp指向esp的地方.
pop ebp中使ebp返回原来压入的main函数的ebp的位置.
ret使程序回到main函数栈帧中,且下一条语句刚好是main函数中之前存好的call Add函数的紧挨着的下一条指令. 这样就完成了函数栈帧的释放.之后就会再执行main函数后面的指令了.
以上就是我了解的函数栈帧的创建与销毁.我们下篇博客见.