嵌入式应用堆内存随机损坏 —— 排查过程
2026年6月 · ARM Linux 嵌入式设备
一、问题全景
Symptom: UI freeze after app startup No crash, no core dump generated │ ▼ Initial hypothesis: serial data corrupting store_data.alloc │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼ [Dead end 1] [Dead end 2] Mid: heap random GDB hardware Valgrind/ASan corruption watchpoint toolchain QMap/QString board OOM not supported rewritten │ ┌──────────────────┼──────────────────┐ │ │ ▼ ▼ [Dead end 3] Root cause confirmed: Suspecting DeviceMonitor SharedDataStore readLen bug is the cross-thread access ONLY cause with NO lock │ ┌──────┴──────┐ ▼ ▼ Fix: recursive Build abc123def mutex protecting deployed, pending all shared data board verification二、排查时间线
Day 1— 用户报告:app 启动不久 alloc 被踩,store_data 扩容时 abort。开始 GDB 远程调试。
弯路— 尝试在 ARM 板子上用 GDB 硬件 watchpoint 抓 alloc 写入者。板子约 1GB 内存,GDB 加载约 100MB 完整调试符号后吃掉 500MB,板子 OOM 红屏。硬件断点不可用,软件断点单步执行 CPU 99% 卡死。
Day 2— 写 heap_monitor.sh 监控脚本,用 ELF 符号表提取 vtable 地址,GDB 扫堆定位 DeviceMonitor/DeviceController 对象,轮询 alloc 字段。脚本能跑但 GDB attach/detach 太频繁会打挂板子。
弯路— 对比 dev_monitor.cpp 和 dev_controller.cpp,发现前者 COMM_MODE_TEXT==0 分支用 bytesAvailable 而非 readLen 构造 QByteArray。修改后编译部署,以为修好了。但这是表层问题。
Day 3— 换板子,release 版复现 UI 卡死。GDB attach 拿到主线程 backtrace:
MainWindow::refresh_SystemTimeSlot │ (timer-triggered system time refresh) ▼ QLabel::setText │ ▼ Qt internal memory allocation │ ▼ malloc / realloc │ ▼ glibc malloc_consolidate │ detects heap corruption ▼ abort() │ ▼ libSegFault.so catches SIGABRT │ signal handler calls malloc again ▼ futex deadlock │ all threads freeze one by one ▼ [UI completely frozen]关键发现— GDB frame 12/13 查看局部变量:showText.d = 0x19a5ef50恰好等于this(MainWindow 地址)。QString 的内部 d 指针被写成了对象指针,这不可能是串口垃圾数据能做到的——是结构性写错。
Day 4— 重新审视代码架构。发现 AppModule::initObject() 中 data_store 的传递模式:
Main Thread SharedDataStore Worker Thread (AppModule) (data_store) (WorkerTask) │ │ │ │──new SharedStore──▶│ │ │ │ │ │──new WorkerTask────┼─────────────────────▶│ │ (passes data_store ptr) │ │ │ │ │──new MainWindow─────┼─▶ │ │ (data_store ptr) │ │ │ │ │ │ │◀──Get_Title() read──│ │ │ QMap access │ │ │ │ │──setValueSlot()────▶│ │ │ write QString │ │ │ │ │ │ ⚠ RACE CONDITION ⚠ │ │ Ref-count corruption → heap damage │WorkerTask 工作线程通过 data_store 调用的函数
Get_Title Get_Value Get_Uint Get_ValueUint Get_MinValue Get_MaxValue Get_OwnerWindow Get_OwnerPage Get_HourToMin Get_ProcessTime Get_RemainingTime Get_TimingStatus getLangEnabled getTestTitle getRunStep get_UIBoardSelection getHasUserInputData getSelfTest isChannelAorBOn isExtendParam0On isExtendParam1On SendToDevice(int,QString,DataSendType)全部读取 sensor_data[] 或 active_locale,与主线程的写入并发,均存在竞态。
┌── Main Thread ──┐ ┌── Worker Thread ──┐ │ │ │ │ │ setValueSlot() │ │ Get_Value() │ │ writes to │ RACE! │ reads from │ │ sensor_data[] │◄────────▶│ sensor_data[] │ │ .Cur_Value │ │ .Cur_Value │ │ │ │ │ │ Set_PumpSpeed() │ │ Get_Title() │ │ writes to │ RACE! │ reads │ │ sensor_data[] │◄────────▶│ active_locale │ │ │ │ + QMap │ └──────────────────┘ └────────────────────┘ QString implicit sharing → ref-count is NOT atomic → double-free / use-after-freeDay 5— Valgrind/ASan 尝试均失败。Yocto 交叉编译链不带 libasan,GCC 7.3 编译时未启用 sanitizer。x86 VM 上 Qt5 只有运行库没有开发包且 app 依赖通信口硬件无法直接跑。
修复— 在 SharedDataStore 中启用已有 QMutex 改为递归锁:
// datastore.h: mutable QMutex mutex;// datastore.cpp constructor: mutex(QMutex::Recursive)// 每个读写共享数据的 public 函数首行:QMutexLockerlocker(&mutex);覆盖30 个函数,setValueSlot 原有手动 tryLock/unlock 替换为 QMutexLocker。纯 emit 信号函数和 QSettings 访问函数无需加锁。
三、技术总结
| 错误思路 | 为什么是弯路 | 最终结论 |
|---|---|---|
| GDB 硬件 watchpoint 抓 alloc 写入指令 | ARM 板性能不足,硬件断点不可用,软件断点单步 CPU 99% 卡死 | 改用轮询监控脚本 + GDB 短暂 attach |
| 只修 DeviceMonitor 的 readLen bug | 这是真实 bug,但不是唯一原因,修了之后旧板子 release 版仍崩 | readLen 修复有价值,但堆损坏根因更深 |
| 怀疑串口垃圾数据污染堆 | 解释不了 showText.d = this 这种结构性写错 | 跨线程 QString 引用计数竞态才能产生这种"把 A 的地址写到 B"的现象 |
| 尝试 Valgrind / ASan | Yocto 工具链不支持,x86 VM 环境不完整 | 直接看代码逻辑找到 root cause |
四、补充隐患:char 数组
代码中大量使用裸char buf[2048]而非 QByteArray 或 std::array:
DeviceMonitor::recv_buffer[2048]— 类成员,位于 store_data 之后仅 8 字节,残留旧数据是 readLen bug 的直接原因DeviceController::recvBuffer[2048]— 栈局部变量,每次重新分配避免了残留,是正确写法
建议统一使用 QByteArray + readLen 精确控制,消除硬编码长度。
五、产出
| 产出 | 路径 |
|---|---|
| SharedDataStore 互斥锁修复 | datastore.cpp + datastore.h(30个函数) |
| 已编译二进制 | BuildID abc123def(VM: /build/product/app/AppModule) |
| 监控脚本 heap_monitor.sh | tools/heap_monitor.sh(不依赖 .debug 文件,自适应任意版本) |
| 分析文档 | tools/跨线程SharedData分析.md |