项目环境: 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));
当遇到这类诡异现象时,应该优先排查:
数组越界
野指针
栈溢出
未初始化变量
这个 bug 在代码审查时很难发现:
gbuf 的定义和使用相距几十行
60 和 64 的差异很容易被忽略
没有运行时错误提示(ARM Cortex-M 默认不检查数组越界)
C 语言不做数组边界检查。越界写入是未定义行为:
可能立刻崩溃
可能悄无声息地破坏其他数据
可能在某些内存布局下"恰好"正常工作
// 推荐:使用 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。
它之所以难以发现,是因为:
越界写入的位置恰好被一个"无用"的数组保护
删除"无用"代码后,内存布局改变,bug 才暴露
教训:在嵌入式开发中,遇到"删除无关代码导致崩溃"的现象,第一反应应该是排查内存问题,而不是怀疑 RTOS 或编译器。
C语言未定义行为
ARM Cortex-M 内存保护单元 (MPU)
GCC -fsanitize=address 选项(桌面调试时可用)