当前位置:首页>java>一个诡异的"删除代码导致程序崩溃"问题排查

一个诡异的"删除代码导致程序崩溃"问题排查

  • 2026-08-23 03:18:50
一个诡异的"删除代码导致程序崩溃"问题排查

项目环境: STM32G4 + FreeRTOS + 多传感器数据采集

关键词: 数组越界、内存布局、静态变量、.bss段

问题现象

在一个 STM32G4 + FreeRTOS 的嵌入式项目中,遇到了一个令人困惑的问题:

删除一段看似无关的数据处理代码后,串口通信功能完全失效。

被删除的代码如下:

// 在 sample_task.c 中static int16_t tmp_xyz[3*MAX_PKTS];  // 360字节的静态数组for (uint16_t i=0; i<n; i++) {tmp_xyz[3*i+0] =acc_pkts[i].x;tmp_xyz[3*i+1] =acc_pkts[i].y;tmp_xyz[3*i+2] =acc_pkts[i].z;}

这段代码只是把加速度计数据拷贝到一个临时数组,与串口通信毫无关系。但删除后,RS485 通信完全无响应;恢复代码后,通信立刻正常。


错误的排查方向

最初我们怀疑是 FreeRTOS 时序问题——删除代码后循环执行变快,影响了任务调度。

但这个解释站不住脚:

  • FreeRTOS 的调度机制是成熟可靠的

  • 一个任务执行快一点不应该导致另一个任务完全失效

  • 如果真是时序问题,应该是偶发而非必现


关键实验

为了定位问题,我们设计了三个实验:

实验操作结果
实验1用空循环替代数据处理❌ 不工作
实验2只保留静态数组声明,删除循环✅ 工作
实验3检查栈使用和内存布局发现真相

实验2的结果是关键线索:问题与代码执行无关,只与静态数组的存在有关。

这说明:删除数组改变了内存布局,触发了某个隐藏的内存越界 bug。


真相大白

仔细审查代码后,发现了罪魁祸首:

// sample_task.cstatic struct sensor_datag buf[60u];  // 数组大小:60// ...volatile int ng=fifo_read(gbuf, 64u);  // 传入最大值:64!

数组只有 60 个元素,但允许函数写入最多 64 个元素!

当陀螺仪 FIFO 数据量较大时,会发生越界写入:

  • gbuf[60]、gbuf[61]、gbuf[62]、gbuf[63] 被写入

  • 这 4 个位置不属于 gbuf,而是其他变量的内存空间


内存布局的"巧合"

为什么 tmp_xyz 的存在能让程序正常工作?

静态变量(static)存储在 .bss 段,编译器会按声明顺序(或优化后的顺序)排列它们。

有 tmp_xyz 时的内存布局:┌─────────────────┐│ gbuf[0..59]     │ ← 60个元素,360字节├─────────────────┤│ tmp_xyz[0..179] │ ← 180个元素,360字节(充当"缓冲垫")├─────────────────┤│ 其他静态变量     │ ← 通信相关的队列、缓冲区等└─────────────────┘         ↑    越界写入落在 tmp_xyz 区域,不影响关键数据
删除 tmp_xyz 后的内存布局:┌─────────────────┐│ gbuf[0..59]     │ ← 60个元素├─────────────────┤│ 通信相关变量     │ ← 被越界写入破坏!└─────────────────┘         ↑    越界写入直接破坏通信数据结构

tmp_xyz 无意中充当了"保护垫",吸收了越界写入的伤害。


修复方案

修改一个字符即可:

// 修改前volatile int ng=fifo_read(gbuf, 64u);// 修改后volatile int ng=fifo_read(gbuf, 60u);

或者更规范的写法:

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))volatile int ng=fifo_read(gbuf, ARRAY_SIZE(gbuf));

经验教训

1. "删除代码导致崩溃"往往是内存问题的信号

当遇到这类诡异现象时,应该优先排查:

  • 数组越界

  • 野指针

  • 栈溢出

  • 未初始化变量

2. 静态分析的局限性

这个 bug 在代码审查时很难发现:

  • gbuf 的定义和使用相距几十行

  • 60 和 64 的差异很容易被忽略

  • 没有运行时错误提示(ARM Cortex-M 默认不检查数组越界)

3. 编译器/链接器不会帮你

C 语言不做数组边界检查。越界写入是未定义行为:

  • 可能立刻崩溃

  • 可能悄无声息地破坏其他数据

  • 可能在某些内存布局下"恰好"正常工作

4. 防御性编程建议

// 推荐:使用 sizeof 或 ARRAY_SIZE 宏#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))static struct sensor_datagbuf[60u];volatile int ng = fifo_read(gbuf, ARRAY_SIZE(gbuf));

或者在函数入口处添加断言:

int  fifo_read(struct  sensor_data*out, uint8_tmax_frames){assert(out!=NULL);assert(max_frames<=EXPECTED_MAX);  // 添加合理性检查// ...}

类似问题的排查思路

遇到"删除/修改无关代码导致异常"时,可以按以下步骤排查:

┌─────────────────────────────────────────┐│ 1. 怀疑内存问题(而不是时序或编译器)    │└───────────────────┬─────────────────────┘                    ▼┌─────────────────────────────────────────┐│ 2. 设计对照实验,缩小范围               ││    - 只保留声明 vs 保留执行代码         ││    - 修改数组大小观察效果               │└───────────────────┬─────────────────────┘                    ▼┌─────────────────────────────────────────┐│ 3. 检查 .bss 段相邻的静态变量           ││    - 查看 .map 文件中的符号地址         ││    - 关注数组和缓冲区的边界             │└───────────────────┬─────────────────────┘                    ▼┌─────────────────────────────────────────┐│ 4. 审查所有数组访问,对比声明大小       ││    和实际使用的索引/长度参数            │└─────────────────────────────────────────┘

总结

这个问题表面上是"FreeRTOS 时序问题",实际上是经典的数组越界 bug。

它之所以难以发现,是因为:

  1. 越界写入的位置恰好被一个"无用"的数组保护

  2. 删除"无用"代码后,内存布局改变,bug 才暴露

教训:在嵌入式开发中,遇到"删除无关代码导致崩溃"的现象,第一反应应该是排查内存问题,而不是怀疑 RTOS 或编译器。


参考资料

  • C语言未定义行为

  • ARM Cortex-M 内存保护单元 (MPU)

  • GCC -fsanitize=address 选项(桌面调试时可用)

最新文章

随机文章