C函数栈帧

一个可执行C程序由一系列二进制码组成,程序代码会被翻译成由二进制组成的机器指令。程序执行时,CPU取出程序指令执行,寄存器用于程序中变量数据的交换以及算术运算。由于寄存器数量等因素,程序执行过程中产生的数据(如变量)不会一直保存在寄存器中,变量和寄存器状态等会被保存在为函数分配的一个栈空间中,这个为函数分配的内存空间,就称为栈帧

栈帧可以加深对C中函数调用与返回的理解,了解函数的栈帧也有利于对C中一些问题进行更深入的思考:

  • 函数参数的传递过程
  • 局部变量的生命周期
  • 为什么应该将一些经常调用到的简短的函数定义为内敛函数

这篇文章使用VC++ 6.0对一个错误的swap函数进行调试,分析其栈帧结构,并解释错误的原因

程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

void swap(int x,int y);

int main(){
int a = 100;
int b = 200;
swap(a,b);
return 0;
}

void swap(int x,int y){
int tmp = x;
x = y;
y = tmp;
}


函数的栈帧结构

两个重要的寄存器:

  • ebp:保存当前栈帧的栈底地址
  • esp:保存当前栈帧的栈顶地址

下图是一个典型的函数栈帧结构:

用swap程序来进行说明:

  • 函数main作为主调函数,定义了a,b两个变量,保存在主调函数的局部变量区
  • 在调用swap函数前,会进行值传递,将形参x,y和返回地址压入栈顶
  • 然后保存旧ebp,修改ebp和esp,为函数swap分配栈帧空间
  • 为swap函数中的局部变量tmp赋值,并存入函数swap栈帧的局部变量区
  • 交换形参的值
  • 函数返回时进行一系列操作,恢复主调函数栈帧,从返回地址处取指,继续执行

因此从上述操作步骤中可以知道,由于函数swap交换的是形参x,y的值,因此并没有修改main函数栈帧空间中局部变量a,b的值,所以a,b还是原来的值

对于内联函数,由于缺少函数调用过程中的栈帧初始化等操作,加上函数的指令地址空间相邻,更有利于局部性,因此通常效率更高

下面在VC++ 6.0中调试,验证swap程序的栈帧操作


函数调用

函数被调用时,执行如下步骤:

  • 将参数压栈
  • 将返回地址压栈
  • 将ebp压栈
  • 将ebp设置为与esp相等
  • 减小esp的地址为函数分配栈帧,并清理局部变量区

执行main中第一行代码之前,通过观察寄存器发现当前main函数的栈底地址是0x0018FF48,栈顶地址为0x0018FEF4

局部变量的定义由两条指令完成:

1
2
mov dword ptr [ebp-4],64h
mov dword ptr [ebp-8],0c8h

所以在执行完a,b的定义后,可以看到地址0x0018FF44和地址0x0018FF40中数据的变化:

接下来即将调用swap函数,在调用swap函数之前,首先会将实参a,b的值传给形参x,y。然后将形参x,y压栈,数据传输过程用到了eax和ecx两个寄存器:

1
2
3
4
mov eax,dword ptr [ebp-8]
push eax
mov ecx,dword ptr [ebp-4]
push ecx

注意,由于压栈操作,esp的值也会变化:

下一条指令会调用swap函数,执行指令后,会将返回地址0x0040B7B3(即main函数中swap函数返回后下一条指令的地址)压入栈中:

在swap函数的第一行代码执行之前,会执行一系列指令为函数swap分配栈帧空间:

首先是保存旧ebp的值(以便函数返回后恢复),然后更新ebp的值为当前栈顶esp的值,也就是将当前栈顶变为新的栈底。减小esp的值使当前栈顶指向更低的地址,从而为函数swap分配出了一个栈帧空间:

1
2
3
push ebp 
mov ebp,esp
sub esp,44h

接着,保存三个寄存器的值:

1
2
3
push ebx
push esi
push edi

得到了函数swap的栈帧:

最后,通过4条指令清理函数swap栈帧的局部变量区:

函数swap首先将形参x的值传入eax,然后将eax的值保存到swap栈帧的局部变量区,完成局部变量tmp的定义:

1
2
mov eax,dword ptr [ebp+8]
mov dword ptr [ebp-4],eax

然后,通过下面4条指令交换形参x,y的值:

1
2
3
4
mov ecx,dword ptr [ebp+0Ch]
mov dword ptr [ebp+8],ecx
mov edx,dword ptr [ebp-4]
mov dword ptr [ebp+0ch],edx

最后结果如下:

所以,swap函数实际上交换的是形参x,y的值,对main函数中a,b的值并没有改动


函数返回

函数返回时,会执行如下操作:

  • 弹出为寄存器保存的值
  • 设置esp等于ebp
  • 从栈中弹出旧ebp的值,并将ebp设置为弹出的旧值
  • 弹出返回地址

函数swap执行完后,在ret前,需要恢复main函数的栈帧:

首先会弹出保存在栈中的3个寄存器的值:

1
2
3
pop edi
pop esi
pop ebx

然后将栈顶更新为栈底,弹出保存的旧ebp的值到ebp中,使栈底变为原来main函数的栈底:

1
2
mov esp,ebp
pop ebp

现在栈帧恢复成了原来main函数的栈帧:

最后一步,将原来main函数中,调用swap函数的下一条指令的地址从栈顶取出,使程序接着swap函数返回后执行:

文章目录
  1. 1. 函数的栈帧结构
  2. 2. 函数调用
  3. 3. 函数返回
|