什么是栈帧?
我们知道每一次函数调用都是一个过程。
这个过程我们通常称之为:函数的调用过程。
这个过程主要为函数开辟栈空间,用于本次函数调用中临时变量的保存、现场保护。这块栈空间我们称之为函数栈帧。
我们用一段简单的c程序来具体了解下函数栈帧:
c程序:
#include<stdio.h>
#include<windows.h>
#include<stdlib.h>
int My_Add(int a,int b)
{
int z=a+b;
return z;
}
int main()
{
int a=10;
int b=020;
int ret=My_Add(a,b);
printf("%d\n",ret);
system("pause");
return 0;
}
当我们要详细研究函数的调用过程,必须得对应汇编代码。
在这之前,我们先要做一些准备工作。
首先,我们要了解下CPU中的寄存器,它们对我们后面对函数栈帧进行剖析时提供了很大的帮助。
CPU中的寄存器:
EIP(又叫作程序计数器):指向当前正在执行指令的下一条指令的地址。
ESP(栈顶指针):存放了指向函数栈帧栈顶的地址。
EBP(栈底指针):存放了指向函数栈帧栈底的地址。
还有四个通用寄存器:EAX、EBX、ECX、EDX。
接下来我们还得了解一个概念:就是程序存放的地址空间,我们通常管地址空间叫内存(但严格意义上,地址空间不是内存)。我们用一个示意图来表示:
这就是一个较为完整的地址空间,地址又低到高依次是:代码区、字符常量区、已初始化全局变量区、未初始化全局变量区、堆区、栈区,其中堆、栈相对而生。(栈是由上到下生长的)。在这里,为了后面图解的方便,我将栈区放大。
有了这些准备工作,下来,我们开始正式了解函数栈帧。
(这次研究是在vc6.0编译器下运行)
首先,我们将代码输入好,F10进入编译,通过工具栏的view调出我们的Memory内存窗口以及Registers寄存器窗口。
我们的代码是从main函数开始,要展开main函数就得为main函数创建栈帧。按F11调试。
这里是一些对main函数的系统调用,这里我们不做具体分析,简单解释一下:
我们发现其实main函数在__tmainCRTStartup函数中调用的,而__tmainCTRStartup函数是在mainCRTStartup被调用的。
下来我们主要对我们所写的代码进行具体剖析:
继续F11调试:
这两句汇编代码其实就是在我们栈帧结构中为变量a、b开辟空间,我们看看在栈帧中是怎样的?
继续F11调试
此时,我们的栈帧结构是这样的:
此时我们将A和B存放到寄存器ECX和EAX中,比入栈保存。
这时,我们监视窗口对EAX、ECX进行监视:
此时进入CALL命令:
CALL命令会完成两个工作:
1. 保护CALL命令之后的指令地址。
2. 通过jmp跳转至目标函数的地址入口。
这里我们看看EIP和ESP:
接下来我们已经进入到我们所写的My_Add函数中了。
继续F11:
此时的栈帧
其实在这里就已经为我们My_Add函数创建了栈帧结构。
在这里我们可以看出函数的形参实例化是从右至左的,因为在main函数中是先将a压入栈中,再是b。但在函数调用的时候是将b提取出来再是a,由此可见临时变量是在函数调用前就形成的,所以我们的形参实例化是从右至左的。
继续F11:
此时的栈帧结构为:
调用完函数就该返回了,这里是怎样返回的,我们来看一下:
我们知道,函数调用完所产生的空间是要被释放的,如果这样我们a+b怎么得到的?
从这条指令我们可以看出,返回值是由寄存器eax得到的。
继续F11:
此时栈帧结构为:
继续F11:
这时的栈帧结构为:
继续F11:
接下来进入ret命令。ret命令也会完成两个工作:
1. 将当时保存的地址出栈。
2. 修改EIP
此时的栈帧结构为:
继续F11:
此时的栈帧结构:
至此,我们完成了一次函数调用,也了解到了地址空间为函数创建栈帧空间的过程以及参数的传递和临时变量的形成。
小思考:
现在我们想用main函数跳转至My_Add函数,再由My_Add函数跳转一个bug函数,再由bug函数回到我们的main函数。
这里我把代码改一下:
Int My__Add(int a,int b)
{
Int *p=&a;
Int z=0;
g_ret=(void*)*p; // 把p的内容保存起来,这里就需要定义一个全局变量g__ret
*p=(int)bug;
Z=a+b;
printf(“My__Add run...\n”);
return z;
}
前面我们已经了解到,CALL命令会将其下一条指令的地址保存起来,然后通过jmp跳转,这时我们通过修改入口地址,将本应该返回到main函数改成进入bug函数。
void Bug()
{
Int a=0;
Int *p=&a;
P+=2;
*p=(int)g_ret;
printf(“i am bug!\n”);
system(“pause”);
}
但在这个地方有问题,调用函数需要使用CALL命令,返回函数要使用ret命令。但在这里,当我们通过My__Add函数进入bug函数时,没有使用到CALL命令,但返回时依然使用了ret命令,这样整个调用过程多了一次POP(出栈),导致栈帧结构失去平衡,使栈顶向上移,这时,我们就需要保持平衡,让栈顶下移,所以我们要用到一个指令 __asm,其功能是在c语言代码中插入汇编指令,所以我们只需要在main函数中添加这么一句:
__asm
{
Sub esp,4
}
(将栈顶下移,确保栈帧平衡)
最后F5运行,得到的结果应该是:
Main --------->My_Add “My_Add run...”
My__Add --------->bug “i am bug!”
Bug ---------------> “You should run here!”
代码:
#include<stdio.h>
#include<windows.h>
#include<stdlib.h>
void *g_ret=NULL;
void bug()
{
int a=0;
int *p=&a;
p+=2;
*p=(int)g_ret;
printf("i am bug!\n");
system("pasue");
}
int My_Add(int a,int b)
{
int *p=&a;
int z=0;
p--;
g_ret=(void*)*p;
*p=(int)bug;
z=a+b;
printf("My_Add run...\n");
return z;
}
int main()
{
int a=10;
int b=20;
int ret=0;
printf("You should run here!\n");
ret=My_Add(a,b);
printf("ret=%d\n",ret);
__asm
{
sub esp,4
}
system("pasue");
return 0;
}