|
去年帮一个做工业网关的嵌入团队看代码,他们用 Coverity 扫了三遍,式软扫工报告全是漏具绿的。结果产品上线两个月,嵌入一台设备在客户现场死机,式软扫工日志显示是漏具串口 DMA 缓冲区溢出。问题出在哪?嵌入Coverity 根本不认识他们自己写的环形缓冲队列——那玩意儿是拿宏拼出来的,静态分析器直接绕过去了。式软扫工 嵌入式软件的漏具漏扫,跟 Web 或桌面端完全不是嵌入一回事。你拿 Fortify 去扫 STM32 固件,式软扫工它连 __attribute__((packed)) 都解析不明白。漏具编译器扩展、嵌入内存映射寄存器、式软扫工中断向量表、漏具链接脚本里的段布局——这些玩意儿对通用 SAST 工具来说就是天书。 我见过太多团队在这上面翻车。有个做 BMS 电池管理的,用某商业工具扫出来 200 多个“高危”,逐条看下去,一半是误报:工具把 volatile 修饰的寄存器读写当成了竞态条件,把 __irq 函数里的局部变量当成了未初始化。工程师花了三周做 triage,真正有问题的只有两个:一个是在中断里调了 malloc,另一个是 CAN 接收回调里没做长度校验。这两个问题,工具的报告里反而排在最后,因为它们的“置信度”评分低。 所以嵌入式漏扫的第一原则:别迷信通用工具。你得先搞清楚你的代码里有哪些“方言”。GCC 的 __attribute__、IAR 的 #pragma、ARM 的 CMSIS 内联函数、RTOS 的宏封装——这些都会让分析器失明。我自己的做法是,先用 gcc -E 把预处理后的代码导出来,把宏全部展开,再喂给分析工具。这样虽然丢了上下文,但至少能看见真实的数据流。 另一个坑是内存模型。嵌入式里到处都是内存映射 I/O,指针直接指向 0x40000000 这种地址。静态分析器看到这种硬编码地址就懵了,要么报“不可达代码”,要么报“空指针解引用”。你得手动写规则,告诉工具哪些地址段是合法的外设区域。Coverity 支持 --checker-option 自定义,但文档写得跟天书一样,我当初调这个规则花了整整两天。 动态分析在嵌入式里更麻烦。你没法像 Web 那样挂个代理抓包,也没法随便插桩——资源受限,RAM 可能就 20KB。我试过用 QEMU 跑固件,配合 AFL 做模糊测试,效果还行,但前提是你得把外设模型搭出来。串口、SPI、GPIO 的模拟要是做得不对,跑出来的 crash 全是假的。有个做智能锁的团队就这么干过,QEMU 里跑出 17 个 crash,拿到真板上一个都复现不了——因为他们的 QEMU 模型里 Flash 读写时序跟真实芯片差太远。 那怎么办?我的经验是分层来。第一层,用 cppcheck 加自定义规则,专门抓那些显而易见的毛病:数组越界、未初始化变量、中断里的阻塞调用。这层快,误报多,但能兜底。第二层,用 clang static analyzer 配合 scan-build,它能理解一部分 GCC 扩展,对内存泄漏和空指针的检出率比 cppcheck 高。第三层,针对 RTOS 的 API 做专项检查——比如 FreeRTOS 里 xQueueSend 在中断里必须用 FromISR 版本,这种规则你得自己写脚本去 grep,工具不会替你判断。 最后说个真实案例。某医疗设备厂商,固件里有个 CRC 校验函数,用的是查表法。漏扫工具报“数组索引可能越界”,因为表的大小是 256,而索引变量是 uint8_t。工程师一看,uint8_t 最大 255,不可能越界,就把这条标记为误报。后来发现,那个索引变量在某个中断里被赋值为 0xFF + 1,溢出后变成 0,但编译器优化时把 uint8_t 的提升规则搞错了,实际生成的代码里索引变成了 int 类型的 256。工具报的是对的,人反而判断错了。 嵌入式漏扫的终点不是工具,是你对代码和硬件的理解。工具能帮你找线索,但每条线索都得你自己去验证。别偷懒。 |