欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

函数的调用过程

程序员文章站 2022-06-14 22:27:02
...


所有抽象数据类型(ADT)都必须明确一件事——如何获取内存来存储值。内存分配方式有三种:一是从静态存储区域分配(全局变量,static变量);二是在栈上创建(局部变量,自动变量);三是从堆上分配(动态内存分配,用malloc或者new申请多大内存,用free或者delete释放内存)。下面来看一个图理解c程序的内存分配:

从低地址到高地址分别为代码区,文字常量区,已初始化全局数据区,未初始化全局数据区,堆区,栈区。

函数的调用过程

在执行函数时,函数内的局部变量的储存单元都可以在栈上创建,下面就来详细介绍堆栈(stack)。

一,栈帧的基础知识

1.栈帧的鲜明特点就是先进后出

2.基本的栈帧操作是push(压入栈顶)和pop(移出栈并返回这个值)。

3.通用寄存器:EAX,EBX,ECX,EDX,esp(栈顶的地址),ebp(栈底的地址);

                       eip([pc]:程序计数器寄存器,当前正在执行指令的下一条指令的地址);

                       call:将当前指令的下一条指令进行保存;然后再跳转到目标函数的入口地址(修改eip);

                        ret:将当前的返回地址出栈;再把弹出的数据修改eip。

二,在vc6.0下实现栈帧(函数调用的过程)

 每次函数调用都是一个过程,我们通常称之为函数的调用的程;研究函数调用的过程对应汇编代码。我们接下来通过下面的程序来认识函数调用的过程:

#include<stdio.h>  
#include<windows.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;  
}  

调用栈的情况:

编写好程序后,按F11进入myadd函数,这时查看堆栈可以看到c程序第一个调用的不是main函数,而是mainCRTStartup()函数;

函数的调用过程

main函数栈帧结构如下:

函数的调用过程

接下来进行调试(转成汇编语言):

1. 从main函数的地⽅开始,要展开main函数的调⽤就得为main函数创建栈帧,那我们先来看main函数栈帧的创建:

函数的调用过程

首先push ebp将ebp(栈底)压栈;mov ebp,esp将esp的值赋给ebp,产生新的ebp;sub esp,4ch给esp减去减去一个16进制的数,产生新的esp.

接下来看mov dword ptr[ebp-4],0AAAAAAAAh 是创建局部变量a(定义并初始化);

             mov dword ptr[ebp-8],0BBBBBBBBBh 是创建局部变量b; 

下面看下栈分配:

函数的调用过程

2.接下来是myadd函数的调用:

函数的调用过程

mov eax,dword ptr[ebp-8]把b放入EAX;push eax把eax的内容压栈;

mov ecx,dword ptr[ebp-4]把a放入EBX;push ebx把ebx的内容压栈;

这时寄存器的内容如下:

函数的调用过程

然后看call 指令:将当前指令的下一条指令的地址进行保存(这里就是00401093入栈);然后再跳转(jmp)到目标函数的入口地址(修改eip成00401020);

执行call指令,汇编语言跳转到了这里:

函数的调用过程

执行jmp后又跳转到这里:

函数的调用过程

这时栈分配为:

函数的调用过程

3.进入myadd函数执行代码处:

函数的调用过程

push  ebp:将ebp的内容(main函数的栈底)入栈;

move  ebp,esp 将esp的内容给ebp(即esb=ebp);

此时栈分配如下:

函数的调用过程

然后执行sub指令esp-44,ESP下移,此时形成myadd栈帧;

接着执行mov  eax,dword ptr[ebp+8]把a放进eax;

               add eax,dword ptr[ebp+12]执行a+b;

               mov  dword ptr[ebp-4],eax的内容给[ebp-4],即z=a+b;

              mov  eax,dword ptr[ebp-4]把z给eax;

此时栈分配如下:

函数的调用过程

接着执行到mov  esp,ebp把ebp赋给esp; 

此时栈分配:

函数的调用过程

pop ebp 出栈,将出栈的内容保存到ebp,回到main函数的栈帧(栈底回到main,栈顶向上移);

执行ret之后进入:

函数的调用过程

执行add esp,8:esp=esp+8,栈底加8向上移;

mov  dword ptr[ebp-12],eax,将结果储存在eax寄存器里,通过寄存器带回函数的返回值;

此时就完成了栈帧的创建和销毁(函数的调用)。

函数的调用过程

通过研究函数调用过程我们发现,调用一个函数要形参实例化,会形成临时变量,且形参实例化是从右向左的。那么我们现在如果不访问最右边的参数,通过a修改b的参数,可以实现调用吗?

形参实例化从右向左是因为低地址先入栈,可以定义指针来寻址,通过以下程序可以实现:

#include<stdio.h>  
#include<windows.h>  
int myadd(int _a,int _b)  
{  
    int z;  
    int *p;//定义一个指针变量p;  
    p=&_a;//p指向a;  
    p++;//指针加一就是加上所指变量的类型,这里指向上一地址;  
   *p=5;//b=5;  
    z=_a+_b;    
    return z;  
}  
int main()  
{  
   int a=10;  
    int b=20;  
    int ret;  
    ret=myadd(a,b);  
    printf("you should run here! %d\n",ret);    
    system("pause");  
    return 0;  
}  

这样就实现了通过b来修改a的参数,打印的结果是15.