函数调用过程以及栈帧详解
函数的调用是一个过程,那么在函数的调用过程中要开辟栈空间,用来对本次函数的调用中需要的临时变量保存。这块空间叫栈帧。这个过程调用包括将数据和控制从代码的一部分传递到另一部分。过程调用的任务:为过程的局部变量分配空间,并在退出时释放这些空间,俗称保存现场/恢复现场。栈的作用:参数传递、局部变量分配、保存调用的返回地址、保存寄存器以供恢复栈帧:为单个过程分配的那部分栈称为栈帧
这是代码在内存的分布:
一般栈形图如下:
下面,我们用一个简单的函数调用过程来看这个过程。我们写了一段简单的代码:
#include<stdio.h>
int Add(int x,int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("result: %d\n", ret);
return 0;
}
对应的反汇编为:
int main()
{
008C17D0 push ebp //把ebp压入栈中,方便返回
008C17D1 mov ebp,esp //把esp赋值给ebp
008C17D3 sub esp,0E4h //产生新的esp
(这一部分的功能就是开辟新的栈帧)
008C17D9 push ebx
008C17DA push esi
008C17DB push edi
008C17DC lea edi,[ebp-0E4h]
008C17E2 mov ecx,39h
008C17E7 mov eax,0CCCCCCCCh
008C17EC rep stos dword ptr es:[edi]
(把栈帧中开辟的空间初始化)
int a = 10;
008C17EE mov dword ptr [a],0Ah
int b = 20;
008C17F5 mov dword ptr [b],14h
(局部变量的创建)
int ret = Add(a, b);
008C17FC mov eax,dword ptr [b]
008C17FF push eax
008C1800 mov ecx,dword ptr [a]
008C1803 push ecx
008C1804 call _Add (08C10FAh) //call指令有两个作用把call下一条指令存进去,来方便下一次查找,然后:jmp,跳转到_add
(把中间变量存储)
#include<stdio.h>
int Add(int x,int y)
{
008C16C0 push ebp
008C16C1 mov ebp,esp
008C16C3 sub esp,0CCh
008C16C9 push ebx
008C16CA push esi
008C16CB push edi
008C16CC lea edi,[ebp-0CCh]
008C16D2 mov ecx,33h
008C16D7 mov eax,0CCCCCCCCh
008C16DC rep stos dword ptr es:[edi]
(类似于main函数,自己给自己开辟新的栈帧,然后初始化)
int z = 0;
008C16DE mov dword ptr [z],0 //创建z变量
z = x + y;
008C16E5 mov eax,dword ptr [x]
008C16E8 add eax,dword ptr [y]
008C16EB mov dword ptr [z],eax
(进行a,b相加并把结果存在eax中,通过eax寄存器待会a+b的值)
return z;
008C16EE mov eax,dword ptr [z]
}
008C16F1 pop edi
008C16F2 pop esi
008C16F3 pop ebx //pop 使esp下移
008C16F4 mov esp,ebp
008C16F6 pop ebp
008C16F7 ret //ret要进行两个步骤,pop把pop的内容保存在ebp中,然后jump到call命令的下一跳。
008C1809 add esp,8
008C180C mov dword ptr [ret],eax
printf("result: %d\n", ret);
008C180F mov eax,dword ptr [ret]
008C1812 push eax
008C1813 push offset string "result: %d\n" (08C6B30h)
008C1818 call _printf (08C1325h)
008C181D add esp,8
return 0;
008C1820 xor eax,eax
}
008C1822 pop edi
008C1823 pop esi
008C1824 pop ebx
}
008C1825 add esp,0E4h
008C182B cmp ebp,esp
008C182D call __RTC_CheckEsp (08C1118h)
008C1832 mov esp,ebp
008C1834 pop ebp
008C1835 ret
从中我们发现
中间变量存在两个栈帧之间,
形参的读取是从右向左的~如图,先b后a
临时变量存在自己的栈帧中用完,随着栈帧一起释放,
自己的栈帧是自己开辟的。
返回值是通过寄存器如eax带回返回值的。
pc指针及elp存的是当前指令的下一条指令。
我们知道函数是通过call实现跳转,ret进行返回
然后我们观察栈帧发现,call的下一跳指令存在自定义变量的上第2个(指针p+=2),所以只要我们自己定义一个变量,顺着就可以找到call的下一跳指令,然后通过指针对地址进行修改。
同样的,我们可以观察main返回存储在栈帧中的位置我们可以发现,我们可以通过找到形参(这里要是最后一个形参,及最左的),(指针P--)就可以找到。
我们可以在vc6.0上试一下这个程序。
_________________________________________________________________________________
#include<stdio.h>
#include<windows.h>
int main_ret = 0;
int bug()
{
int i = 0;
int *p = &i;
p += 2;
*p = main_ret;
Sleep(1000);
//system("cls");
printf("joke,you,now you are in bug!\n\n");
//system("pause");
return 1;
}
int Add(int x,int y)
{
int z = 0;
int *p = &x;
p--;
main_ret =*p;
*p = (int)bug;
printf("now is in Add\n");
z = x + y;
printf("jump out Add\n");
return z;
}
int main()
{
printf("begin in main\n");
int a = 10;
int b = 20;
int ret = Add(a, b);
_asm {
sub esp, 4;
}
printf("result: %d\n", ret);
printf("return to main\n");
system("pause");
return 0;
}
_____________________________________________________________________
运行结果如下~