5.2 函数栈帧

本文详细介绍了C语言中函数栈帧的概念、创建和销毁过程,包括栈空间的分配、寄存器的作用以及汇编指令的应用。通过实例代码和反汇编分析,展示了main函数和Add函数的栈帧操作,阐述了参数传递、局部变量存储以及函数返回的过程。
摘要由CSDN通过智能技术生成

C语言(5.2 函数栈帧)

一、函数栈帧

1. 函数栈帧概念

函数栈帧(stack frame):函数调用过程中在程序的调用栈所开辟的空间

  • 函数栈帧用来存放:
    1. 函数参数和返回值
    2. 临时变量(包括函数的非静态局部变量,和编译器自动产生的其他临时变量)
    3. 保存上下文信息(包括函数调用前后需要保持不变的寄存器)

2. 栈stack

(stack):是一种特殊的容器,用户可以将数据压入栈中,也可以将已经压入栈中的数据弹出。栈的特点是先进后出,后进先出(详情请阅读数据结构,这里不做赘述)

​ 计算机中的栈空间:由栈底到栈顶存储与计算机的高地址到低地址,因为在实际开发中计算机不能确定程序对堆内存使用多还是对栈内存使用多,所以不能将二者以一条分界线隔开从低地址向高地址使用,容易导致空间利用率降低。将栈从高地址向低地址增长增加了空间的利用率,更方便管理。

  • 栈空间使用栈底寄存器和栈顶寄存器记录栈底指针和栈顶指针来维护栈空间
  • 局部变量存储在栈中,函数调用结束,,栈空间被销毁,局部变量被释放

3. 相关寄存器和汇编指令

相关寄存器

  • eax:通用寄存器,保存临时数据,常用于返回值
  • ebx:通用寄存器,保存临时数据
  • ebp:栈底寄存器,用来存放栈底指针(Extended Base Pointer)
  • esp:栈顶寄存器,用来存放栈顶指针(Extended Stack Pointer)
  • eip:指令寄存器,保存当前指令的下一条指令的地址

相关汇编指令

  • mov:数据转移指令
  • push:数据入栈,使esp栈顶寄存器也发生改变
  • pop:数据弹出至指定位置,同时esp栈顶寄存器也发生改变
  • sub:减法指令
  • add:加法指令
  • call:函数调用,1.压入返回地址,2.转入目标函数
  • jump:修改eip,转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip指令

二、函数栈帧的创建和销毁

1. 函数调用堆栈过程

1.1 VS2019函数调用堆栈

​ 在VS2019编译器中,main函数不是直接调用的,而是由编译器的函数进行调用

​ 编译器的函数tmainCRTStartup调用编译器中的函数__tmainCRTStartup,再调用main函数,程序正式启动

1.2 程序代码

​ 我们用以下代码进行函数栈帧创建与销毁的的分析,由main函数调用Add函数

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 3;
	int b = 5;
	int ret = Add(3, 5);
	printf("%d", ret);
	return 0;
}

1.3 反汇编分析

​ 我们进入调试后,对程序进行反汇编,对这些汇编语言进行分析

  1. main函数
int main()
{
//函数栈帧的创建
    //开辟main函数空间
009B1810  push        ebp  					//将当前函数地址(__tmainCRTStartup函数)压入栈中
009B1811  mov         ebp,esp  				//栈底指针=栈顶指针
009B1813  sub         esp,0E4h  			//栈顶指针-0E4h(栈顶指针上移0E4h个字节,开辟了0E4h个空间)

    //每个函数开辟空间后会压入3个寄存器内容,作用不用深究
009B1819  push        ebx 
009B181A  push        esi 
009B181B  push        edi 
 
    //初始化main函数空间
009B181C  lea         edi,[ebp-24h]  		 //将main函数空间大小给 edi
009B181F  mov         ecx,9  				//为ecx赋值为9,ecx用来存储初始化次数
009B1824  mov         eax,0CCCCCCCCh  		 //将0CCCCCCCCh赋值给eax寄存器,是初始化的内容
009B1829  rep stos    dword ptr es:[edi] 	 //进行初始化,将eax内容分ecx次赋值给edi大小的内存
    
//main中的代码
    //创建2个变量a、b
	int a = 3;
009B182B  mov         dword ptr [a],3  
	int b = 5;
009B1832  mov         dword ptr [b],5  

    //调用函数Add
	int ret = Add(3, 5);
009B1839  push        5  							//将参数5压入栈中
009B183B  push        3  							//将参数3压入栈中
009B183D  call        _Add (09B10B4h)  				//下一条指令为_Add函数的地址(调用Add函数)
    
    //从此处暂停阅读,移步至Add函数的反汇编过程
    
009B1842  add         esp,8  						//栈顶指针+8(弹出参数的大小)
009B1845  mov         dword ptr [ret],eax  			//创建ret变量并得到eax寄存器中的值

    //打印ret的值
	printf("%d", ret);
009B1848  mov         eax,dword ptr [ret]  
009B184B  push        eax  
009B184C  push        offset string "%d" (09B7B30h)  
009B1851  call        _printf (09B10D2h)  
009B1856  add         esp,8  
	return 0;
009B1859  xor         eax,eax  
}
//销毁main函数空间
	//弹出3个指针
