资讯动态

嵌入式软件单元测试(十九)——内存越界与野指针无处遁形:结合Sanitizer进行嵌入式单元测试

发布时间:2026/9/12 15:33:36 来源:尧图企业网站定制
❄️ 我的个人专栏《智能软件工程AI4SE》《嵌入式面试总结》《嵌入式处理器架构解析》《嵌入式与虚拟化》《嵌入式软件测试》 Simplicity is the ultimate sophistication摘要本文介绍如何借助 Sanitizer 工具在嵌入式单元测试阶段发现内存越界与野指针问题。文章首先说明嵌入式单元测试引入 Sanitizer 的必要性随后讲解 ASan、UBSan、LSan 等核心组件的原理并给出在 GCC 与 CMake 工程中的集成方法。接着通过缓冲区越界和野指针访问两个实战案例演示 Sanitizer 如何精准定位错误并与 Valgrind 进行适用场景对比。最后总结嵌入式环境下的注意事项建议将 Sanitizer 纳入单元测试标准流程让内存问题在开发早期无处遁形。1. 引言在嵌入式软件开发中内存越界和野指针是两类最隐蔽、最难排查的缺陷。它们往往不会在第一次运行时立即暴露而是潜伏在代码深处直到某个偶然的时机才引发系统崩溃或数据错乱。传统的单元测试虽然能验证功能逻辑却很难捕捉这类内存问题。本文介绍如何借助 Sanitizer 工具在嵌入式单元测试阶段就让内存越界与野指针无处遁形。2. 为什么嵌入式单元测试需要 Sanitizer嵌入式系统资源受限开发者通常依赖交叉编译工具链进行单元测试。然而目标板上难以直接运行复杂的动态分析工具因此很多内存问题被推迟到集成测试甚至现场运行阶段才暴露。Sanitizer 提供了一种在主机环境模拟运行、提前发现内存错误的有效手段。常见的内存问题包括缓冲区越界读写超出数组或堆内存边界。野指针访问使用已释放或未初始化的指针。内存泄漏动态分配的内存未释放。重复释放对同一块内存执行多次 free。这些问题在单元测试阶段通过 Sanitizer 即可被精准定位从而大幅降低后期修复成本。3. Sanitizer 核心组件与原理Sanitizer 是编译器内置的一组动态分析工具常用的包括 AddressSanitizerASan、UndefinedBehaviorSanitizerUBSan和 LeakSanitizerLSan。它们通过在编译期插入检测代码在运行时捕获异常行为。工具检测目标典型错误AddressSanitizer内存访问合法性堆越界、栈越界、野指针、重复释放UndefinedBehaviorSanitizer未定义行为整数溢出、空指针解引用、移位越界LeakSanitizer内存泄漏未释放的堆内存ASan 的核心原理是在每次内存访问前后插入检查代码并维护一份影子内存shadow memory来记录每块内存的合法状态。当检测到非法访问时立即输出详细的错误报告包括调用栈和出错位置。4. 在嵌入式单元测试中集成 Sanitizer嵌入式单元测试通常在主机上编译运行以便利用丰富的调试工具。以 GCC 工具链为例只需在编译和链接时添加相应编译选项即可启用 Sanitizer。gcc -fsanitizeaddress,undefined -g -O1 -o test_runner test_runner.c对于使用 CMake 构建的嵌入式单元测试工程可以在 CMakeLists.txt 中统一配置set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} -fsanitizeaddress,undefined -fno-omit-frame-pointer) set(CMAKE_EXE_LINKER_FLAGS ${CMAKE_EXE_LINKER_FLAGS} -fsanitizeaddress,undefined)需要注意的是Sanitizer 会增加运行时开销和内存占用因此仅建议在单元测试构建中启用发布版本应关闭这些选项。5. 对比Sanitizer 与 Valgrind 的适用场景在嵌入式单元测试中除了 SanitizerValgrind 也是常用的内存检测工具。两者各有优劣适用场景也有所不同。下表从多个维度对二者进行对比对比维度SanitizerValgrind检测能力可检测堆越界、栈越界、野指针、重复释放、内存泄漏及未定义行为可检测堆越界、野指针、重复释放、内存泄漏但对栈越界检测能力较弱性能开销运行速度约为原生程序的 1.5 到 2 倍内存占用增加约 2 到 3 倍运行速度约为原生程序的 10 到 50 倍内存占用显著增加编译方式需在编译期插入检测代码重新编译被测工程无需重新编译直接运行二进制程序即可平台支持依赖编译器支持GCC 和 Clang 均内置主机模拟环境即可运行主要支持 Linux 和 macOS对交叉编译产物支持有限嵌入式适配适合与 CMake 等构建系统集成便于纳入持续集成流程对裸机或资源受限目标板支持较差更适合主机侧验证错误报告报告包含出错位置、调用栈和影子内存信息定位精准报告详细但输出量大解析成本相对较高综合来看选择建议如下优先选择 Sanitizer当工程使用 GCC 或 Clang 工具链且可以重新编译单元测试时Sanitizer 的性能开销更低检测能力更全面更适合作为嵌入式单元测试的默认选择。选择 Valgrind当无法重新编译被测代码或需要检测已编译好的二进制程序时Valgrind 无需改动构建流程即可直接运行适合作为补充验证手段。两者结合在持续集成环境中可先用 Sanitizer 跑常规单元测试再对关键模块用 Valgrind 做交叉验证以覆盖更全面的内存问题场景。6. 实战案例捕获缓冲区越界下面通过一个典型示例演示 ASan 如何捕获缓冲区越界。假设被测函数如下#include string.h void copy_data(char *dest, const char *src, size_t len) { memcpy(dest, src, len); }编写单元测试时故意传入过长的数据长度#include assert.h #include stdlib.h void test_copy_data_overflow(void) { char dest[8]; char src[16] 0123456789abcdef; copy_data(dest, src, 16); /* 越界写入 */ assert(dest[0] 0); } int main(void) { test_copy_data_overflow(); return 0; }使用 ASan 编译并运行后会输出类似如下的错误报告ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc... WRITE of size 16 at 0x7ffc... thread T0 #0 copy_data test.c:4 #1 test_copy_data_overflow test.c:12 #2 main test.c:18报告明确指出了越界类型、出错位置和调用栈开发者可以据此快速定位并修复问题。7. 实战案例定位野指针访问野指针通常源于释放后继续使用或未初始化指针。以下示例演示 ASan 对 use-after-free 的检测#include stdlib.h int *get_ptr(void) { int *p (int *)malloc(sizeof(int)); *p 42; free(p); return p; /* 返回野指针 */ } int main(void) { int *q get_ptr(); return *q; /* 访问已释放内存 */ }运行 ASan 构建的程序后会报告 heap-use-after-free 错误并给出分配和释放的调用栈帮助开发者理解指针的生命周期问题。8. 嵌入式环境下的注意事项在嵌入式单元测试中应用 Sanitizer需要注意以下几点平台差异Sanitizer 依赖操作系统提供的内存管理接口因此通常只能在主机模拟环境中运行无法直接部署到裸机目标板。性能开销ASan 会显著增加运行时间和内存占用建议在持续集成环境中单独设置测试任务。硬件抽象层隔离被测代码应尽量与硬件解耦通过模拟外设接口来保证主机可编译运行。与模拟器结合对于依赖特定寄存器的代码可使用 QEMU 等模拟器配合 Sanitizer 进行更接近真实环境的验证。9. 总结内存越界和野指针是嵌入式软件中最具破坏力的缺陷类型而 Sanitizer 为单元测试阶段提前发现这些问题提供了高效手段。通过在主机环境集成 AddressSanitizer 和 UndefinedBehaviorSanitizer开发者可以在代码合入前就捕获大量内存错误显著提升软件质量。建议将 Sanitizer 纳入嵌入式单元测试的标准流程并配合持续集成机制让内存问题在开发早期就无处遁形。ehaviorSanitizer开发者可以在代码合入前就捕获大量内存错误显著提升软件质量。建议将 Sanitizer 纳入嵌入式单元测试的标准流程并配合持续集成机制让内存问题在开发早期就无处遁形。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价