【C语言】 第三课 函数与栈帧机制详解

1 函数的基本概念

在C语言中,函数是程序的基本执行单元。一个函数的定义包括返回类型、函数名、参数列表和函数体。例如:

int add(int x, int y) { // 函数定义int z = x + y;return z;
}

在使用函数前,通常需要声明( declaration)其类型,让编译器知道函数的存在和格式:

int add(int, int); // 函数声明

调用(Call)函数时,需要传递参数(Argument)。C语言默认采用传值调用(Pass by Value),即函数内部操作的是实参值的一份临时拷贝(形参),修改形参不会影响原始实参。

2 栈与栈帧的核心概念

2.1 什么是栈 (Stack)

栈是程序运行时内存中的一块特殊区域,遵循 “后进先出” (LIFO, Last In First Out) 的原则。它主要用于支持函数调用。栈的生长方向是从高地址向低地址扩展。

2.2 什么是栈帧 (Stack Frame)

每次函数调用,操作系统都会在栈上为其分配一块独立的连续内存区域,称为栈帧 (Stack Frame) 或活动记录 (Activation Record)。函数执行结束后,其对应的栈帧被销毁。

栈帧的核心作用包括:

  • 存储函数的参数
  • 保存返回地址:函数执行完后需要回到调用者的位置
  • 存储函数的局部变量
  • 保存调用函数的栈帧基址:以便被调函数返回后能恢复调用函数的栈帧
  • 提供临时数据的存储空间:如表达式计算的中间结果

2.3 寄存器的作用

在x86架构下,有两个关键寄存器用于管理栈帧:

  • ESP (Extended Stack Pointer):栈指针寄存器,始终指向当前栈帧的顶部
  • EBP (Extended Base Pointer):基指针寄存器,指向当前函数栈帧的底部。通过EBP,可以稳定地访问栈帧内的参数和局部变量(因为ESP在函数执行过程中会随着push/pop操作而变化)。

3 函数调用过程中栈帧的详细布局与变化

我们以下面的简单代码为例,深入剖析栈帧的创建和销毁过程:

#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 c = 0;c = Add(a, b);printf("%d\n", c);return 0;
}

3.1 main 函数栈帧的创建

main 函数被调用之初(其内部代码执行前),会先建立自己的栈帧:

push   ebp        ; 将调用main函数的函数(如invoke_main)的ebp压栈保存
mov    ebp, esp   ; 将当前esp的值赋给ebp,此刻ebp成为main函数栈帧的新基址
sub    esp, 0E4h  ; 为main函数的局部变量、临时数据等预留空间(0E4h字节)

此时栈空间的布局大致如下(地址从高到低增长):

地址内容说明
ebp + 4
ebp旧的ebp值调用者的栈基址
ebp - 4可能保存的寄存器(如edi)
ebp - 8int a (值为10)main的局部变量
ebp - 14hint b (值为20)main的局部变量
ebp - 20hint c (值为0)main的局部变量
… (可能包含对齐填充)
esp栈顶

3.2 调用 Add 函数:参数传递与栈帧变化

执行 c = Add(a, b); 时,会发生以下步骤:

  1. 参数从右向左压栈

    mov    eax, dword ptr [ebp-14h] ; 将变量b的值(20)存入eax
    push   eax                       ; 将参数y (b的值) 压栈
    mov    ecx, dword ptr [ebp-8]    ; 将变量a的值(10)存入ecx
    push   ecx                       ; 将参数x (a的值) 压栈
    

    此时栈顶附近增加了两个新的值(yx),ESP 随之减小。

  2. 执行 call 指令

    call   00D110B9          ; 调用Add函数(地址由编译器决定)
    

    call 指令做了两件事:

    • 返回地址(即call指令下一条指令的地址,例如00D11917)压入栈。
    • 跳转到 Add 函数的地址开始执行。
      此时,在参数之上又压入了返回地址,ESP 再次更新。

    此时栈的布局变为:

    地址内容说明

| … | … | … |
| ebp + 4 | … | |
| ebp | 旧的ebp值 | 调用者的栈基址 |
| … | … | |
| ebp - 20h | c (0) | |
| … | … | |
| ← ESP指向这里 | 返回地址 | 00D11917 |
| | 参数 x (10) | a的拷贝 |
| | 参数 y (20) | b的拷贝 |

3.3 Add 函数栈帧的创建与执行

进入 Add 函数后,它会立即建立自己的栈帧:

push   ebp        ; 将main函数的ebp压栈保存
mov    ebp, esp   ; 将当前esp设为Add函数栈帧的基址(ebp)
sub    esp, 0CCh  ; 为Add的局部变量等开辟空间

现在栈的布局是:

