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 568
main.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"
#endif
typedef 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,都可以添加到内存监控中去。文章来源:https://uudwc.com/A/EyZZ4
#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)这样的形式,如果是这种情况,可能需要使用者将待排查的内存接口转成宏定义,会比较麻烦。文章来源地址https://uudwc.com/A/EyZZ4