嵌入式系统内存溢出防护 — SVM:运行时检查 + 死空间复用 + 压缩(ACM TECS 2006)

马里兰大学 Barua 组发表于 ACM Transactions on Embedded Computing Systems 2006 的长文(34 页)。主题:没有 MMU/虚拟内存的嵌入式处理器(8051、68K、i960Sx、ARM7TDMI、MSP430 等)如何防内存溢出——纯软件方案 SVM(Safety run-time checks + reUse + Message compression)。对无 MMU 的 MCU 固件(如医疗仪器裸机程序)至今仍有工程参考价值。

问题本质

  • 桌面系统靠虚拟内存兜底(swap + 段隔离);嵌入式通常无磁盘无 MMU,设计师必须保证”并发任务总内存足迹任何时刻都放得下物理内存”——这要求编译期精确估算每个任务的最大内存需求
  • 栈难估算:递归、alloca、变长数组;堆更难:链表/树/图等输入依赖结构。工业惯例是”多组输入实测最大值 × 安全系数”,永远没有上界保证
  • 后果不对等:桌面崩溃是烦恼,嵌入式崩溃是失控、停产、事故乃至人命(自动驾驶仪失效样例)
  • 加大内存不是替代方案:估算无上界、成本竞争压着安全系数、应用规模随芯片容量同步增长,溢出概率不降反升

三层机制

1) 安全检查(可独立部署):malloc 自带失败检查(零开销);栈检查由编译器在每个过程序体栈指针递减后插入 SP < 边界 ? 报错 比较,边界三选一(堆指针/相邻任务栈底/内存末尾)编译期确定。核心优化 rolling checks:把子过程的检查”卷”进父过程(检查一次容纳父子帧总量),按调用频率从高到低卷。不可卷的例外:父过程在调用前分配堆、虚函数调用、alloca、递归、超过尺寸阈值。实测:运行时间开销未优化 3.01% → 优化后 1.35%,能耗 2.41% → 1.12%,代码 0.12% → 0.10%。开销大头来自尺寸阈值(1.08%),阈值从 10% 提到 20% 可把开销压到 1.0%(代价:略微提前报 OOM)。对比 Capriccio(栈分块放堆上)平均 4.43%,本方案常态连续分配只在真溢出才付出非连续代价,各基准稳定低开销。

2) 死空间复用:编译器做全局变量活跃性分析,按”区域”(region:每个循环/函数起止为界,DPRG 图 DFS 时间戳=运行序)预编译每个区域的 reuse candidate list(该区域及其全部可达函数中全程死亡的数组,按体积降序)。溢出时直接把 SP 改写到死亡数组末尾实现非连续增长;区段合并优化去冗余。堆场景要按”下次访问时间”排序(选最晚复活的数组),且在数组复活前运行时检查扩展堆是否已释放。栈还可长进堆内 free hole(配合 Lea malloc 的 2 幂空桶)。I/O 优化:预分配 stdio 缓冲,让库内大死数组提前释出。

3) 活数据压缩:候选列表扩展进”本区域不访问的活跃数组”,溢出时原地压缩(WKS 算法——WKdm 变体,支持原地压缩/解压;全局数据平均释放约 60% 空间,43 cycles/word),区域入口自动解压回原位(原地解压保证地址与外部指针不失效)。死数组永远排在活数组前面。

实验结果(GCC 3.2 → Motorola M-Core,MiBench/UTDSP 10 基准)

  • 回收空间占栈+堆总需求 0.7%(JPEG,堆活期贯穿全程序)到 93.5%(SUSAN,360KB 条件使用大数组)——均值无意义,应用相关
  • 最见效的是”栈长进死全局”;“堆 hole 给栈”与”压缩全局给堆”在该基准集零收益(堆活期覆盖全程序)
  • 全局段通常占数据大头(除 JPEG 外普遍 45%-100%),这正是死全局复用有料的原因
  • 实时性:正常路径开销编译期可预测,硬实时可行;仅”溢出后靠回收空间续命”阶段开销不可预测,只保软实时——“慢响应好过没响应”

对嵌入式固件工程的启示

  1. 无 MMU 平台上”每函数入口一条 SP 边界比较”的防护性价比极高(<1.5% 开销换确定性 OOM 检测 + 三类补救:交人工接管/安全停机/降级跛行模式)
  2. main() 里的大数组提为全局、输入输出数组错开活期、库 I/O 缓冲预分配——这些编码习惯能显著扩大可回收死空间
  3. 区域划分 + 编译期候选表的思想可迁移到任何”内存分区借用”设计(如双 bank 借用、overlay)

关联