news 2026/7/14 22:46:18

多线程环境下内存越界造成crash的完整示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多线程环境下内存越界造成crash的完整示例

多线程内存越界为何总在凌晨三点崩?一个可复现的堆破坏实验

你有没有遇到过这种场景:程序跑了好几天都正常,突然某天夜里服务挂了,日志只留下一行冰冷的Segmentation fault,而开发环境怎么也复现不了?

如果你怀疑是“玄学问题”,那很可能不是玄学——而是多线程 + 内存越界联手制造的一场完美谋杀。今天我们就来亲手导演这起“命案”:通过一段简洁却致命的代码,让系统在几秒内崩溃,并用工具精准还原犯罪现场。


从一次“偶然”的 crash 说起

设想这样一个嵌入式系统:

  • 有一个传感器采集线程,负责把数据写入缓冲区;
  • 另一个主线程处理业务逻辑,频繁申请和释放临时内存;
  • 某天传感器驱动升级后,采样率翻倍,但缓冲区大小没改;
  • 几小时后,系统莫名重启,故障点指向某个free()调用。

奇怪的是,单步调试时一切正常。为什么?

因为真正的凶手——越界写操作——早已悄然潜伏,在某个调度时机下引爆了堆管理器的校验机制。

我们接下来就构建这个典型场景。


实验设计:两个线程,一场灾难

我们要创建两个角色分明的线程:

  1. Writer 线程(杀手)
    分配一个 32 字节的小缓冲区,却往里面写 64 字节的数据 —— 明目张胆地越界。

  2. Allocator 线程(受害者)
    正常地malloc/free内存,就像大多数服务中的内存使用模式。

看似无辜的操作,为何会 crash?关键在于:堆不是一片平坦的草原,而是一张精心维护的表格。当你越界写入,很可能改写了下一个内存块的“身份证信息”(chunk header)。当另一个线程试图释放这块内存时,发现“证件照”对不上,立刻报警终止进程。


核心代码:越界就在那一行memcpy

#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define NORMAL_SIZE 32 #define OVERWRITE_SIZE 64 char* target_buffer; char pattern[OVERWRITE_SIZE]; // 杀手线程:持续越界写入 void* evil_writer(void* arg) { printf("Writer: 开始缓冲区溢出攻击...\n"); while (1) { memcpy(target_buffer, pattern, OVERWRITE_SIZE); // 危险!仅分配32字节 usleep(100); } return NULL; } // 受害者线程:正常内存操作 void* memory_allocator(void* arg) { printf("Allocator: 启动常规内存分配循环...\n"); while (1) { char* p1 = malloc(NORMAL_SIZE); char* p2 = malloc(NORMAL_SIZE); if (p1) free(p1); // ⚠️ 这里可能触发 crash if (p2) free(p2); usleep(200); } return NULL; } int main() { memset(pattern, 0xCD, sizeof(pattern)); // 填充特征值便于观察 target_buffer = malloc(NORMAL_SIZE); if (!target_buffer) { fprintf(stderr, "内存分配失败\n"); return -1; } pthread_t writer_tid, alloc_tid; pthread_create(&writer_tid, NULL, evil_writer, NULL); pthread_create(&alloc_tid, NULL, memory_allocator, NULL); pthread_join(writer_tid, NULL); pthread_join(alloc_tid, NULL); return 0; }

关键细节解析

行为风险点
malloc(32)memcpy(..., 64)向后越界,覆盖后续 chunk 的元数据
使用usleep()控制节奏增加线程交错概率,提高 crash 触发几率
Allocator 频繁调用free()主动触发堆检查,加速暴露问题

编译运行:

gcc -g -o mem_crash mem_crash.c -lpthread ./mem_crash

不出几秒,你可能会看到类似输出:

