函数调用过程——栈帧

函数的调用是一个过程,那么在函数的调用过程中要开辟栈空间,用来对本次函数的调用中需要的临时变量保存。这块空间叫栈帧。这个过程调用包括将数据和控制从代码的一部分传递到另一部分。过程调用的任务:为过程的局部变量分配空间,并在退出时释放这些空间,俗称保存现场/恢复现场。栈的作用:参数传递、局部变量分配、保存调用的返回地址、保存寄存器以供恢复栈帧:为单个过程分配的那部分栈称为栈帧

这是代码在内存的分布:

一般栈形图如下:

面,我们用一个简单的函数调用过程来看这个过程。我们写了一段简单的代码:

#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;
}

_____________________________________________________________________

运行结果如下~


猜你喜欢

转载自blog.csdn.net/weixin_40921797/article/details/78734165