谈谈函数的调用过程,栈帧的创建和销毁

一.什么是栈帧?!

什么是栈帧,引用百度百科的经典解释:“栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。”

 实际上,可以简单理解为:栈帧就是存储在用户栈上的(当然内核栈同样适用)每一次函数调用涉及的相关信息的记录单元。让我们从栈开始来理解什么是栈帧...

 作为一种特殊的数据结构而存在(和“队列”相反的记录结构和操作规则),是一种只能在一端进行插入和删除操作的特殊线性表

栈按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。

  栈有很多自己的特性,它具有记忆功能,对栈的插入与删除操作中,不需要改变栈底指针;而且栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。因此栈作用就是用来保持栈帧的活动记录(即函数调用)。


二、函数调用

每一次函数调用都是一个过程这个过程我们称为函数的调用过程,这个过程要为函数开辟栈空间,用于本次函数的调用中临时变量的保存,现场保护。这块栈空间我们称为函数栈帧。

栈帧的维护必须先了解两个寄存器。 
寄存器ebp称为“基址指针”,在未受改变之前始终指向栈底,用途是:在堆栈中寻址。 
寄存器esp称为“栈指针”,会随着数据的入栈出栈移动,也就是说始终指向栈 顶。

还有几个通用寄存器:EAX,EBX,ECX,EDX.EIP(PC).

其中EIP(PC)是程序计数器,用来保存当前正在执行的指令的下一条指令地址。

当我们要详细研究函数调用过程,必须得对照的汇编代码。 

从main函数的地方开始,要展开main函数的调用就得为main函数创建栈帧。 

根据汇编代码和栈帧结构内存分布详细说明每一步:

源代码:

#include<stdio.h>
int myadd(int _a,int _b)
{
	int z=_a+_b;
	return z;
}
int main()
{
	int a=0XAAAAAAAA;
	int b=0XBBBBBBBB;
	int ret=myadd(a,b);
	printf("you should run here!%d\n",ret);
	system("pause");
	return 0;
}

先说明一下程序内存分布:如下图所示且将栈放大讨论:

以上代码运行后进行调试,转成汇编语言,打开Registers,打开内存Memory,反汇编Disaassembly,打开Call stack:

首先 ,映入眼帘的是如下图所示

由此可知:C程序第一个被调用的函数是mainCRTStartup()

按F11,接下来开辟main函数的栈帧并定义并初始化a,b在main函数栈顶与栈底之间如下图所示:  



继续按F11,程序继续运行,a,b进行入栈操作,此时Registers和Disaassembly如下图所示:

1>.b入栈:


2>.a入栈:

 



由上面的结果可以看出:此时的a,b均为临时变量,并且在 形参实例化的时候,它的顺序是由右向左的。

接下来就是执行call指令:

特别注意的是:call指令会有两个动作:(1)将当前正在执行的汇编指令的下一条指令地址予以入栈保存,以便下次恢复。


            


                                                           

(2)跳转(JMP)至目标函数入口地址处(修改EIP(PC))。

   

此时栈帧中的情况:


  ESP下移44h:





继续按F11,程序继续运行:

指向a的时候:


>mov eax,dword ptr[ebp+8]:[ebp+8]指向的是a,把a放到eax中。

指向b 的时候:


>add eax,dword ptr[ebp+0Ch]: eax的内容是a的内容,[ebp+0Ch]是b的内容。相加后的结果保存在eax中

add执行结束后就该return返回结果了

函数调用结束后是需要将该函数的栈帧空间释放的,那么都已经释放了,计算后的结果z是如何得到呢?!

sum(a+b)的时候:


>mov  dword ptr [ebp-4],eax:[ebp-4]里的内容是z,将eax的内容放到z中。



由上面可知:此时计算的结果(z值)是会保存在EAX中的。

继续按F11,程序继续运行:


>mov    esp,ebp:把ebp放在esp,内容和ebp一样


继续按F11,程序继续运行:


>pop ebp:将栈顶的内容弹出来,弹到ebp,栈顶放的是main函数的ebp地址,将main函数的ebp弹到栈底。esp上移


接下来就是执行ret指令(即栈帧的释放过程)

值得注意的是:ret也有两步动作(1).将当时保存的地址(即call指令的下一条指令地址)地址进行出栈,弹出栈顶地址。

继续按F11,程序继续运行:

(2)将弹出的数据修改EIP(PC)




其中由 mov  dword ptr [ebp-0Ch],eax可知:函数常规的返回值通过寄存器返回。(该程序中由eax返回)

以上过程完成了对一个函数的调用,即实现了为一个函数开辟栈帧到释放栈帧的过程。

释放栈帧只是将指向它的指针去除了,是这段空间成了没有指向的空间(即为可以再次被利用的空间),但是里面的值并未删除或是修改。所以呢,如果你刚刚释放了栈帧还想用栈帧里面的数据时候是可以找到的,只不过这段空间可能会随时被其他程序利用并修改内部的值。

另外:

1). 一个变量修改另一个变量,只需知道一个变量的地址然后通过指针的来访问:

#include<stdio.h>  
#include<windows.h>  
int add(int x,int y)  
{  
    int *p=&x;  
    p++; //指向上一个地址y的地址  
    *p=40;  
    int z=0;  
    z=x+y;  
    return z;  
}  
int main()  
{   int a=10;  
    int b=20;  
    int ret=add(a,b);  
    printf("ret=%d\n",ret);  
    system("pause");  
    return 0;  
}  

不用改变a,b的值通过改变*p的值来修改变量,来修改结果。

前提是知道x变量的地址。

2). 在调用中插入第三个调用;

#include<stdio.h>  
#include<stdio.h>  
#include<windows.h>  
void *g_ret=NULL;  
void bug()  
{      
    int x=0;  
    int *p=&x;  
    p+=2;         //指向返回值地址  
    *p=(int)g_ret;  
    printf("i am bug\n");  
    system("pause");  
}  
int add(int x,int y)  
{  
    printf("add begin run\n");  
    int *p=&x;        
    p--;              //找返回值  
    g_ret=(void*)*p   //把p的内容放到g_ret中保存起来  
    *p=(int)bug;      //把bug的地址填到返回值  
    int z=0;  
    z=x+y;  
    return z;  
}  
int main()  
{    int a=10;  
    int b=20;  
    int ret=0;  
    printf("main begin run\n");  
    ret=add(a,b);  
    printf("ret=%d\n",ret);  
    __asm{  
    sub esp,4  
    }  
    system("pause");  
    return 0;  
}  
调用bug函数,是直接跳转,没用call。调用call要用push,返回ret用了pop,调bug没用push,返回的时候有用pop,整个调用多pop了一次,栈帧的栈顶上移了,所以应该把栈顶的内容下移,所以要用
__asm{  
   sub esp,4  
   }  
来调整栈帧平衡。












猜你喜欢

转载自blog.csdn.net/zy_20181010/article/details/80258819
今日推荐