函数调用与栈帧
3. 利用下述程序理解函数调用时创建栈帧的过程
#include<stdio.h>
#include<windows.h>
void *main_ret =NULL;
int bug()
{
int first = 0;
int *p =&first;
p+=2;
*p =(int)main_ret;
Sleep(1000);
printf("haha,I catch you!, I am a bug!\n");
Sleep(1000);
return 0;
}
int myadd(int x,int y)
{
int *p = &x;
int z=0;
p--;
printf("beginmyadd...\n");
main_ret=(void*) *p;
*p =(int) bug;
z = x + y;
printf("endmyadd...\n");
return z;
}
void mytest(intx, int y)
{
int z = 0;
int *p = &x;
int *q = &z;
p--;
q+=2;
printf("%#x,%#x\n", p, q);
}
int main()
{
int a =0xAAAAAAAA;
int b =0xBBBBBBBB;
int c = 0;
printf("beginmain...\n");
//mytest(a, b);
c = myadd(a, b);
_asm{
sub esp,4
}
printf("youshould run here!\n");
printf("result:%d\n", c);
printf("endmain...\n");
system("pause");
return 0;
}
以下是该程序的部分汇编代码,通过该汇编代码,理解函数调用时创建栈帧的过程
因为main函数也是被调用的,所以也会创建栈帧,如上图所示
初始时,栈底指针和栈底指针指向main函数的栈帧起止处。
int a = 0xAAAAAAAA;
(1)00401208 mov dword ptr[ebp-4],0AAAAAAAAh
int b= 0xBBBBBBBB;
(2)0040120F mov dword ptr[ebp-8],0BBBBBBBBh
int c = 0;
(3)00401216 mov dword ptr [ebp-0Ch],0
上述三条语句,表示a,b,c放置的位置如上图所示
c = myadd(a, b);
(4)0040122A mov eax,dword ptr [ebp-8] //将b放入寄存器eax中
(5)0040122D push eax //将eax压入栈中
push的作用:栈顶指针下移,将操作数压入栈顶,此句表示将b压入栈顶,见图中(5)
(6)0040122E mov ecx,dword ptr [ebp-4]
(7)00401231 push ecx //同(4),(5),将a压入栈中,见图中(7)
(8)00401232 call @ILT+15(myadd)(00401014)
执行到call语句时,按f11进入call函数。
call的作用有两个:第一,将正在执行指令的下一条指令的地址(00401237)压入栈中
第二,跳到调用函数处,见下述(9)
00401237 add esp,8
0040123A mov dword ptr [ebp-0Ch],eax
(9)00401014 jmp myadd (004010d0)
//004010d0表示调用函数的入口地址,程序要执行myadd函数,寄存器eip的内容要存有下一条要执行指令的地址,所以,执行语句(9)时,寄存器eip的内容变为004010d0
myadd(a, b)
{
(10)004010D0 push ebp //该语句表示将main函数的栈底指针内容压入栈中,见图中(10)
(11)004010D1 mov ebp,esp
//该语句是将栈底指针的地址传给栈顶指针,所以栈顶,栈底指针指向同一处,见图中(10),(11)
(12)004010D3 sub esp,48h //表示栈顶指针减去48h,见图中(12)
此时表示myadd函数栈帧已经创建好
int *p = &x;
(13)004010E8 lea eax,[ebp+8]
(14)004010EB mov dword ptr [ebp-4],eax
//(13)(14)两句表示创建好指针变量p,并将上述x的地址赋给p
int z=0;
004010EE mov dword ptr [ebp-8],0//创建好变量z
p--; //此时p指向x的下一个地址,即main:ret处
004010F5 mov ecx,dword ptr [ebp-4]
004010F8 sub ecx,4
004010FB mov dword ptr [ebp-4],ecx
main_ret =(void*) *p; //将p指向的内容保存在去全局变量main_ret中
0040110B mov edx,dword ptr [ebp-4]
0040110E mov eax,dword ptr [edx]
00401110 mov [main_ret (00429c64)],eax
*p =(int) bug; //将p指向的内容改为bug函数的入口地址,即图中main:ret变为bug函数的入口地址
00401115 mov ecx,dword ptr [ebp-4]
00401118 mov dword ptr [ecx],offset @ILT+5(bug) (0040100a)
z = x + y; //将x + y即a+b得结果赋给变量z,将图中(15)处
0040111E mov edx,dword ptr [ebp+8]
00401121 add edx,dword ptr [ebp+0Ch]
00401124 mov dword ptr [ebp-8],edx
return z;
00401134 mov eax,dword ptr [ebp-8] //返回z值时,是先将该值保存到寄存器eax中
} //此时myadd函数的调用完成
接下来就是销毁myadd函数栈帧的过程:
00401144 mov esp,ebp
//先将ebp的值赋给esp,即栈顶,栈底指针此时都指向myadd函数栈帧的起始处
00401146 pop ebp
//pop:将栈顶指针中的内容弹出到ebp中,栈顶指针上移,即栈底指针重新指向main函数的起始处,而//栈顶指针指向图中的main:ret处,即后来内容被修改为bug函数入口地址处的位置
00401147 ret
//ret:将栈顶指针指向的内容弹出到寄存器eip中,栈顶指针esp上移,见图中的(16)
//因为寄存器eip中的内容时程序要执行的下一条指令,所以执行完ret后,程序开始执行调用bug函数
0040100A jmp bug (00401030) //bug函数的入口地址为00401030,所以eip的内容变为00401030
int bug()
{
00401030 push ebp
//与myadd函数的调用类似,将main函数的ebo压入栈顶,栈顶指针下移,见图中(17)
00401031 mov ebp,esp
//栈底指针也指向栈顶处,将图中(18)
00401033 sub esp,48h//栈顶指针减48h,栈顶指针下移,见图中(18)
此处,创建好bug函数的栈帧
int first = 0; //在bug函数的栈帧中创建变量first
00401048 mov dword ptr [ebp-4],0
int *p= &first; //在bug函数的栈帧中创建指针变量p,并指向first
0040104F lea eax,[ebp-4]
00401052 mov dword ptr [ebp-8],eax
p+=2; //p上移两个单元
00401055 mov ecx,dword ptr [ebp-8]
00401058 add ecx,8
0040105B mov dword ptr [ebp-8],ecx
*p = (int)main_ret;
//将p指向的内容改为)main_ret,即main函数在调用myadd函数时的下一条指令地址
0040105E mov edx,dword ptr [ebp-8]
00401061 mov eax,[main_ret (00429c64)]
00401066 mov dword ptr [edx],eax
}//此时bug函数调用结束
004010AC mov esp,ebp //esp上移到ebp处,见图中(20)
004010AE pop ebp //弹出栈顶内容到ebp中,栈底指针指向main函数栈帧的起始处
004010AF ret //bug函数的栈帧销毁
开始返回到main函数继续执行调用myadd函数之后的指令:
00401237 add esp,8
0040123A mov dword ptr [ebp-0Ch],eax
……..
执行结束!
运行结果:
总结:
1. 形参实例化的变量在两栈帧之间
2. 形参实例化的变量从右向左开始创建
3. 每调用一次函数形成一个栈帧,函数自身形成自己的栈帧,函数定义的变量在自己的栈帧中,这些变量的生命周期随栈帧结束而释放,所以称之为临时变量
4. 常规情况下,函数的返回值以寄存器的形式返回。