*** Error in `./mem_crash': double free or corruption (out): 0x0000000001d7b030 *** Aborted (core dumped)

或者直接Segmentation fault

📌 注意:是否 crash、何时 crash 具有随机性,取决于堆布局和调度顺序。但这正是真实世界中“偶发故障”的本质。


为什么单线程不崩,多线程就崩?

这个问题非常关键。

假设只有 Writer 线程执行越界写入,即使破坏了堆结构,只要它自己不再进行复杂的内存操作,程序可能继续“带伤运行”。

但一旦引入 Allocator 线程,情况变了:

  • 它不断触发mallocfree,这些函数内部会对堆结构做完整性校验;
  • glibc 的 ptmalloc 在free时会检查相邻 chunk 的 size 字段是否合法;
  • 若该字段已被越界写入篡改(比如变成了非法值或奇数),则立即调用abort()中止程序;
  • 信号通常是SIGABRTSIGSEGV,表现为 crash。

换句话说:越界是定时炸弹,Allocator 是那个按下启动按钮的人


如何定位真正的问题源头?

GDB 能告诉你 crash 发生在free(),但它不会告诉你谁破坏了堆。这时候就得靠现代调试利器登场了。

使用 AddressSanitizer(ASan)一击命中

重新编译并启用 ASan:

gcc -fsanitize=address -g -o mem_crash mem_crash.c -lpthread

再次运行,你会看到类似如下报告:

==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000eff0 WRITE of size 64 at 0x60200000eff0 thread T1 #0 0x4012ab in memcpy (mem_crash+0x12ab) #1 0x40115e in evil_writer mem_crash.c:35 #2 0x7f8c1a2bb6da in start_thread (libpthread.so.0+0x76da) Address 0x60200000eff0 is a wild pointer. SUMMARY: AddressSanitizer: heap-buffer-overflow

看到了吗?ASan 不仅指出这是堆缓冲区溢出,还精确定位到evil_writer函数中的memcpy调用,甚至标注出写入大小为 64 字节,远超分配的 32 字节。

这才是真正的“零容错”检测。


堆内存长什么样?越界到底破坏了什么?

为了更深入理解,我们来看典型的堆布局(以 glibc ptmalloc 为例):

+---------------------+ | Chunk 1 Header | <-- size=0x21 (32+1), prev_inuse +---------------------+ | target_buffer | 用户数据区 (32 bytes) +---------------------+ | Chunk 2 Header | <-- 被越界写入覆盖的位置! +---------------------+ | next buffer | 下一个 malloc 返回的内存 +---------------------+

每个 chunk 头部包含:
-prev_size: 前一个 chunk 的大小
-size: 当前 chunk 的大小(低三位用于标志)
-fd,bk: 空闲时指向双向链表前后节点

当我们用memcpy(target_buffer, ..., 64),前 32 字节写入合法区域,后 32 字节就会冲进下一个 chunk 的 header 区域,把原本的size字段改成垃圾数据。

当下一个free()尝试合并或校验时,读取到一个非法的size,轻则 abort,重则跳转到野指针地址,引发不可预测行为。


实际工程中的常见陷阱

这类问题绝不只是实验室玩具,现实中比比皆是:

场景风险表现
DMA 直接写用户缓冲区驱动未校验长度,导致硬件越界写
协议解析未检查包长收到恶意数据包造成栈/堆溢出
日志拼接使用sprintf格式化字符串过长导致缓冲区爆掉
多线程共用全局缓冲区缺少锁保护 + 边界检查双重缺失

尤其在 C/C++ 编写的高性能服务、嵌入式固件、操作系统模块中,这类 bug 是稳定性头号杀手。


如何避免成为下一个受害者?

✅ 编码阶段:防御性编程

  • 所有拷贝使用安全函数:
    c snprintf(buf, bufsize, "%s", str); memcpy_s(dst, dstsize, src, copylen); // C11 Annex K
  • 对外部输入严格验证长度;
  • 尽量使用固定大小数组 + 编译期断言;
  • 共享数据加锁或采用无锁队列替代裸指针传递。

✅ 构建阶段:强制开启运行时检查

# 开发/测试构建务必启用 gcc -fsanitize=address -fsanitize=undefined -g -O1

虽然性能下降约 2x,但在 CI 流水线中运行一轮,能提前拦截 90% 以上内存类错误。

✅ 部署阶段:监控与隔离

  • 关键模块使用独立内存池(memory pool),防止相互污染;
  • 记录 core dump 并集成符号服务器,实现快速回溯;
  • 使用静态分析工具(如 Coverity、PVS-Studio)扫描潜在风险。

✅ 终极方案:换语言 or 换思维

如果条件允许,考虑:
- 使用 Rust 替代高风险模块(所有权机制杜绝悬垂指针);
- C++ 中优先使用std::vector,std::string等容器;
- 引入 Bounds Checker 工具链进行灰盒测试。


最后的思考:crash 点从来不是起点

这个实验最深刻的启示是:

程序崩溃的地方,往往不是出错的地方

越界发生在 Writer 线程的第 35 行,但 crash 却出现在 Allocator 线程的free()调用。中间可能隔了几百毫秒、几千次内存操作。

如果没有 ASan 这样的工具,开发者只能靠猜、靠 log、靠运气去追踪问题根源,效率极低。

所以,请记住这句话:

“你以为你在调试内存错误,其实你在逆向时间。”


如果你也在维护一个长期运行的服务或嵌入式系统,不妨现在就做一件事:
👉 在测试环境中跑一遍 AddressSanitizer。

也许你会发现,那些“偶尔重启”的背后,正藏着一个默默写越界的“幽灵线程”。

欢迎在评论区分享你的排查经历,我们一起揭开更多“深夜崩盘”的真相。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/10 16:51:57

Open-AutoGLM GitHub地址发布在即,错过将再等一年?

第一章&#xff1a;Open-AutoGLM开源代码地址Open-AutoGLM 是一个面向自动化自然语言处理任务的开源框架&#xff0c;旨在通过可扩展的架构支持大语言模型的快速集成与调优。该项目由社区驱动&#xff0c;代码托管于主流代码托管平台&#xff0c;便于开发者协作与持续集成。项目…

作者头像 李华
网站建设 2026/7/8 7:19:31

揭秘Open-AutoGLM底层架构:5个关键点看懂其爆发潜力

第一章&#xff1a;Open-AutoGLM会和chatgpt一样火吗 Open-AutoGLM作为近期开源社区关注的焦点&#xff0c;其定位是打造一个可自主迭代、支持多任务自动推理的开源大模型框架。它融合了AutoGPT的任务分解能力与GLM系列语言模型的强大生成性能&#xff0c;试图在自动化智能代理…

作者头像 李华
网站建设 2026/7/9 2:36:13

为什么顶尖团队都在用AutoGLM沉思模式?3个真实案例告诉你答案

第一章&#xff1a;AutoGLM沉思模式的崛起背景随着大语言模型在自然语言处理领域的广泛应用&#xff0c;传统推理模式逐渐暴露出响应速度慢、逻辑连贯性不足等问题。为应对复杂任务中对深度思考与多步推理的需求&#xff0c;智谱AI推出了AutoGLM沉思模式。该模式借鉴“系统1与系…

作者头像 李华
网站建设 2026/7/1 9:23:57

springboot基于Java的神农药店中药仓库管理系统设计与实现_0q0g2sp5

目录具体实现截图项目介绍论文大纲核心代码部分展示可定制开发之亮点部门介绍结论源码获取详细视频演示 &#xff1a;文章底部获取博主联系方式&#xff01;同行可合作具体实现截图 本系统&#xff08;程序源码数据库调试部署讲解&#xff09;同时还支持Python(flask,django)、…

作者头像 李华
网站建设 2026/7/5 1:49:29

LangFlow知识产权申报材料生成器

LangFlow&#xff1a;让AI创新可见、可述、可保护 在大模型技术席卷各行各业的今天&#xff0c;一个现实问题摆在开发者面前&#xff1a;如何快速将一个模糊的智能应用构想&#xff0c;变成可演示、可验证、可申报知识产权的技术成果&#xff1f;传统方式下&#xff0c;哪怕是最…

作者头像 李华