首先,我们得了解什么是栈帧。
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的的一种数据结构。
C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
函数的调用需要在栈区创建一个空间,又叫做函数栈帧的创建,而调用完之后又得销毁这块空间,又叫做函数栈帧的销毁。
而在了解函数栈帧的创建和销毁之前我们需要了解一下相关知识。
寄存器:eax,ebx,ecx,edx,ebp,esp。
eax:累加器寄存器
ebx:基地址寄存器
ecx:计数器
edx:通用寄存器
ebp:基址指针,或者是栈底指针
esp:堆栈指针,或者是栈顶指针
而ebp和esp是用于维护函数栈帧的寄存器,正在调用哪个函数,ebp和esp就去维护哪个函数的函数栈帧。
栈区的使用是从高地址到低地址。
push:为栈增加一个元素的操作
pop:从栈中取出一个元素的操作
mov:mov是传送数据的指令,mov ax,0123H表明把0123h这个值给ax,h代表0123是十六进制数 dword:双字,也就是四个字节
ptr:pointer的缩写,也就是指针
[]:[]里的数据是一个地址数,这个地址指向一个双字型数据
sub:减法指令,sub ax,bx 等同于: ax = ax - bx
add:加法指令,sub ax,bx 等同于: ax = ax + bx
lea:目标地址传送指令
rep:指令的目的是重复其上面的指令,ecx是重复的次数
stos:stos指令的作用是将eax中的值拷贝到ES:EDI指向的地址
call:call也是转移的效果,转到call之后写的标识符
ret:ret是子程序或函数返回指令
现在让我们从研究vs2013环境下的自定义的加法函数来研究函数栈帧的创建和销毁。
代码如下:
按F10逐行调试完代码后,我们会发现一下情况
其中__tmainCRTStartup和mainCRTStartup其实是两个函数,他们与main函数的关系其实是
现在让我们用反汇编看一下代码的汇编情况
第一步的操作则是为栈区增加一个元素也就是压栈操作
而后的move操纵则是将esp的值给ebp,即
之后的sub则是将低地址的值赋给esp
即
为main函数预存储一块空间
而之后的三次push操作则是压入ebx,edi,esi三个元素
之后的lea操作把[ebp-0E4h]的地址传入edi
根据上述的前置知识可知,这三步的目的则是将ebp和esp之间的内容全部变为0CCCCCCCCh,即
而这三步操作
即
就是将三个值分别赋到这个地址的内容中去
而以下的操作
则是分别将ebp-8和ebp-14h的值分别先存入eax和ecx,然后进行压栈操作
而后的call指令则是将call指令之后下一条指令的地址放入栈区
而后就是进入add函数的汇编操作,汇编代码如下
而add函数的函数栈帧的创建与main函数的函数创建相同
其中ebp-8也是同main函数一样对add函数临时变量z的创建
这步是将ebp+8的值赋给eax
而这部则是家ebp+0ch的值加到eax中实现加法运算
而后通过这步将两数之和传给ebp-8,也就是z
以此完成加法运算
而这步则是将z的值赋给eax,由于寄存器不随函数栈帧的销毁而销毁,故我们通过寄存器eax将z的值返还给main函数
接下来进行pop操作,即出栈操作,可以理解为压栈的反操作,操作后变为
而接下来的操作则是将ebp和esp转移,用于维护主函数
而这个ebp-main处存放的main函数的地址,此时就可以保证ebp能成功回到main函数的栈底
而ret则是让程序进入下一步操作的地址,即
保证调用函数结束能继续下一步的操作
至此,add函数的函数的创建和销毁全部完成
之后main函数的销毁也与此相同,读者可自行尝试之后的相关操作。
这是本人第一次对函数栈帧的创建和销毁进行相关的简单描述,如果有什么不足之处,还请各位老铁赐教