> 技术文档 > 【Zephyr炸裂知识系列】11_手撸内存泄露监测算法

【Zephyr炸裂知识系列】11_手撸内存泄露监测算法


文章目录

  • 前言
  • 一、监测核心算法架构
      • 1.1:算法架构图
      • 1.2:关键设计理念
  • 二、链表记录算法实现原理
      • 2.1:宏拦截机制
      • 2.2 内存记录结构
      • 2.3 核心算法流程
  • 三、泄露检测算法原理
      • 3.1 泄露判定标准
      • 3.2 检测算法实现
  • 四、实际内存泄露监测案例测试
      • 4.1:内存申请
      • 4.2:内存释放
      • 4.3:制造内存泄露
      • 4.4:手动打印内存信息
      • 4.5:制造重复释放
  • 五、算法优势与特点
    • 5.1 技术优势
    • 5.2 适用场景
  • 总结

前言

在嵌入式系统开发中,内存泄漏是一个常见且难以调试的问题。特别是在资源受限的物联网或单片机设备中,即使是微小的内存泄漏,长期运行后也可能导致系统崩溃。世面上有Valgrind、AddressSanitizer等强大的工具,但对于资源受限的嵌入式设备,这些工具往往过于庞大或无法使用,而手动检查又效率低下。
为此,我们本文将介绍一种轻量级的内存泄露检测算法,专为RTOS环境设计,它能够在资源受限的环境中实时监测内存使用情况,准确识别内存泄漏和错误操作。本文将详细介绍该检测器的核心架构、使用方法和实际测试效果。


一、监测核心算法架构

1.1:算法架构图

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│ 应用程序代码 │ │ 内存检测拦截宏 │ │ 拦截宏内重新调用 ││  │ │  │ │  ││ malloc() │───▶│ ml_malloc() │───▶│ malloc() ││ free() │───▶│ ml_free() │───▶│ free() ││ calloc() │───▶│ ml_calloc() │───▶│ calloc() ││ realloc() │───▶│ ml_realloc() │───▶│ realloc() │└─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▼ ┌─────────────────┐ │ 内存记录链表 │ │ - 分配地址 │ │ - 分配大小 │ │ - 文件位置 │ │ - 行号信息 │ │ - 时间戳 │ └─────────────────┘

1.2:关键设计理念

  • 拦截层:通过宏替换拦截所有内存操作函数
  • 记录层:维护分配记录链表,保存完整的上下文信息
  • 分析层:实时分析内存使用情况,检测泄漏和错误
  • 报告层:提供多种输出方式(日志、文件、统计信息)

二、链表记录算法实现原理

2.1:宏拦截机制

// 条件编译控制,避免循环引用#ifdef MEMORY_LEAK_USER_OPERATE #define malloc(size) ml_malloc(size, __FILE__, __LINE__) #define free(ptr) ml_free(ptr, __FILE__, __LINE__) #define calloc(num, size) ml_calloc(num, size, __FILE__, __LINE__) #define realloc(ptr, size) ml_realloc(ptr, size, __FILE__, __LINE__)#endif

关键点

  • 使用 __FILE____LINE__ 宏获取调用位置
  • 通过条件编译避免在检测器内部形成循环
  • 保持与标准库函数相同的接口

2.2 内存记录结构

