【深入理解函数栈帧:探索函数调用的内部机制】

简介: 【深入理解函数栈帧:探索函数调用的内部机制】

本章重点


  • 了解汇编指令
  • 深刻理解函数调用过程


样例代码:


#include <stdio.h>
int MyAdd(int a, int b)
{
  int c = 0;
  c = a + b;
  return c;
}
int main()
{
  int x = 0xA;
  int y = 0xB;
  int z = MyAdd(x, y);
  printf("z = %x\n", z);
  return 0;
}


C语言地址空间学习



  • 代码段:存储程序的机器指令,包括函数的二进制代码。
  • 字符常量区:存储字符串常量和字符常量,这些值在程序中是只读的,不可修改。
  • 已初始化变量区:存储已经初始化的全局变量和静态变量。
  • 未初始化变量区:存储未初始化的全局变量和静态变量,在程序加载时会被初始化为默认值(如0)。
  • 堆:动态分配的内存区域,用于存储动态分配的变量、数据结构和对象。它的大小和位置在程序运行时动态调整。
  • 栈:存储函数调用的局部变量、函数参数、函数返回地址以及其他与函数调用相关的信息。栈是一种后进先出(LIFO)的数据结构,函数调用时会在栈上创建一个新的帧,函数返回时会将该帧从栈中弹出。


认识相关寄存器


  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器 esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址


认识相关汇编命令


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


讲解思路图


备注: vs编译器有栈随机化的处理,所以每次看到的相关数据可能会不太一致,不过我们重点关注变化原理,弱化数据。


1、起步,main函数也是要被调用的



2、main函数也是要形成栈帧结构的



3、变量x和入栈



4、临时变量的入栈拷贝



6、开始调用函数



7、MyAdd函数栈帧形成



8、变量c入栈并完成加法



9、寄存器eax保存返回值



10、释放MyAdd函数栈帧



11、ret返回



12、函数参数的临时变量被销毁, 程序已经回到main函数栈帧,并且已经将寄存器eax的值给到z变量。



总结:


  1. 调用函数,需要先形成临时拷贝,形成过程是从右向左的
  2. 临时空间的开辟,是在对应函数栈帧内部开辟的
  3. 函数调用完毕,栈帧结构被释放掉
  4. 临时变量具有临时性的本质:栈帧具有临时性
  5. 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
  6. 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的


根据第六点,我们可以发现一个现象


#include <stdio.h>
void MyAdd(int a, int b)
{
  //由于临时变量是从右往左创建的
  //所以b的地址高于a
  printf("before:%d\n", b);
  *(&a + 1) = 100;
  printf("after:%d\n", b);
}
int main()
{
  int x = 0xA;
  int y = 0xB;
  MyAdd(x, y);
  return 0;
}



我们上面的代码能很好的体现函数参数的地址位置关系,但这是一种不可预测和不可靠的行为。


临时变量的内存布局是由编译器决定的,这是未定义行为。不同的编译器可能采用不同的策略和规则来分配内存。因此,不能依赖于特定的内存布局来进行编程。


临时变量在栈上分配内存,通常是从高地址向低地址分配。因此,b的地址可能比a的地址更高。然而,这并不意味着b的地址正好位于a的下一个内存位置。



相关文章
|
C语言
数据结构之栈详解(C语言手撕)
数据结构之栈详解(C语言手撕)
700 1
|
NoSQL 安全 Redis
Bugku S3 AWD排位赛-3(带你入门awd流程)
Bugku S3 AWD排位赛-3(带你入门awd流程)
1951 1
|
存储 NoSQL Linux
《探秘程序崩溃:核心转储(Core Dump)分析全攻略》
在软件开发中,程序崩溃如同暴风雨,核心转储(Core Dump)则是这场风暴后的“事故现场记录”。它保存了程序崩溃时的内存状态和寄存器信息,为开发者提供了关键线索,帮助快速定位问题根源,节省调试时间。通过设置如Linux的`ulimit -c unlimited`或Windows的WinDbg,可生成核心转储文件,并利用GDB等工具分析调用栈和内存信息,结合源代码,揭示崩溃原因,提升软件稳定性。
1023 7
|
存储 编译器
函数栈帧深度剖析(一篇带你牢牢掌握函数栈帧)(二)
函数栈帧深度剖析(一篇带你牢牢掌握函数栈帧)(二)
660 1
|
Ubuntu Linux 数据安全/隐私保护
内核实验(七):使用内核KFIFO环形缓冲区机制
本文通过一个内核模块实验,演示了如何在Linux内核中使用KFIFO环形缓冲区机制,包括定义KFIFO、编写驱动程序以及在Qemu虚拟机中进行编译、部署和测试,展示了KFIFO在无需额外加锁的情况下如何安全地在读者和写者线程间进行数据传输。
1181 0
内核实验(七):使用内核KFIFO环形缓冲区机制
|
存储 编译器 容器
函数栈帧的创建和销毁讲解
函数栈帧的创建和销毁讲解
394 0
|
Linux Shell Docker
如何在Docker容器中使用巨页(大页)
在linux环境下常规页面大小是4K,常规巨页大小有两种一种是2MB,一种是1GB。巨页的好处是:减少硬件tlb miss,如此在连续内存访问场景下可以得到较大的性能提升。
5978 0
|
编译器 API C语言
在x86架构汇编语言中函数参数传递的三种约定
在x86架构汇编语言中函数参数传递的三种约定
945 2
|
存储 程序员 编译器
深入理解函数调用--函数栈帧
深入理解函数调用--函数栈帧

热门文章

最新文章