尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

嵌入式软件单元测试(三十九)——嵌入式单元测试中的内存泄漏检测:集成TLSF或malloc_hook

嵌入式软件单元测试(三十九)——嵌入式单元测试中的内存泄漏检测:集成TLSF或malloc_hook ❄️ 我的个人专栏《智能软件工程AI4SE》《嵌入式面试总结》《嵌入式处理器架构解析》《嵌入式与虚拟化》《嵌入式软件测试》 Simplicity is the ultimate sophistication摘要本文介绍嵌入式软件单元测试中的内存泄漏检测方法围绕 TLSF 分配器与 malloc_hook 机制两条技术路线展开。TLSF 方案利用分配器元数据实现可追踪的泄漏检测适合目标板与实时场景malloc_hook 方案通过拦截 glibc 分配函数实现零侵入检测适合主机快速迭代。文章结合代码示例说明两种方案的实现原理、分配点追踪与调用栈定位方法并通过对比表格给出选型建议最后总结实践中的注意事项。1. 引言在嵌入式软件单元测试中内存泄漏是隐蔽性最高、排查成本最大的缺陷类型之一。与桌面应用不同嵌入式环境往往没有操作系统级别的内存回收机制也没有 Valgrind 这类重量级工具可以直接运行。因此在单元测试阶段引入轻量、可控的内存泄漏检测手段是保障固件长期稳定运行的关键一环。本文围绕两条主流技术路线展开一是集成 TLSFTwo-Level Segregated Fit分配器利用其可统计、可追踪的元数据能力实现泄漏检测二是通过 malloc_hook 机制在测试构建中拦截 malloc/free 调用记录分配与释放的配对关系。两种方案各有适用场景本文会结合代码示例说明如何在单元测试工程中落地。2. 内存泄漏检测的基本原理内存泄漏的本质是程序在运行过程中申请了堆内存但失去了对该内存的引用导致无法释放也无法再次使用。在单元测试中检测内存泄漏通常采用「分配计数」与「分配追踪」两种策略。分配计数是最简单的策略统计测试用例执行前后的堆分配次数与释放次数若两者不相等则判定存在泄漏。这种策略实现简单、开销小但无法定位泄漏发生的具体位置。分配追踪则更进一步在每次分配时记录调用栈或分配点信息测试结束后遍历未释放的分配记录即可精确定位泄漏代码。代价是额外的内存开销和性能损耗。在嵌入式单元测试中通常采用「计数 追踪」结合的方式先用计数快速判断是否存在泄漏再对未释放记录做详细追踪定位。3. 方案一集成 TLSF 分配器TLSFTwo-Level Segregated Fit是一种面向实时系统的内存分配算法具有 O(1) 时间复杂度的分配与释放性能且内存碎片率低。更重要的是TLSF 的元数据结构天然支持遍历已分配块这为内存泄漏检测提供了便利。3.1 TLSF 的核心优势确定性性能分配与释放均为 O(1)适合对实时性有要求的嵌入式系统。低碎片率两级隔离适配算法有效控制外部碎片。可追踪性每个内存块头部包含元数据可遍历所有已分配块。可移植性不依赖操作系统可在裸机环境和 RTOS 环境中运行。3.2 在单元测试中集成 TLSF在单元测试工程中集成 TLSF通常的做法是将 TLSF 作为被测模块的堆分配器并在测试框架中增加泄漏检测逻辑。下面给出一个基于 TLSF 的泄漏检测示例。#include stdio.h #include tlsf.h static tlsf_t g_test_heap; static int g_alloc_count 0; static int g_free_count 0; void test_heap_init(void *mem, size_t size) { g_test_heap tlsf_create(mem, size); g_alloc_count 0; g_free_count 0; } void *test_malloc(size_t size) { void *ptr tlsf_malloc(g_test_heap, size); if (ptr) { g_alloc_count; } return ptr; } void test_free(void *ptr) { if (ptr) { tlsf_free(g_test_heap, ptr); g_free_count; } } int test_heap_check_leak(void) { if (g_alloc_count ! g_free_count) { printf(Memory leak detected: alloc%d free%d\n, g_alloc_count, g_free_count); return -1; } return 0; }在上述代码中test_malloc 和 test_free 是对 TLSF 分配器的封装同时维护分配与释放的计数。测试用例执行完毕后调用 test_heap_check_leak 即可判断是否存在泄漏。3.3 基于 TLSF 的分配点追踪仅靠计数无法定位泄漏位置。TLSF 的块头部包含物理地址信息我们可以在此基础上扩展记录每个分配点的文件名与行号。#define TRACK_MAX 1024 typedef struct { void *ptr; const char *file; int line; size_t size; } alloc_record_t; static alloc_record_t g_records[TRACK_MAX]; static int g_record_count 0; void *test_malloc_tracked(size_t size, const char *file, int line) { void *ptr tlsf_malloc(g_test_heap, size); if (ptr g_record_count TRACK_MAX) { g_records[g_record_count].ptr ptr; g_records[g_record_count].file file; g_records[g_record_count].line line; g_records[g_record_count].size size; g_record_count; } return ptr; } void test_free_tracked(void *ptr) { for (int i 0; i g_record_count; i) { if (g_records[i].ptr ptr) { g_records[i] g_records[g_record_count - 1]; g_record_count--; break; } } tlsf_free(g_test_heap, ptr); } void test_dump_leaks(void) { for (int i 0; i g_record_count; i) { printf(Leak: %zu bytes at %s:%d\n, g_records[i].size, g_records[i].file, g_records[i].line); } }配合宏定义可以在调用点自动记录文件与行号#define MALLOC_TRACKED(size) \ test_malloc_tracked((size), __FILE__, __LINE__) #define FREE_TRACKED(ptr) \ test_free_tracked((ptr))4. 方案二使用 malloc_hook 机制malloc_hook 是 glibc 提供的一种动态拦截机制允许应用程序在运行时替换 malloc、free、realloc 等函数的实现。该机制在 Linux 主机上运行嵌入式单元测试时非常实用无需修改被测代码即可实现泄漏检测。4.1 malloc_hook 的工作原理glibc 通过全局函数指针 __malloc_hook、__free_hook 等实现钩子机制。当应用程序调用 malloc 时glibc 会先检查 __malloc_hook 是否为空若非空则调用钩子函数。利用这一特性可以在钩子函数中记录分配信息。4.2 基于 malloc_hook 的泄漏检测实现#define _GNU_SOURCE #include stdio.h #include stdlib.h #include malloc.h static void *(*orig_malloc_hook)(size_t, const void *); static void (*orig_free_hook)(void *, const void *); static int g_alloc_count 0; static int g_free_count 0; static void *test_malloc_hook(size_t size, const void *caller) { void *ptr; __malloc_hook orig_malloc_hook; ptr malloc(size); orig_malloc_hook __malloc_hook; __malloc_hook test_malloc_hook; if (ptr) { g_alloc_count; } return ptr; } static void test_free_hook(void *ptr, const void *caller) { __free_hook orig_free_hook; free(ptr); orig_free_hook __free_hook; __free_hook test_free_hook; if (ptr) { g_free_count; } } void leak_detect_enable(void) { orig_malloc_hook __malloc_hook; orig_free_hook __free_hook; __malloc_hook test_malloc_hook; __free_hook test_free_hook; } void leak_detect_disable(void) { __malloc_hook orig_malloc_hook; __free_hook orig_free_hook; } int leak_detect_check(void) { if (g_alloc_count ! g_free_count) { printf(Memory leak: alloc%d free%d\n, g_alloc_count, g_free_count); return -1; } return 0; }在测试用例的 setUp 中调用 leak_detect_enable在 tearDown 中调用 leak_detect_check 并随后调用 leak_detect_disable即可实现每个用例的泄漏检测。4.3 记录调用栈定位泄漏点malloc_hook 的 caller 参数携带了调用返回地址可以通过 backtrace 函数将其转换为调用栈从而定位泄漏发生的代码位置。#include execinfo.h #define BACKTRACE_DEPTH 16 typedef struct { void *ptr; void *bt[BACKTRACE_DEPTH]; int bt_size; size_t size; } trace_record_t; static trace_record_t g_traces[TRACK_MAX]; static int g_trace_count 0; static void *test_malloc_hook(size_t size, const void *caller) { void *ptr; __malloc_hook orig_malloc_hook; ptr malloc(size); orig_malloc_hook __malloc_hook; __malloc_hook test_malloc_hook; if (ptr g_trace_count TRACK_MAX) { g_traces[g_trace_count].ptr ptr; g_traces[g_trace_count].size size; g_traces[g_trace_count].bt_size backtrace(g_traces[g_trace_count].bt, BACKTRACE_DEPTH); g_trace_count; } return ptr; } static void test_free_hook(void *ptr, const void *caller) { __free_hook orig_free_hook; free(ptr); orig_free_hook __free_hook; __free_hook test_free_hook; for (int i 0; i g_trace_count; i) { if (g_traces[i].ptr ptr) { g_traces[i] g_traces[g_trace_count - 1]; g_trace_count--; break; } } } void leak_detect_dump(void) { char **symbols; for (int i 0; i g_trace_count; i) { printf(Leak: %zu bytes\n, g_traces[i].size); symbols backtrace_symbols(g_traces[i].bt, g_traces[i].bt_size); for (int j 0; j g_traces[i].bt_size; j) { printf( %s\n, symbols[j]); } free(symbols); } }5. 两种方案的对比与选型对比维度TLSF 方案malloc_hook 方案运行环境裸机、RTOS、主机均可仅限 glibc 主机环境被测代码侵入性需替换分配器或链接封装层无需修改被测代码性能开销O(1) 分配开销低每次调用多一次间接跳转定位能力需自行扩展记录分配点可直接获取调用栈适用场景目标板测试、长期稳定性测试主机上的快速单元测试选型建议如果单元测试运行在 Linux 主机上且被测代码不依赖特定硬件优先选择 malloc_hook 方案接入成本低、定位能力强。如果测试需要部署到目标板或被测模块需要确定性实时性能则选择 TLSF 方案。6. 实践建议与注意事项区分测试构建与发布构建内存泄漏检测代码只应编译进测试构建发布构建保持原有分配路径避免性能损耗。关注静态分配嵌入式代码大量使用静态数组和全局变量这些不经过堆分配器不会被上述方案检测到需要结合代码审查和静态分析工具补充覆盖。处理第三方库第三方库内部的 malloc/free 调用同样会被 malloc_hook 拦截可能产生误报可通过白名单机制过滤已知的合法常驻分配。结合退出时检查在测试进程退出前做一次全局泄漏检查可以捕获测试框架自身或全局对象造成的泄漏。注意线程安全如果被测代码涉及多线程分配记录表的读写需要加锁保护避免数据竞争导致误判。7. 总结内存泄漏检测是嵌入式单元测试中不可忽视的质量关卡。TLSF 方案适合目标板与实时场景利用分配器元数据实现可追踪的泄漏检测malloc_hook 方案适合主机快速迭代通过拦截 glibc 分配函数实现零侵入检测。实际工程中建议根据测试运行环境和被测代码特性选择合适方案并将泄漏检测纳入每个测试用例的 setUp 与 tearDown 流程形成常态化质量保障机制。
返回列表