博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
函数的调用过程
阅读量:4117 次
发布时间:2019-05-25

本文共 2503 字,大约阅读时间需要 8 分钟。



所有抽象数据类型(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
#include
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
#include
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.

 

  

             

             

           

                   

             

     

        

你可能感兴趣的文章
Vue项目中使用img图片和background背景图的使用方法
查看>>
vue 项目中图片选择路径位置static 或 assets区别
查看>>
vue项目打包后无法运行报错空白页面
查看>>
Vue 解决部署到服务器后或者build之后Element UI图标不显示问题(404错误)
查看>>
element-ui全局自定义主题
查看>>
facebook库runtime.js
查看>>
vue2.* 中 使用socket.io
查看>>
openlayers安装引用
查看>>
js报错显示subString/subStr is not a function
查看>>
高德地图js API实现鼠标悬浮于点标记时弹出信息窗体显示详情,点击点标记放大地图操作
查看>>
初始化VUE项目报错
查看>>
vue项目使用安装sass
查看>>
HTTP和HttpServletRequest 要点
查看>>
在osg场景中使用GLSL语言——一个例子
查看>>
关于无线PCB中 中50欧姆的特性阻抗的注意事项
查看>>
Spring的单例模式源码小窥
查看>>
后台服务的变慢排查思路(轻量级应用服务器中测试)
查看>>
MySQL中InnoDB事务的默认隔离级别测试
查看>>
微服务的注册与发现
查看>>
bash: service: command not found
查看>>