深度理解函数

来源:互联网 发布:100日内成本 的源码 编辑:程序博客网 时间:2024/04/30 07:58

C语言中的函数扮演者很重要的角色,函数可以使程序更简洁,可读性更
好,更便于复用,在实际运用中,很多程序基本都是由许多函数组成的,
为了更好的使用函数,我们需要对函数做一个深度的剖析。而理解函数的关键在于“栈帧”二字,函数的调用实际就是栈帧的创建与释放过程,
在研究函数调用过程之前,先介绍几个关键词。

  1. 内存,c/c++编译的程序占用内存分布如下图
    这里写图片描述

  2. 寄存器EIP:存储当前CPU正在执行指令的下一条指令;

  3. 通用寄存器:eax,ebx,存储数据;

  4. 寄存器ebp:保存的是栈底地址,称为帧指针;

    寄存器esp:保存的是栈顶地址,称为栈指针:

  5. 栈指针和帧指针一次只能存储一个地址,所以,任何时候,这一对指针
    指向的是同一个函数的栈帧。在函数执行过程中,栈指针esp会随着数 据
    的入栈和出栈而移动,因此函数中对大部分数据的访问都基于帧指针ebp
    进行。即esp负责数据的存储与丢弃,而ebp负责数据的读取.

  6. 要详细了解函数调用过程,必须得对应相应的汇编代码。
    下面以一个简单程序为例,来深入研究函数的调用过程。

这里写代码片#include <stdio.h>#include <windows.h>int Add(int x,int y){    int z=0;    z=x+y;    return z;}int main(){    int a=0xAAAAAAAA;    int b=0xBBBBBBBB;    int c=Add(a,b);    printf("%c",c);    system("pause");    r,eturn 0;}

一 main函数栈帧创建

我们知道main函数其实也是函数,作为程序的入口,它在使用过程中也
是被调用的,main函数开始被mainCRTStartup函数调用。要展开main
函数的调用就得为main函数创建栈帧,下面先来看下main函数栈帧的创建过程

这里写图片描述查看寄存器如下

这里写图片描述

二 ADD函数调用

main函数栈帧创建成功后,接下来就是Add函数的调用,参数传递过程如下

这里写图片描述

这里我们主要要明白两点,(1)call指令的两个功能,功能1是将main函
数的pc压入堆栈,因为pc指针里面放的始终是下一条要执行指令的地
址,这里把pc压入堆栈进行保护,是因为在执行fun函数的时候pc指针里
面放的将会是fun函数需要执行里面的内容,同样的道理,我们知道某一
时刻只能有一个栈帧结构,当要去到fun函数的时候,寄存器ebp就要存
储fun函数的帧指针,所以main函数的帧指针就要保存起来,即图
中“main ret”,功能二是跳转到目标函数函数地址(见图中蓝色标记)
查看main函数的汇编语言也可以详细的看到这一点。

1.将当前正在执行的指令的下一条指令地址压入栈中

这里写图片描述

2 跳到目标函数地址处

这里写图片描述

三 Add函数代码执行处

按F11得

这里写图片描述

再按f11就进入Add函数内部

这里写图片描述

过程详解如下:
此时已经进入Add函数内部,开始形成Add函数自己的栈帧
首先将ebp压入栈中(ebp是寄存器,在CPU中,栈在内存里)

这里写图片描述

压ebp最终目的是为了形成新的栈帧,压入后ebp成为新栈帧的栈底,

而后,执行 mov esp-8语句,将esp往下移动八个字节,此时Add函数

栈帧空间创建成功,如图

这里写图片描述

所以以后在Add定义的变量按理来说都应该在Add函数创建的栈帧里面。

栈帧创建成功以后,开始执行Add函数内的操作,继续看汇编指令,(中

间其他准备操作指令跳过)

这里写图片描述

该指令是将a放入寄存器eax中,继续往下走,可以看到此时CPU将a与b相加,把值再放入寄存器eax中,即本来eax里面放的是a值:AAAAAAAA,

这里写图片描述
执行语句后放的是+b的和,

a

而后执行下一条语句,将eax的值保存在变量z中(变量z是之前定义在

Add栈帧中的临时变量),此时Add函数内部操作执行完毕。下一步开始

执行函数返回值的操作。开始将z的值放入eax寄存器里,让eax将返回值

带回去,Z会随着Add函数栈帧的释放而消失。)

四 返回值部分

这里写图片描述

esp移到和ebp相同的位址,销毁Add函数的栈帧。接着“ebp” 弹出,

将存储内容”main ebp“转移到ebp中,回到main函数栈帧里面,同

add esp向上移4字节至存储“main ebp”的起始位置处。程序

执行下一句,使得ebp不动,esp继续上移4字节到“main ret

继续F11遇到ret指令,将main ret中地址保存在esp中,并跳到该

地址处,再按F11将eax中值给c。如此,函数返回值 z 就被带到main函

数所求值 c里面。详细过程如下图

这里写图片描述

至此,一个函数的调用过程完成。得出以下结论

  1. 栈是往低(小)地址方向扩展的,而esp指向当前栈顶处的元素。通过使用push和pop指令我们可以把数据压入栈中或从栈中弹出。
  2. 指令CALL和RET用于处理函数调用和返回操作。 调用指令CALL的作用是把返回地址压入栈中并且跳转到被调用函数开始处执行。返回地址是程序中紧随调用指令CALL后面一条指令的地址。因此当被调函数返回时就会从该位置继续执行。返回指令RET用于弹出栈顶处的地址并跳转到该地址处。在使用该指令之前,应该先正确处理栈中内容,使得当前栈指针所指位置内容正是先前CALL指令保存的返回地址。可以发现,CALL和RET指令均完成两项工作,从栈中把返回地址存储或恢复,跳转PC到指定位置,可以说,两者的操作是相反的。若返回值是一个整数或一个指针,那么寄存器eax将被默认用来传递返回值。
  3. .函数临时变量在自己的栈帧里,生命周期随栈帧存在而存在,随栈帧释放而消失,
  4. 常规下,函数返回值存放在寄存器里,由寄存器返回相关参数;
  5. 形参室例化 顺序从右向左,如例中c=add(a+b),先在Add函数中创建b再创建a.重点内容
原创粉丝点击