地址内容说明
ebp + 8参数 x (10)通过[ebp+8]访问
ebp + 0Ch参数 y (20)通过[ebp+0Ch]访问
ebp + 4返回地址00D11917
ebpmain的ebp保存的main函数基址
ebp - 4可能保存的寄存器
ebp - 8int z (0)Add的局部变量
ESPAdd栈帧顶部

接着执行 Add 函数体:

mov    dword ptr [ebp-8], 0    ; int z = 0;
mov    eax, dword ptr [ebp+8]  ; 将参数x的值(10)存入eax
add    eax, dword ptr [ebp+0Ch] ; 加上参数y的值(20),结果在eax
mov    dword ptr [ebp-8], eax  ; z = x + y; (结果30存入z)
mov    eax, dword ptr [ebp-8]  ; 将z的值(30)存入eax,作为返回值

返回值通常存放在EAX寄存器中

3.4 Add 函数栈帧的销毁与返回

函数返回前,需要销毁其栈帧:

mov    esp, ebp   ; 将esp移回ebp处,释放Add函数的所有局部变量空间
pop    ebp        ; 弹出栈顶值到ebp,此时栈顶值正是之前保存的main函数的ebp,从而恢复main函数的栈帧基址
ret               ; 从栈顶弹出返回地址(00D11917)并跳转回去

ret 指令执行后,ESP 会指向之前压入的参数 xy 所在的位置。回到 main 函数后,它会清理栈上的参数:

add    esp, 8     ; 将栈顶指针esp向上移动8字节,清理掉两个4字节的参数
mov    dword ptr [ebp-20h], eax ; 将eax中的返回值(30)存储到变量c中

4 使用GDB调试器观察栈帧

理论结合实践是关键。你可以使用GDB(GNU Debugger)来动态跟踪函数调用过程,直观地观察栈帧、寄存器和内存的变化。

4.1 准备工作

  1. 编译带调试信息的程序:使用 gcc-g 选项编译你的C程序,以便GDB能显示源代码和调试信息。
    gcc -g -o my_program my_program.c
    
  2. 启动GDB
    gdb my_program
    

4.2 基本调试命令与观察点

在GDB中,你可以使用以下命令来观察栈帧(以分析上面的 Add 函数调用为例):

