1、原理
在编写复杂代码的时候,有时一大意就会忘了释放申请的内存;或是调试前人代码时,发现有内存泄漏,这些情况排查起来相当麻烦。这里基于RT-Thread写了一个内存泄漏定位工具(实际和RTT无关,什么系统都可以用,要适当修改),原理非常简单:申请内存时,记录申请的内存地址、大小,以及申请内存这行代码所在的文件名和行号,当释放内存时,根据内存地址找到之前的记录并删除,最后留下的记录就极有可能是发生内存的代码(当然有些内存是常驻的,需要使用者自己辨别)。
linux下有mtrace库,它的原理也是类似,利用钩子函数(hook)在内存申请的地方插入一段代码,这段代码记录了内存申请的信息,包含了申请时的PC指针等,在回溯时可以根据这些地址反推出申请的代码位置;而本文的实现则是靠__func__和__LINE__宏直接记录调用的位置信息,虽然方便,但也有一定的局限。
2、使用方法
demo程序如下,使用时,需要做两件事:
- 将原先使用的内存申请和释放接口分别复制给MEM_USR_MALLOC和MEM_USR_FREE,demo里使用的是rt_malloc,根据情况而定,并包含头文件memtrace.h。
- 将要待排查的内存接口换成MEM_TRACE_MALLOC和MEM_TRACE_FREE。
#include <rtthread.h>#define MEM_USR_MALLOC rt_malloc
#define MEM_USR_FREE rt_free
#include "memtrace.h"int main(void)
{void *p = RT_NULL;p = MEM_TRACE_MALLOC(1024);MEM_TRACE_FREE(p);p = MEM_TRACE_MALLOC(568);rt_thread_mdelay(100);p = MEM_TRACE_MALLOC(8);return 0;
}
demo里申请了三段内存,第一段内存及时释放了,第二、三段内存申请后没有释放,造成了内存泄漏。此时用memtrace命令可以看到泄漏的位置:
msh />memtrace--------------------Memory Trace Info-------------------------file line addr size main.c 14 0x80d264a8 568main.c 18 0x80b6d158 8
3、源码
memtrace.c,这里有两个可调的变量MEM_TRACE_MAX和MEM_TRACE_FILE_NAME_LEN,MEM_TRACE_MAX是最大可记录的内存记录数目,默认值100,记录所用的空间是编译时预先分配的静态内存,所以这个组件只适合出问题的时候调试用。申请内存时会记录代码所在的文件名,MEM_TRACE_FILE_NAME_LEN就是文件名的最大长度,最终记录的只有最后级的文件名,不包含路径,所以16字节应该是够用的。
#include "rtthread.h"
#include "memtrace.h"#define MEM_TRACE_MAX 100
#define MEM_TRACE_FILE_NAME_LEN 16#ifndef RT_USING_MUTEX
#error "please define RT_USING_MUTEX"
#endiftypedef struct
{rt_list_t node;char file[MEM_TRACE_FILE_NAME_LEN];rt_uint16_t line;rt_uint8_t res[2];rt_uint32_t size;void *addr;
}mem_record_t;typedef struct
{rt_uint32_t init;rt_uint32_t used;rt_uint32_t total;struct rt_mutex lock;
}mem_info_t;static mem_info_t mem_info;
static mem_record_t mem_node[MEM_TRACE_MAX];
static rt_list_t free_list;
static rt_list_t used_list;rt_err_t mem_trace_add(char *file, rt_uint16_t line, rt_uint32_t size, void *addr)
{mem_record_t *record = RT_NULL;rt_uint32_t index = 0, len = 0;rt_mutex_take(&mem_info.lock, RT_WAITING_FOREVER);if(rt_list_isempty(&free_list)){rt_kprintf("please increase MEM_TRACE_MAX\n");rt_mutex_release(&mem_info.lock);return -RT_EEMPTY;}record = (mem_record_t*)free_list.next;rt_list_remove((rt_list_t*)record);/* find last '/' */len = rt_strlen(file);for(index = len - 1; index != 0; index--){if(file[index] == '/'){index++;break;}}rt_strncpy(record->file, &file[index], len - index);record->line = line;record->size = size;record->addr = addr;rt_list_insert_before(&used_list, (rt_list_t*)record);rt_mutex_release(&mem_info.lock);return RT_EOK;
}rt_err_t mem_trace_del(void *addr)
{rt_list_t *list_node = RT_NULL;mem_record_t *record = RT_NULL;int find = 0;if(rt_list_isempty(&used_list)){rt_kprintf("no memory in use!\n");return -RT_EEMPTY;}rt_mutex_take(&mem_info.lock, RT_WAITING_FOREVER); rt_list_for_each(list_node, (&used_list)){record = (mem_record_t*)list_node;if(record->addr == addr){find = 1;break;} }if(find){rt_list_remove(&(record->node));rt_list_insert_before(&free_list, &(record->node));}else{rt_kprintf("mem_trace_del err,can't find addr:0x%p\n", addr);}rt_mutex_release(&mem_info.lock);return find ? RT_EOK : -RT_ERROR;
}static int cmd_print_memtrace(int argc, char **argv)
{ rt_list_t *list_node = RT_NULL;mem_record_t *record = RT_NULL;if(rt_list_isempty(&used_list)){rt_kprintf("no memory in use!\n");return 0;}rt_kprintf(" --------------------Memory Trace Info-------------------------\n");rt_kprintf(" file line addr size \n");rt_mutex_take(&mem_info.lock, RT_WAITING_FOREVER);rt_list_for_each(list_node, (&used_list)){record = (mem_record_t*)list_node;rt_kprintf("%16.16s %5d 0x%08p %d\n", record->file, record->line, record->addr, record->size);}rt_mutex_release(&mem_info.lock);return 0;
}
MSH_CMD_EXPORT_ALIAS(cmd_print_memtrace, memtrace, print memory trace info);static int mem_trace_init(void)
{rt_uint32_t i = 0;rt_err_t err = 0;rt_memset(mem_node, 0 , sizeof(mem_node));rt_memset(&mem_info, 0 , sizeof(mem_info));err = rt_mutex_init(&mem_info.lock, "mem_trace", RT_IPC_FLAG_PRIO);if(err){rt_kprintf("mem_trace_init fail!\n");return -1;}mem_info.init = 1;mem_info.used = 0;mem_info.total = MEM_TRACE_MAX;rt_list_init(&free_list);rt_list_init(&used_list);for(i = 0; i < MEM_TRACE_MAX; i++){rt_list_insert_before(&free_list, &mem_node[i].node);}return 0;
}
INIT_ENV_EXPORT(mem_trace_init);
memtrace.h,没错,这个头文件没有用“#ifndef …… #define”这样的写法,这样有个好处是MEM_TRACE_MALLOC宏可以随着MEM_USR_MALLOC宏变化,即这个组件能用同时监控不同的内存接口,例如test_a.c里用的是malloc,test_b.c里用的是rt_malloc.c,都可以添加到内存监控中去。
#define MEM_TRACE_MALLOC(size) \
({ \void *addr = RT_NULL; \addr = MEM_USR_MALLOC(size); \if(addr != RT_NULL) \mem_trace_add(__FILE__, __LINE__, size, addr); \addr; \
})#define MEM_TRACE_FREE(addr) \
({ \MEM_USR_FREE(addr); \mem_trace_del(addr); \
})extern rt_err_t mem_trace_add(char *file, rt_uint16_t line, rt_uint32_t size, void *addr);
extern rt_err_t mem_trace_del(void *addr);
4、局限性
有些模块可能会进一步封装内存申请的接口,而memtrace只支持void* malloc(size_t size)这样的形式,如果是这种情况,可能需要使用者将待排查的内存接口转成宏定义,会比较麻烦。