当前位置: 首页 > news >正文

Linux应用程序 栈溢出 内存踩踏 问题 排查学习

目录

    • 前言
    • 一、先理解问题:什么是栈溢出和内存踩踏?
      • 1. 栈溢出(Stack Overflow)
      • 2. 内存踩踏(Memory Corruption)
    • 二、新手处理流程:从简单到复杂
      • 第1步:观察现象,收集信息
      • 第2步:使用调试工具定位问题
        • 工具1:GDB(最简单的调试器)
        • 工具2:AddressSanitizer(ASan,内存检测神器)
      • 第3步:代码审查(重点检查常见错误)
      • 第4步:验证修复
      • 特殊情况
        • 栈保护(Stack Canary)选项
        • 栈溢出检查(Stack Check)选项
        • 其他相关安全选项
        • 组合使用示例
        • 反汇编检查栈保护是否生效?
        • 适用场景
      • 注意事项
    • 三、常见问题与解决方法
    • 四、学习建议
    • 五、总结

前言

  遇到程序崩溃问题时,不要慌张!尤其是像栈溢出内存踩踏这类内存相关的问题,它们虽然复杂,但通过系统化的方法可以一步步解决。以下是从学习角度出发的详细指南,适合小白阅读:


一、先理解问题:什么是栈溢出和内存踩踏?

1. 栈溢出(Stack Overflow)

  • 比喻:想象你有一个杯子(栈空间),每次调用函数就像往杯子里倒水(分配内存)。如果倒太多水(比如递归太深或局部变量太大),水就会溢出(程序崩溃)。
  • 常见原因
    • 函数递归调用层数太多。
    • 在函数内定义了过大的局部数组(如 int arr[1000000];)。

2. 内存踩踏(Memory Corruption)

  • 比喻:你的程序像一栋大楼,每个房间(内存地址)存放数据。如果程序不小心闯入了别人的房间(越界访问)或破坏了门锁(非法写内存),大楼就会出问题。
  • 常见原因
    • 数组越界访问(如 arr[10] 但数组只有5个元素)。
    • 使用已释放的内存(悬空指针)。
    • 多线程竞争导致数据被意外修改。

二、新手处理流程:从简单到复杂

第1步:观察现象,收集信息

  • 现象:程序突然崩溃、输出“Segmentation fault”错误。
  • 怎么做
    1. 查看崩溃日志

      dmesg | tail -n 20   # 在终端输入,查看内核日志的最后20行
      

      如果看到类似 segfault at 0x123456 的信息,说明发生了内存错误。

    2. 启用 Core Dump(程序崩溃时的“现场快照”):

      ulimit -c unlimited          # 允许生成 core 文件
      echo "/tmp/core.%t.%p" > /proc/sys/kernel/core_pattern  # 设置 core 文件保存路径
      

      重新运行程序,崩溃时会生成一个 core 文件(如 /tmp/core.12345)。如果默认开启,则不需要重新执行程序,崩溃时已经生成了core文件;


第2步:使用调试工具定位问题

工具1:GDB(最简单的调试器)
  • 安装 GDB(如果还没有):

    sudo apt install gdb   # Ubuntu/Debian 系统如果是嵌入式arm主控,则需要自己下载gdb源码自己去交叉编译一个,可以百度下教程,蛮多的
    再就是你买的学习开发板的话,一般这些调试工具都有编译好的提供,不清楚在哪可以找官方客服问下!
    
  • 用 GDB 分析 Core 文件

    gdb ./your_program /tmp/core.12345   # 替换为你的程序和 core 文件名
    注意:如果程序编译时没有加-g选项,则看不到多少有用的信息,建议编译一个-g版本的程序进行加载
    

    在 GDB 中输入以下命令:

    bt          # 查看崩溃时的函数调用栈(Backtrace)
    info locals # 显示崩溃时的局部变量
    x/10x $sp   # 查看栈内存内容(sp 是栈指针寄存器)
    
    • 重点看哪里
      • bt 输出的最后一行为崩溃时的代码位置(如 main.c:15)。
      • 如果看到 __stack_chk_fail,说明发生了栈溢出。
