C++--函数栈帧的创建与销毁
一.什么是函数栈帧函数栈帧是程序在运行时为每一次函数调用在栈内存中分配的一块独立的内存空间它在函数调用时创建在函数运行结束后销毁。二.函数栈帧的创建与销毁以这样一个简单的函数为例首先程序运行时要调用main函数会调用一些汇编指令创建main函数的栈帧如图的三条指令就是创建main函数栈帧的过程其中ebpesp都是寄存器ebp是指向栈底的指针esp指向栈顶这里向创建了栈底指针然后使栈顶指针和栈底指向同一地方最后减小栈顶指针移动到栈顶与栈底间大小为0E4h这样就创建了main函数的栈帧如图如图这些汇编指令是分别是在将 ebx、esi、edi 这三个寄存器的值压入栈中保护。内存初始化将区域初始化为0xCCCCCCCC中文字符为著名的烫烫烫用于防止未初始化变量的错误。调试器检查。这些汇编指令与函数栈帧的创建与销毁关系不大这里不再深入讨论。如图紧接着这些步骤首先是将main函数内的局部变量入栈注意这里并不是压栈操作压栈的指令是push这里使用的是move指令是通过直接修改栈内存来赋值。这里在指令上访问局部变量都是通过栈底指针的偏移来访问的例如这里的a入栈的指令实际上是mov dword ptr [ebp-4], 0Ah 但是这里为了方便阅读修改成了a。这里的a和b进行了赋值但是ret的值暂时不确定。a,bret入栈后main函数的栈帧如图紧接着是sum中的形式参数a,b进行压栈操作谁调用谁压栈将a,b从右向左压入main函数的栈顶这里ab mov指令同样作了便于阅读的修改不再赘述。a,b压栈后栈顶指针要进行修改这里vs将修改操作进行了隐藏。将形参ab压栈后main函数的栈帧如图如图,接下来的call指令的作用有两个其一是将本指令下一行指令的地址压入栈中即将0x00E818D1这一地址压栈目的是让main知道执行完sum函数后要从此处接着执行。其二是让程序直接跳转到sum函数的地址0E8117Ch去执行sum函数。add指令的作用是sum函数运行结束后将esp的地址加8实际上执行的是将已经不需要的形参a出栈。最后mov指令的作用是将sum函数的返回值给ret。这里将add指令地址压栈后的main函数栈帧如图接下来main完了就进入sum函数。如图与main函数一样先通过栈顶指针与栈底指针创建sum函数的栈帧注意这里push ebp时将main函数栈帧中的ebp压栈保存了然后执行保存寄存器内存初始化调试器检查操作。如图然后将temp入栈不是压栈,之后将形参a,b的值加到eax寄存器中再将eax的值移动到temp中之后因为temp是局部变量无法出sum函数,需要通过eax寄存器带出即将temp的值移动到寄存器eax中。经过这些操作sum与main的栈帧如图sum函数运行结束后到sum的右括号时会执行指令 mov esp, ebppop ebppop将存入栈中的main中的ebp的值出栈并赋值给ebp使ebp重新指向了main栈帧的栈底 ,经过这些操作销毁了sum的栈帧但是注意这里并没有对栈上的数据进行清理此时访问虽然并不安全但是能得到正确的访问结果,此时函数栈帧如图紧接着会执行 ret 指令将栈顶内容出栈并将栈顶的内容存入CPU的PC寄存器中而此时栈顶存放的是前面存放的sum函数执行完毕后下一行指令的地址而CPU运行指令的顺序看的就是PC寄存器中的地址由此sum函数就知道执行完自己后再执行什么指令也就是说此时将执行前面讲过的指令将形参a,b出栈并利用eax寄存器中存放的tempab的值给ret赋值最后main函数执行完毕销毁main函数的栈帧。以上就是函数栈帧的创建与销毁。为什么压栈要从右向左压栈操作之所以要从左向右是因为C/C要支持可变参函数如printf这类可变参函数的参数个数不确定func(int a,...)而编译器需要从第一个参数开始访问我们知道编译器访问栈中元素是通过栈底指针的偏移但是假设这里是从左向右压栈那么先压栈的a就在最下面而我们编译器在编译阶段根本不知道我们究竟传入了多少个参数导致了编译器找不到a。而如果我们从右向左压栈那么这里的a最后压栈永远在栈顶通过ebp的偏移就很轻松的能够访问到a了之后从a向下访问就行了。