命令说明
break mainb mainmain 函数入口处设置断点
break addb addadd 函数入口处设置断点
runr开始运行程序,直到遇到第一个断点
nextini执行一条汇编指令(Step instruction)
stepisi执行一条汇编指令,如果是call指令则会进入函数(Step into instruction)
info registers显示所有寄存器的当前值(重点关注 eax, ebp, esp, eip
print $ebp打印ebp寄存器的值
print $esp打印esp寄存器的值
x/20xw $esp以十六进制字(4字节)格式检查栈指针($esp)附近20个字的内存内容
disassembledisas反汇编当前函数的汇编代码
backtracebt显示当前的函数调用栈回溯(call stack)
continuec继续运行程序,直到下一个断点或程序结束

4.3 实践观察步骤

  1. main 函数开始和 Add 函数入口处设置断点:(gdb) b main, (gdb) b add
  2. 运行程序:(gdb) run
  3. 程序会在 main 开始处停下。使用 (gdb) disas 查看 main 函数的反汇编代码。
  4. 单步执行(ni)直到 call add 指令之前。
  5. 记录下此时 ESPEBP 的值:(gdb) info registers esp ebp
  6. 查看此时栈的内容(例如,ESP指向的位置):(gdb) x/10xw $esp
  7. 执行 call 指令(ni)。观察 ESP 的变化(减少了4字节,因为压入了返回地址),并再次查看栈顶内容,应该能看到返回地址。
  8. 现在进入 Add 函数。再次查看反汇编 (gdb) disas
  9. 单步执行 Add 函数开头的 push ebpmov ebp, espsub esp, ... 等指令。每执行一条,观察 ESP 和 EBP 寄存器值的变化,并结合 x/xw $espx/xw $ebp 查看栈内存内容。
  10. 重点关注 mov eax, dword ptr [ebp+8] 这样的指令,它正是在通过 EBP + 偏移 来访问参数。你可以用 (gdb) print *(int*)($ebp+8) 来验证是否拿到了参数x的值。
  11. 当执行到 ret 指令前,观察 EAX 寄存器的值,它应该保存着返回值。
  12. 执行 ret 后,注意程序流是否跳回了 main 函数,以及 ESP 如何变化(指向参数所在位置)。
  13. 回到 main 后,执行 add esp, 8,再次观察 ESP 的变化,确认参数被清理。

通过以上步骤,你可以非常直观地看到栈帧的创建、使用和销毁全过程,以及参数和返回值是如何传递的。

5 C代码与汇编代码对比

理解汇编代码是逆向工程的基石。下面是一个简单的对比,展示了C代码和它可能对应的x86汇编代码(使用GCC风格):

C代码汇编代码 (x86)说明
int a = 10;mov DWORD PTR [ebp-0x4], 0xa将立即数10(0xa)存入[ebp-4]的地址(局部变量a)
int b = 20;mov DWORD PTR [ebp-0x8], 0x14将立即数20(0x14)存入[ebp-8]的地址(局部变量b)
c = add(a, b);mov eax, DWORD PTR [ebp-0x8]``push eax``mov eax, DWORD PTR [ebp-0x4]``push eax``call <add>``add esp, 0x8``mov DWORD PTR [ebp-0xc], eax参数从右向左(b then a)压栈,调用函数,清理栈空间,保存返回值到c
int z = x + y; (在add函数内)mov eax, DWORD PTR [ebp+0x8]``add eax, DWORD PTR [ebp+0xc]``mov DWORD PTR [ebp-0x4], eax通过EBP+偏移访问参数(x在[ebp+8], y在[ebp+12]),结果存入局部变量z([ebp-4])
return z;mov eax, DWORD PTR [ebp-0x4]将返回值放入eax寄存器

这种对比能帮助你建立高级语言与低级机器指令之间的直观联系,是逆向分析中不可或缺的能力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/97961.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/97961.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多个大体积PDF文件怎么按数量批量拆分成多个单独文件

在现代社会中&#xff0c;电子文档在我们的身边无所不在&#xff0c;而PDF文件时我们日常接触非常多的文档类型之一。PDF由于格式稳定、兼容性好&#xff0c;因此经常被用于各行各业。但是&#xff0c;我们平时在制作或搜集PDF文件时&#xff0c;文件太大&#xff0c;传输和分享…

ansible-角色

角色 一、利用角色构造ansible playbook 随着开发更多的playbook&#xff0c;会发现有很多机会重复利用以前编写的playbook中的代码。或许&#xff0c;一个用于为某一应用配置MySQL数据库的play可以改变用途。通过利用不同的主机名、密码和用户来为另一个应用配置MySQL数据库。…

git命令行打patch

在 Git 里打 patch&#xff08;补丁&#xff09;其实就是把某些提交的改动导出来&#xff0c;生成一个 .patch 文件&#xff0c;方便别人用 git apply 或 git am 打进代码里。&#x1f539; 常用方式1. 基于提交导出 patch导出最近一次提交&#xff1a;git format-patch -1 HEA…

文华财经多空提示指标公式 变色K线多空明确指标 文华wh6赢顺多空买卖提示指标

XX:240C;YY:MA(C,1);A1:POW(XX,2)/360-POW(YY,2)/260;A5:EMA2(EMA2(A1,20),5),LINETHICK2;A6:A5*0.9999,COLORSTICK;A20:EMA2(EMA2(A5,20),5),LINETHICK2;A60:EMA2(EMA2(A20,20),5),LINETHICK2;支撑:HHV(A5,30),COLORRED;天数:BARSSINCE(A5HHV(A5,0));YL:REF(A5,1)2.79-天数*0.…

记录一个防重Toast

当我们已经对某个按钮做了防暴力点击&#xff0c;但是依然在业务上有些复杂交互的情况&#xff0c;需要我们封装一个防重Toast。针对这类情况&#xff0c;可以直接使用下面的showDebouncedToastdata class ToastInfo(val id: Any? null,val command: MediaCommandDebouncer.M…

在线测评系统---第n天

主要完成了退出登录前后的代码的实现&#xff0c;以及题目列表的查询1.退出登录前端引入了全局前置守卫&#xff0c;如果cookie里面没有token则直接跳转到login页面&#xff1b;有则直接跳转到layout页面&#xff0c;无需重新登录后端接收到退出登录&#xff0c;将token置为无效…

机器学习从入门到精通 - 卷积神经网络(CNN)实战:图像识别模型搭建指南

机器学习从入门到精通 - 卷积神经网络(CNN)实战&#xff1a;图像识别模型搭建指南 各位&#xff0c;是不是觉得那些能认出照片里是猫还是狗、是停车标志还是绿灯的AI酷毙了&#xff1f;今天咱们就撸起袖子&#xff0c;亲手搭建一个这样的图像识别模型&#xff01;别担心不需要你…

python sqlalchemy模型的建立

SQLAlchemy 是一个功能强大的 Python SQL 工具包和对象关系映射&#xff08;ORM&#xff09;库&#xff0c;用于管理和操作关系数据库。它为 Python 开发者提供了一种用 Python 对象来运行和管理 SQL 数据库的方式。 目录 SQLAlchemy 的两个核心组成部分 SQLAlchemy 的主要功…

Rust中使用RocksDB索引进行高效范围查询的实践指南

在当今海量数据处理场景下,高效的范围查询能力成为许多系统的关键需求。RocksDB作为一款高性能的嵌入式键值存储引擎,其独特的LSM树结构和索引设计为范围查询提供了底层支持。本文将深入探讨如何在Rust中利用RocksDB的特性来实现高效范围查询,从键的设计原则到迭代器的工程实…

怎么做到这一点:让 Agent 可以像人类一样 边听边想、边说,而不是“等一句话 → 一次性返回”

要实现“边听边想、边说”&#xff0c;核心是把整条链路做成全双工、分片流式、可中断的流水线&#xff1a; ASR 连续吐字 →&#xff08;短缓冲&#xff09;→ LLM 连续出 token&#xff08;可抢断&#xff09;→ TTS 连续合成并播放&#xff08;可打断/续播&#xff09;。 下…

Ubuntu 22.04 网络服务安装配置

Ubuntu 22.04 网络服务安装配置 一键安装所有服务 # 更新系统 sudo apt update# 安装所有服务 sudo apt install -y openssh-server vsftpd telnetd inetutils-inetd ftp telnet# 启动所有服务 sudo systemctl start ssh vsftpd inetutils-inetd sudo systemctl enable ssh vsf…

【Unity知识分享】Unity实现全局监听键鼠调用

1、实现该功能前&#xff0c;优先学习Unity接入dll调用Window系统接口教程 【Unity知识分享】Unity接入dll调用Window系统接口 2、初始化动态连接库后&#xff0c;进行脚本功能实现 2.1 创建脚本KeyBoardHook.h和KeyBoardHook.cpp&#xff0c;实现功能如下 KeyBoardHook.h …

深度学习篇---MNIST:手写数字数据集

下面我将详细介绍使用 PyTorch 处理 MNIST 手写数字数据集的完整流程&#xff0c;包括数据加载、模型定义、训练和评估&#xff0c;并解释每一行代码的含义和注意事项。整个流程可以分为五个主要步骤&#xff1a;准备工作、数据加载与预处理、模型定义、模型训练和模型评估。# …

k8s集群搭建(二)-------- 集群搭建

安装 containerd 需要在集群内的每个节点上都安装容器运行时&#xff08;containerd runtime&#xff09;&#xff0c;这个软件是负责运行容器的软件。 1. 启动 ipv4 数据包转发 # 设置所需的 sysctl 参数&#xff0c;参数在重新启动后保持不变 cat <<EOF | sudo tee …

【Docker】P1 前言:容器化技术发展之路

目录容器发展之路物理服务器时代&#xff1a;一机一应用的局限虚拟化时代&#xff1a;突破与局限并存容器化时代&#xff1a;轻量级的革新技术演进的价值体现各位&#xff0c;欢迎来到容器化时代。 容器发展之路 现代业务的核心是应用程序&#xff08;Application&#xff09;…

WPF依赖属性和依赖属性的包装器:

依赖属性是WPF&#xff08;Windows Presentation Foundation&#xff09;中的一种特殊类型的属性&#xff0c;特别适用于内存使用优化和属性值继承。依赖属性的定义包括以下几个步骤&#xff1a; 使用 DependencyProperty.Register 方法注册依赖属性。 该方法需要四个参数&…

图生图算法

图生图算法研究细分&#xff1a;技术演进、应用与争议 1. 基于GAN的传统图生图方法 定义&#xff1a;利用生成对抗网络&#xff08;GAN&#xff09;将输入图像转换为目标域图像&#xff08;如语义图→照片、草图→彩图&#xff09;。关键发展与趋势&#xff1a; Pix2Pix&#…

Go 自建库的使用教程与测试

附加一个Go库的实现&#xff0c;相较于Python&#xff0c;Go的实现更较为日常&#xff0c;不需要额外增加setup.py类的文件去额外定义,计算和并发的性能更加。 1. 创建 Go 模块项目结构 首先创建完整的项目结构&#xff1a; gomathlib/ ├── go.mod ├── go.sum ├── cor…

What is a prototype network in few-shot learning?

A prototype network is a method used in few-shot learning to classify new data points when only a small number of labeled examples (the “shots”) are available per class. It works by creating a representative “prototype” for each class, which is typical…

Linux中用于线程/进程同步的核心函数——`sem_wait`函数

<摘要> sem_wait 是 POSIX 信号量操作函数&#xff0c;用于对信号量执行 P 操作&#xff08;等待、获取&#xff09;。它的核心功能是原子地将信号量的值减 1。如果信号量的值大于 0&#xff0c;则减 1 并立即返回&#xff1b;如果信号量的值为 0&#xff0c;则调用线程&…