typedef struct { void* address;  // 内存地址 uint32_t size;  // 分配大小 uint32_t timestamp; // 分配时间戳 uint32_t line;  // 源代码行号 char file_name[32]; // 文件名(仅文件名,不包含路径) char thread_name[25]; // 线程名称 leak_status_t status; // 泄露状态} memory_info_t;typedef struct memory_node { memory_info_t mem_info; struct memory_node* next;} memory_node_t;

内存优化策略

  • 文件名只存储文件名部分,节省内存
  • 使用链表结构,支持动态增长
  • 每个节点开销约71字节

2.3 核心算法流程

分配跟踪算法

void* ml_malloc(size_t size, const char* file, uint32_t line) { // 1. 调用C语言真实malloc void* ptr = malloc(size); if (ptr != NULL) { // 2. 记录分配信息 ml_add_info(ptr, k_uptime_get_32(), size, file, line,  k_thread_name_get(k_current_get()), LEAK_OK); } return ptr;}

释放跟踪算法

void ml_free(void* ptr, const char* file, uint32_t line) { // 1. 从记录中查找并移除 memory_node_t* node = ml_find_and_remove(ptr); if (node == NULL) { // 2. 检测重复释放 ml_add_info(ptr, k_uptime_get_32(), 0, file, line,  k_thread_name_get(k_current_get()), LEAK_DOUBLE_FREE); } else { // 3. 正常释放,更新统计 g_stats.total_deallocations++; g_stats.current_allocations--; g_stats.total_memory_used -= node->mem_info.size; free(node); } // 4. 调用真实free free(ptr);}

三、泄露检测算法原理

3.1 泄露判定标准

typedef enum { LEAK_OK = 0,  // 正常 LEAK_MEMORY_LEAK, // 内存泄露 LEAK_SUSPICIOUS, // 可疑分配 LEAK_DOUBLE_FREE, // 重复释放 LEAK_FREE_NULL // 释放空指针} leak_status_t;

泄露判定逻辑

  • 内存泄露:分配后超过 MIN_LIFETIME_MS 仍未释放
  • 可疑分配:分配时间超过阈值但可能正常(如全局变量)
  • 重复释放:对同一地址多次调用 free()
  • 释放空指针:对 NULL 调用 free()

3.2 检测算法实现

void ml_check_leaks(void) { if (lifetime > CONFIG_MEMORY_LEAK_DETECTOR_MIN_LIFETIME_MS) { if (lifetime > CONFIG_MEMORY_LEAK_DETECTOR_MIN_LIFETIME_MS * 3) { // 超过3倍时间阈值,判定为泄露 current->mem_info.status = LEAK_MEMORY_LEAK; g_stats.leak_count++; } else { // 超过阈值但未超过3倍时间,判定为可疑 current->mem_info.status = LEAK_SUSPICIOUS; g_stats.suspicious_count++; } }}

四、实际内存泄露监测案例测试

必须操作:包含宏代替文件

// 在main.c中的集成示例#include #ifdef CONFIG_MEMORY_LEAK_DETECTOR #define MEMORY_LEAK_USER_OPERATE #include #endif

4.1:内存申请

申请程序:

LOG_INF(\"--- 测试1:正常内存操作 ---\"); void* normal_ptr = malloc(100, __FILE__, __LINE__); LOG_INF(\"正常分配: %p\", normal_ptr);

插桩反馈:

 <inf> main: --- 测试1:正常内操作 ---[MEMORY_LEAK] malloc(100) = 0xc3225d8 at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:122 (thread: Thread_0x809eb0) <inf> main: 正常分配: 0xc3225d8

说明:

  • 成功拦截malloc调用
  • 正确记录分配地址、大小、文件位置、行号
  • 线程信息正确显示

4.2:内存释放

释放程序:

free(normal_ptr, __FILE__, __LINE__); LOG_INF(\"正常释放\");

插桩反馈

[MEMORY_LEAK] free(0xc3225d8) at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:124<inf> main: 正常释放

说明:

  • 成功拦截free调用
  • 正确记录释放地址、大小、文件位置、行号
  • 正确移除分配记录

4.3:制造内存泄露

制造泄露程序

LOG_INF(\"--- 测试2:制造内存泄露 ---\");static void create_memory_leaks(void){ // 故意泄露1:分配内存但不释放 void* leak1 = malloc(100); LOG_INF(\"分配了100字节内存但不释放: %p\", leak1); // 故意泄露2:分配更多内存但不释放 void* leak2 = calloc(10, 50); LOG_INF(\"分配了500字节内存但不释放: %p\", leak2); // 故意泄露3:重新分配内存但不释放 void* leak3 = malloc(200); void* leak3_new = realloc(leak3, 300); LOG_INF(\"重新分配了300字节内存但不释放: %p\", leak3_new); // 正常的内存操作(对比) void* normal = malloc(150); LOG_INF(\"正常分配150字节内存: %p\", normal); free(normal); LOG_INF(\"正常释放内存\");}

插桩反馈

<inf> main: --- 测2:制造内存泄露 ---MEMORY_LEAK] malloc(100) = 0xc3225d8 at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:28 (thread: Thread_0x809eb0)<inf> main: 分配了100字节内存但不释放: 0xc3225d8[MEMORY_LEAK] malloc(500) = 0xc342908 at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:32 (thread: Thread_0x809eb0)<inf> main: 分配了500节内存但不释放: 0xc342908[MEMORY_LEAK] malloc(200) = 0xc3226f8 at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:36 (thread: Thread_0x809eb0)[MEMORY_LEAK] malloc(300) = 0xc342b08 at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:37 (thread: Thread_0x809eb0)<inf> main: 重新分配了300字节内存但不释放: 0xc342b08[MEMORY_LEAK] malloc(150) = 0xc322750 at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:41 (thread: Thread_0x809eb0)<inf> main: 正常分配150字节内存: 0xc322750[MEMORY_LEAK] free(0xc322750) at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:43

说明:

  • 准确检测到3个故意制造的内存泄漏
  • 显示完整的泄漏信息

4.4:手动打印内存信息

信息打印

static void test_memory_stats(void){ // 先检查泄露,更新统计信息 ml_check_leaks(); // 然后获取最新的统计信息 memory_stats_t stats; ml_get_stats(&stats); LOG_INF(\"=== 内存统计信息 ===\"); LOG_INF(\"总分配次数: %d\", stats.total_allocations); LOG_INF(\"总释放次数: %d\", stats.total_deallocations); LOG_INF(\"当前活跃分配: %d\", stats.current_allocations); LOG_INF(\"当前使用内存总量: %d\", stats.total_memory_used); LOG_INF(\"峰值内存使用: %d\", stats.peak_memory_used); LOG_INF(\"泄露数量: %d\", stats.leak_count); LOG_INF(\"可疑数量: %d\", stats.suspicious_count); LOG_INF(\"错误数量: %d\", stats.error_count); LOG_INF(\"==================\");}

统计反馈

 <inf> main: === 内存统计信息 ===<inf> main: 总分配次数: 6<inf> main: 总释放次数: 3 <inf> main: 当前活跃分配: 3<inf> main: 当前使用内存总量: 900<inf> main: 峰值内存使用: 1050<inf> main: 泄露数量: 3<inf> main: 可疑数量: 0<inf> main: 错误数量: 0<inf> main: ==================

说明:

  • 统计数字准确反映内存使用情况
  • 峰值内存计算正确(100+500+300+150=1050字节)
  • 泄漏和错误计数准确

4.5:制造重复释放

制造重复释放程序

static void create_double_free_error(void){ void* ptr = malloc(100); LOG_INF(\"分配内存用于测试重复释放: %p\", ptr); // 第一次释放(正常) free(ptr); LOG_INF(\"第一次释放内存\"); // 第二次释放(错误!) free(ptr); LOG_INF(\"第二次释放内存(这会导致错误)\");}

插桩反馈

<inf> main: --- 测试5:重复释错误 ---MEMORY_LEAK] malloc(100) = 0xc322768 at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:52 (thread: Thread_0x809eb0)<inf> main: 分配内存用于测试重复释放: 0xc322768[MEMORY_LEAK] free(0xc322768) at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:56<inf> main: 第一次释放内存[MEMORY_LEAK] malloc(20) = 0xc322768 at WEST_TOPDIR/projects/watch_demo/w30_app/main.c:60 (thread: Thread_0x809eb0)Warning !!! ERROR: LEAK_FREE_NULL<inf> main: 第二次释放内存(会导致错误)

说明:

  • 重复释放检测正确
  • 警告输出正确:Warning !!! ERROR: LEAK_FREE_NULL

五、算法优势与特点

5.1 技术优势

  1. 轻量级设计:每个分配仅需71字节开销
  2. 零侵入性:通过宏拦截,无需修改应用代码
  3. 实时检测:支持运行时泄露检测和报告
  4. 线程安全:支持多线程环境
  5. 可配置性:通过Kconfig灵活配置参数

5.2 适用场景

  • 嵌入式系统:资源受限的RTOS环境
  • 实时应用:需要低延迟的内存监控
  • 开发调试:快速定位内存问题
  • 生产监控:长期运行的内存健康监控

总结

本文介绍的小型内存泄露检测算法具有以下特点:

  1. 算法简洁:核心逻辑清晰,易于理解和维护
  2. 资源高效:内存开销小,适合嵌入式环境
  3. 功能完整:支持多种内存错误检测
  4. 易于集成:与现有代码无缝集成
  5. 可扩展性:支持自定义配置和扩展

这种轻量级算法为嵌入式系统提供了一种实用的内存监控解决方案,填补了大型工具无法使用的空白,是嵌入式开发者的有力工具。
详情程序请私信获取!!!!
详情程序请私信获取!!!!
详情程序请私信获取!!!!