函数调用
函数之间的调用关系是用栈维护的,每 一次函数调用都是一个过程,该过程称为函数的调用过程。该过程需要为函数开辟空间,用于本次函数的调用中临时变量的保存、现场保护,开辟的空间就称为函数栈帧,也叫函数运行时堆栈。接下来我就简要介绍一下函数调用过程中,栈帧的创建与销毁。栈帧的创建需要用到两个寄存器:ebp和esp,在函数调用过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针。ebp存放了指向函数栈帧栈底的地址,称为栈底寄存器或者基址寄存器;esp存放了指向函数栈帧栈顶的指针,称为栈顶寄存器。还有一个较为重要的寄存器,eip寄存器,称为程序计数器,在微机原理中eip也称为PC(point code/program count)指针。eip寄存器是用于存放CPU当前正在执行的指令的下一条指令。
当我们要详细研究函数调用过程,必须得对应汇编代码。以下面这个程序为例,简要介绍一下函数调用过程栈帧的创建与销毁。
#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 = 0;
ret = Add(a,b);
printf("%d\n",ret);
return 0;
}
程序的地址空间主要分为这几部分:栈区(stack)、堆区(heap)、全局区(也称静态区static)、文字常量区、程序代码区(code)。其中全局区可分为初始化区和未初始化区。
注:程序的地址空间指的不是物理地址而是虚拟地址。
今天我们主要关注的是栈区(stack),它是向下生长的,即从高地址指向低地址。
调用过程
对程序进行调试,整个调试过程是在VC6.0下完成的。在调试状态下打开call stack窗口,如图所示,我们可以看到程序在调用main()函数之前,先调用了mainCRTStartup()函数,并为其创建栈帧结构。
1. 要调用main函数,就要为main函数开辟空间,创建栈帧结构。转到反汇编观察main函数的调用过程。
第一条指令将mainCRTStartup的ebp压入栈中,esp减1,紧接着将esp赋值给ebp,产生新的ebp,再将esp减去4Ch,产生新的esp,这是为main函数预开辟空间。再将ebx、esi、edi分别压入栈中,其结构如图所示。
lea指令,load effetive address加载有效地址,rep stos指令重复存储,dword指的是双字,也就是每次存储四个字节。红色框中的这四条指令就是将ebp至ebp-4Ch初始化为0CCCCCCCCh(初始化13h次,也就是19次)。这几条指令就是将为main函数预开辟的空间初始化。
接着执行这几条指令,处理局部变量
在栈底依次存放a,b,ret。
2. 接下来是Add函数的调用。
参数传递过程(即形参实例化)
将ebp-8也就是b放入寄存器eax中,再将eax压入栈中,将ebp-4也就是a放入寄存器ecx,再将ecx压入栈中。也就是先将b压入栈中,再将a压入栈中。
call指令: 1.将当前正在执行的指令的下一条指令的地址压入栈中
2.随即就会跳转至指定函数处(jmp)
如图所示将call指令的下一条指令的地址0040109A压入栈中。
在执行call指令是按F11来到jmp处,再按F11就进入Add函数的执行代码处。
这段代码和main函数调用几乎一样,将main函数的ebp压入栈中,为Add函数预开辟空间,并将其初始化为0XCCCCCCCCh。
创建变量z,将ebp+8即a放入寄存器eax,再将ebp+0ch即b与寄存器eax相加其结果放入eax,也就是执行a+b,将eax放至ebp-4即变量z中。将z的值保存到寄存器eax中。
此时变量z中的值为30。
将edi、esi、ebx出栈,esp向下移动,把ebp赋值给esp,pop指令弹出ebp并将其保存到ebp,使ebp获得新值,回到main函数的栈底。ret指令弹出栈顶,并将其值放入eip,程序执行跳到该地址处。
按F10,跳转至下图处
给ebp加上8,将形参弹出栈结构,再将eax的值放到ebp-0Ch即ret中。
返回值以寄存器的形式返回,至此函数调用结束。为函数开辟的这块空间称为函数运行时堆栈或函数栈帧。
结论:
- 形参实例化的顺序是从右向左;
- 形参实例化,存放于调用函数和被调函数的栈帧之间;
- 临时变量在自己的栈帧中,随栈帧存在,栈帧销毁,临时变量销毁,所以临时变量具有临时性;
- 函数通过ebp和esp来维护自己的栈帧(创建和销毁);
- 常规情况下,函数的返回值以寄存器的形式返回。

函数调用使用栈来维护调用关系。栈帧由ebp和esp寄存器管理,用于保存临时变量和现场信息。调用过程包括栈帧创建(分配空间、初始化)、参数传递、函数执行和返回。在调用结束后,栈帧被销毁,空间回收。形参实例化从右向左进行,返回值通常通过寄存器返回。
333

被折叠的 条评论
为什么被折叠?