工具2:AddressSanitizer(ASan,内存检测神器)
  • 如何用
    1. 编译时添加 ASan 检测
      gcc -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 your_app your_app.c
      
      • -g:保留调试符号(能看到代码行号)。

      • -O1:适度优化,不影响检测。

      • -fsanitize=address

        • 功能:启用 AddressSanitizer (ASan),用于检测内存错误(如:
          • 使用未初始化的内存
          • 使用已释放的内存(Use-after-free)
          • 内存泄漏
          • 堆/栈/全局缓冲区溢出
        • 特点
          • 在运行时插入额外代码以检测内存访问。
          • 程序运行时性能开销较大(通常为2-3倍)。
          • 需要高版本 GCC/Clang(4.8+)支持。
      • -fsanitize=undefined

        • 功能:启用 UndefinedBehaviorSanitizer (UBSan),用于检测 未定义行为(Undefined Behavior, UB),例如:
          • 整数溢出(如 int a = 2147483647 + 1;
          • 无效的类型转换(如 int* p = reinterpret_cast<int*>(0x1);
          • 无效的 switch 表达式值
          • 除以零
        • 特点
          • 检测标准 C/C++ 中的未定义行为。
          • 运行时性能开销较小(相比 ASan)。
      • -fno-omit-frame-pointer

        • 功能保留帧指针(Frame Pointer)
        • 作用
          • 通过保留帧指针,允许调试器(如 GDB)生成更准确的调用栈信息(backtrace)。
          • 在 ASan报告错误时,能更精确地定位错误发生的位置(包括文件名和行号)。
        • 默认行为
          • 在启用优化(如 -O2)时,编译器会省略帧指针以提高性能(-fomit-frame-pointer)。使用O1 或者 O0
          • 使用 -fno-omit-frame-pointer 可强制保留帧指针。
      • 性能开销

        • ASan:程序运行速度可能降低 2-3 倍。
        • UBSan:性能影响较小,但某些检测(如整数溢出)会显著增加开销。
        • 建议:仅在调试或测试阶段使用,生产环境应禁用。
      • GCC/Clang 版本要求

        • ASan:GCC 4.8+ / Clang 3.1+
        • UBSan:GCC 4.9+ / Clang 3.4+
        • 建议:使用最新稳定版本以获得最佳兼容性和功能支持。
      • 动态链接库问题

        • ASan 和 UBSan 需要链接对应库(如 libasanlibubsan)。
        • 如果遇到 cannot find -lasan 错误,请安装开发包(如 libasan-dev)或手动链接库路径。
      • 环境变量配置

        • ASAN_OPTIONS:调整 ASan 行为(如忽略某些错误)。
          ASAN_OPTIONS=halt_on_error=0 ./program
          
        • UBSAN_OPTIONS:调整 UBSan 行为。
          UBSAN_OPTIONS=print_stack_trace=1 ./program
          
选项功能注意事项
-fsanitize=address检测内存错误(越界、泄漏、Use-after-free)需要高版本编译器,性能开销大
-fsanitize=undefined检测未定义行为(整数溢出、无效类型转换等)与 ASan 可组合使用
-fno-omit-frame-pointer保留帧指针,确保调试信息完整与优化选项冲突,需配合 -O0/-O1 使用

通过合理配置这些选项,可以显著提升代码的健壮性和安全性,尤其适合在开发和测试阶段使用。


第3步:代码审查(重点检查常见错误)

  • 针对栈溢出

    1. 检查所有局部变量的大小
      void func() {int huge_array[1000000];  // 错误!栈空间不足// 应改为动态分配:int *huge_array = malloc(1000000 * sizeof(int));
      }
      
    2. 检查递归函数
      void recursive_func() {recursive_func();  // 错误!无限递归导致栈溢出
      }
      
  • 针对内存踩踏

    1. 检查数组越界
      int arr[5];
      arr[5] = 10;  // 错误!有效索引是 0~4
      
    2. 检查指针操作
      char *str = malloc(10);
      strcpy(str, "这段字符串太长了!");  // 错误!超出分配的10字节
      

第4步:验证修复

  1. 修改代码后,重复第2步:用 ASan 或 GDB 重新运行程序,确认问题是否消失。
  2. 如果问题依旧
    • 检查是否修复了所有同类错误(如多个数组越界)。
    • 使用 printf 打印关键变量的值,观察程序行为。
    • 向他人求助或在论坛提问(附上错误日志和代码片段)。

特殊情况

  有时候会遇到这样一种情况,崩溃了但是gdb回溯栈帧被破坏了无法继续排查,只能看到一些C库调用。那么这个可能并不是越界的第一现场,而是其他地方越界或者溢出,修改了当前运行代码的数据,导致当前代码运行时出现崩溃,针对这种情况gdb就无法很好的排查了,利用asan会好些。

  但是一些开发板运行asan后,由于性能问题无法正常运行业务功能复现问题,所以主要还是基于gdb回溯分析,不过我们可以在编译时加入一些选项,在触发栈溢出时就进行崩溃,这样生成的core文件就是第一现场。

  GCC 编译器提供了多种 栈保护(Stack Protection) 选项,用于检测和防御栈溢出攻击。这些选项通过插入 Canary 值(栈哨兵)来检测栈是否被非法覆盖,从而防止攻击者利用缓冲区溢出篡改返回地址或执行恶意代码。以下是主要的编译选项及其作用:


栈保护(Stack Canary)选项

-fstack-protector

  • 作用:为 包含字符数组(char array)的函数 插入栈保护代码。
  • 特点
    • 仅对存在局部缓冲区的函数启用保护。
    • 性能开销较小。
  • 示例
    gcc -fstack-protector -o my_program my_program.c
    

-fstack-protector-strong

  • 作用:提供更严格的保护,覆盖 更多类型的函数(如包含指针或大结构体的函数)。
  • 特点
    • -fstack-protector 保护范围更广。
    • 性能开销略高。
  • 示例
    gcc -fstack-protector-strong -o my_program my_program.c
    

-fstack-protector-all

  • 作用:为 所有函数 插入栈保护代码。
  • 特点
    • 保护最全面,但会显著增加程序体积和运行时开销。
    • 适合对安全性要求极高的场景。
  • 示例
    gcc -fstack-protector-all -o my_program my_program.c
    

-fno-stack-protector

  • 作用:禁用栈保护。
  • 特点
    • 默认未启用(需显式关闭)。
  • 示例
    gcc -fno-stack-protector -o my_program my_program.c
    

栈溢出检查(Stack Check)选项

-fstack-check

  • 作用:插入代码检查栈空间是否不足(例如递归调用导致栈溢出)。
  • 特点
    • 适用于嵌入式系统或资源受限环境。
    • 通过定期检查栈指针是否接近栈边界来防止溢出。
  • 示例
    gcc -fstack-check -o my_program my_program.c
    

其他相关安全选项

-D_FORTIFY_SOURCE=2

  • 作用:启用 缓冲区溢出检查,替换不安全函数(如 strcpymemcpy)为更安全的版本。
  • 特点
    • 需与优化级别 -O1 或更高一起使用。
    • 在编译时检查缓冲区大小。
  • 示例
    gcc -D_FORTIFY_SOURCE=2 -O2 -o my_program my_program.c
    

-z noexecstack / -z execstack

  • 作用
    • -z noexecstack:将栈标记为不可执行(NX 保护),防止在栈上执行恶意代码。
    • -z execstack:禁用 NX 保护(通常不推荐)。
  • 示例
    gcc -z noexecstack -o my_program my_program.c
    

-pie / -fpie

  • 作用:启用 位置无关可执行文件(PIE),结合 ASLR(地址空间随机化)防止攻击者预测内存地址。
  • 示例
    gcc -pie -fpie -o my_program my_program.c
    

组合使用示例

为了最大化安全性,可以组合使用多个选项:

gcc -fstack-protector-strong \-D_FORTIFY_SOURCE=2 \-O2 \-z noexecstack \-pie -fpie \-o my_program my_program.c
  • 说明
    • -fstack-protector-strong:启用强栈保护。
    • -D_FORTIFY_SOURCE=2:启用缓冲区溢出检查。
    • -z noexecstack:禁用栈执行。
    • -pie -fpie:启用地址随机化。

反汇编检查栈保护是否生效?

使用 objdump 查看生成的二进制文件中是否插入了 Canary 检查代码:

objdump -d my_program | grep -A 10 "__stack_chk_fail"

如果看到 __stack_chk_fail 调用,则表示栈保护已启用。

适用场景
  • 嵌入式系统(ARM64)-fstack-protector-fstack-check 可有效防止栈溢出。
  • 高安全性要求-fstack-protector-all-D_FORTIFY_SOURCE=2 适合关键安全模块。
  • 调试阶段:结合 AddressSanitizer-fsanitize=address)实时检测栈/堆溢出。

注意事项

  1. 性能开销

    • 栈保护会增加每个函数的额外检查代码,可能导致性能下降(尤其是 -fstack-protector-all)。
    • 在 ARM64 等嵌入式设备上需权衡安全性与资源消耗。
  2. 兼容性

    • -fstack-protector 在 GCC 4.9+ 和 Clang 3.1+ 中支持。
    • -D_FORTIFY_SOURCE 需要 glibc 2.16+。
  3. 调试工具配合

    • 使用 gdbValgrind 结合编译选项,可以更精确地定位问题。

三、常见问题与解决方法

问题现象可能原因解决方法
程序崩溃,日志显示 segfault数组越界、使用空指针用 ASan 检测,检查所有数组和指针操作
递归函数导致崩溃递归层数太深或无限递归改为循环,或增大栈大小(ulimit -s 65536
多线程程序数据混乱未加锁导致数据竞争使用互斥锁(pthread_mutex
内存占用持续增长(内存泄漏)未释放 malloc 的内存用 Valgrind 检查泄漏,确保每个 malloc 都有 free

四、学习建议

  1. 从小程序开始练习
    • 故意写一个有内存错误的程序(如数组越界),用 ASan 和 GDB 观察现象。
#include <stdio.h>
#include <string.h>// 存在栈溢出漏洞的函数
void vulnerable_function()
{char buffer[8]; // 只分配 8 字节的栈空间printf("buffer地址: %p\n", buffer); // 打印缓冲区地址(用于调试)// 故意使用不安全的函数读取输入(无长度检查)gets(buffer); // 输入超过8字节将导致栈溢出printf("输入内容: %s\n", buffer);
}int main()
{printf("=== 栈溢出测试 Demo ===\n");printf("尝试输入超过 8 个字符(例如输入16个A):\n");vulnerable_function();printf("=== 程序正常退出 ===\n");return 0;
}
  1. 阅读文档
    • ASan 官方文档
    • GDB 入门教程
  2. 不要怕报错
    • 每一个错误都是学习机会!遇到问题先尝试理解日志,再动手修复。
    • 遇到看不懂的报错,可以全部的报错打印发给AI大模型进行分析,让自己去看懂理解报错的含义;

五、总结

  • 核心工具:ASan(快速定位内存问题)、GDB(分析崩溃现场)。
  • 关键习惯:小步调试、多打印日志、代码逐行审查。
  • 心态调整:内存错误是初学者的常见挑战,耐心和系统性排查一定能解决!
http://www.xdnf.cn/news/643969.html

相关文章:

  • 第九课 影像文章插图及图表制作完全指南:从原理到应用
  • 市场需求文档撰写
  • C++11(2):
  • 《算法导论(第4版)》阅读笔记:p1178-p1212
  • 吴恩达机器学习笔记:逻辑回归3
  • Python元类(Metaclass)深度解析
  • Volatile的相关内容
  • Lombok与Jackson实现高效JSON序列化与反序列化
  • Python类与对象:面向对象编程的基础
  • Kubernetes 核心原理详解
  • Python实现基于线性回归的空气质量预测系统并达到目标指标
  • 内存管理 : 02 内存分区与分页
  • Python实例题:Python打造漏洞扫描器
  • 【AI论文】KRIS-基准测试:评估下一代智能图像编辑模型的基准
  • LangChain4j HelloWorld
  • 分词算法BPE详解和CLIP的应用
  • 测试计划与用例撰写指南
  • SAP Commerce(Hybris)开发实战(二):登陆生成token问题
  • 企业级智能体 —— 企业 AI 发展的下一个风口?
  • 【公式】批量添加MathType公式编号
  • [Linux]磁盘分区及swap交换空间
  • 第38节:PyTorch模型训练流程详解
  • Baklib知识中台构建实战
  • [DS]使用 Python 库中自带的数据集来实现上述 50 个数据分析和数据可视化程序的示例代码
  • 【LangChain全栈开发指南】从LLM应用到企业级AI助手构建
  • LLM多平台统一调用系统-LiteLLM概述
  • MYSQL备份恢复知识:第五章:备份原理
  • 渗透测试流程-下篇
  • 定时任务调度平台XXL-JOB
  • 基于Python实现JSON点云数据的3D可视化与过滤