009B185B  pop         edi  
009B185C  pop         esi  
009B185D  pop         ebx  
    //释放main函数空间
009B185E  add         esp,0E4h  	//栈顶指针+0E4h(释放main空间)
009B1864  cmp         ebp,esp  		//比较栈顶指针和栈底指针
009B1866  call        __RTC_CheckEsp (09B1244h)  //执行__RTC_CheckEsp函数
009B186B  mov         esp,ebp  		//栈顶指针=栈底指针
009B186D  pop         ebp  			//释放栈底指针
009B186E  ret  					   //恢复返回地址,继续执行调用之前的指令,程序运行结束
  1. Add函数
int Add(int x, int y)
{
//开辟Add函数空间
009B1760  push        ebp  		//将当前函数(main函数)栈底压栈
009B1761  mov         ebp,esp  	//栈顶指针=栈底指针
009B1763  sub         esp,0C0h  //开辟0C0h个字节的空间
    //压入3个寄存器内容
009B1769  push        ebx 
009B176A  push        esi 
009B176B  push        edi 
    //计算x+y结果,并存入eax寄存器中
	return x + y;
009B176C  mov         eax,dword ptr [x]  
009B176F  add         eax,dword ptr [y]  
}

//销毁Add函数空间
	//弹出3个指针
009B1772  pop         edi 
009B1773  pop         esi 
009B1774  pop         ebx 
    //销毁函数空间
009B1775  mov         esp,ebp  	 //栈顶指针 = 栈底指针(销毁Add函数空间)
009B1777  pop         ebp  		 //弹出栈底指针
009B1778  ret  					//恢复返回地址,继续执行调用之前的指令

2. 函数调用栈图

2.1 main函数栈空间示意图

2.2 Add函数栈空间示意图

在这里插入图片描述

3. 创建销毁过程总结

3.1 调前准备

  1. 复制参数并压入栈中(形式参数)
  2. 压入当前指令的下一条指令地址call(为了被调函数执行完时接着执行中断的下一条指令)
  3. 压入调用函数的栈底指针last-ebp(记录当前函数的栈底指针,被调函数执行完时能回到当前函数继续执行)

3.2 创建空间

  1. 开辟函数的空间
  2. 分别压入3个寄存器的内容(ebx、esi、edi)
  3. 初始化内存空间(只有main函数初始化,VS2019初始化为0xCCCCCCCCh,就是经常越界访问的”烫“)

3.3 执行函数

  1. 在栈空间内可以创建局部变量

  2. 可以局部变量和参数,以ebp栈底地址为界限,进行偏移量计算访问局部变量和参数

    1. 访问局部变量:ebp-n(栈底地址相对于局部变量地址高)
    2. 访问参数:ebp+n(栈底地址相对于局部变量地址低)
  3. return语句执行时,返回值存储在eax通用寄存器中

3.4 销毁空间

  1. 弹出3个寄存器的内容(ebx、esi、edi)
  2. 释放函数空间(esp = ebp
  3. 弹出栈底指针,并赋值给ebp栈底寄存器(上一个函数正式接手)
  4. 弹出call指令地址,接着执行
  5. 弹出参数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值