第一部分:内存泄漏的说明和解决方案的介绍 (转载)
第二部分:实现模拟检查内存泄漏的样例代码 (原创)
内存泄漏的说明和解决方案的介绍
以下文章转载以:https://www.bcdaren.com/557359488538251265/blog_content.html
众所周知,C/C++执行效率高,但难以驾驭,开车一时爽,但稍不留神容易翻车。估计每个C/C++程序员都遭受过内存泄漏的困扰。本文提供一种通过wrap malloc查找memory leak的思路,使得你翻车的时候能够自救,而不至于车毁人亡。=
什么是内存泄漏?
内存泄漏就是动态申请的内存丢失引用,造成没有办法回收它(我知道杠jing要说进程退出前系统会统一回收),相当于在人身上轧个口子,伤口一直流血不止,关键这口子还不知道轧哪儿。
内存泄漏对于客户端应用可能不是什么大事,而对于长久运行的服务器程序则可能是致命的。
隐式释放
Java等傻瓜式编程语言会自动管理内存回收,你只管用,系统会通过引用计数技术跟踪动态分配的每块内存,在合适的时机自动释放掉,这种方式叫隐式释放,相当于车的自动挡。
显式释放
而C/C++需要显式的释放,也就是开发者需要确保malloc/free配对,确保每块申请的内存都恰当的释放掉,相当于车的手动挡。有很多手段可以避免内存泄漏,比如RAII、比如智能指针(大多基于引用计数)、比如内存池。C/C++程序员也是蛮拼的,一直在跟内存泄漏做殊死搏斗。
理论上,只要我们足够小心,在每次申请的时候,都牢记释放,那么这个世界就清净了。但现实往往没有这般美好,比如抛异常了,释放内存的语句执行不到,比如模块之间的指针传递,又或者某菜鸟程序员不小心埋了颗雷,所以,我们必须直面真实的世界,那就是我们会遭遇内存泄漏。
怎么查内存泄漏?
我们可以review代码,double check,结对编程,但从海量代码里找到隐藏的问题,这如同大海捞针,谈何容易啊?兄弟。
所以,我们需要借助工具,比如valgrind,但这些找内存泄漏的工具,往往对你使用动态内存的方式有某种期待,或者说约束,比如常驻内存的对象会被误报出来,然后真正有用的信息会被掩盖在误报的汪洋大海里,所以,很多时候,valgrind根本解决不了日常项目中的问题,并没什么卵用。
很多著名的开源项目,为了能用valgrind跑,都大张旗鼓的修改源代码,从而使得项目符合valgrind的要求,用vargrind跑过没有任何报警叫valgrind干净,这倒也不失为一个一劳永逸的办法。
既然这些个玩意儿都中看不中用,所以,求人不如求己,还得自力更生,多大点事儿。
operator new/delete重载和hook malloc/free
可以通过operator new/delete,operator new[]/delete[]重载,但这里有很细致的功夫,你需要全面了解,而不是贸然行动,建议看看Effective C++,对operator new系列操作符重载有专门的阐述。
你也可以hook malloc、free等c编程接口。
你还可以开启ptmalloc的调试功能,它有时候也能管点用。
什么是动态内存分配器?
动态内存分配器是介于kernel跟应用程序之间的一个函数库,linux glibc提供的动态内存分配器叫ptmalloc,因为抱了linux的大腿,故而是应用最广泛的动态内存分配器。
从kernel角度看,动态内存分配器属于应用程序层;而从应用程序的角度看,动态内存分配器属于系统层。到底属于哪一层,这取决于你的身份和角度。
应用程序可以通过mmap系统调用直接向系统申请动态内存,也可以通过动态内存分配器的malloc接口分配内存,而动态内存分配器会通过sbrk、mmap向系统分配内存,所以应用程序通过free释放的内存,并不一定会真正返还给系统,它也有可能被动态内存分配器缓存起来。
所以当你malloc/free配对得很好,但通过top命令去看进程的内存占用,还是很高,你不必感到惊讶。
google有自己的动态内存分配器tcmalloc,另外jemalloc也是著名的动态内存分配器,他们有不同的性能表现,也有不同的缓存和分配策略。你可以用它们替换linux系统glibc自带的ptmalloc。
new/delete跟malloc/free的关系
new是c++的用法,比如Foo *f = new Foo,其实它分为3步。
- 通过operator new()分配sizeof(Foo)的内存,最终通过malloc分配。
- 在新分配的内存上构建Foo对象。
- 返回新构建的对象地址。
new=分配内存+构造+返回,而delete则是等于析构+free。
所以搞定malloc、free就是从根本上搞定动态内存分配。
chunk
每次通过malloc返回的一块内存叫一个chunk,动态内存分配器是这样定义的,后面我们都这样称呼。
wrap malloc
gcc支持wrap,即通过传递-Wl,--wrap,malloc的方式,可以改变调用malloc的行为,把对malloc的调用链接到自定义的__wrap_malloc(size_t)函数,而我们可以在__wrap_malloc(size_t)函数的实现中通过__real_malloc(size_t)真正分配内存,而后我们可以做搞点小动作。
同样,我们可以wrap free。
malloc跟free是配对的,当然也有其他相关API,比如calloc、realloc、valloc,这些都是细节,根本上还是malloc和free,比如realloc就是malloc + free的组合。
怎么去定位内存泄漏呢?
我们会malloc各种不同size的chunk,也就是每种不同size的chunk会有不同数量,如果我们能够跟踪每种size的chunk数量,那就可以知道哪种size的chunk在泄漏。很简单,如果该size的chunk数量一直在增长,那它很可能泄漏。
光知道某种size的chunk泄漏了还不够,我们得知道是哪个调用路径上导致该size的chunk被分配,从而去检查是不是正确释放了。
怎么跟踪到每种size的chunk数量?
我们可以维护一个全局 unsigned int malloc_map[1024 * 1024]数组,该数组的下标就是chunk的size,malloc_map[size]的值就对应到该size的chunk分配量。
这等于维护了一个chunk size到chunk count的映射表,它足够快,而且可以覆盖到0 ~ 1M大小的chunk的范围,它已经足够大了,试想一次分配一兆的块已经很恐怖了,可以覆盖到大部分场景。
那大于1M的块怎么办呢?我们可以通过log的方式记录下来。
在__wrap_malloc里,++malloc_map[size]
在__wrap_free里,--malloc_map[size]
如此一来,我们便通过malloc_map记录了各size的chunk的分配量。
如何知道释放的chunk的size?
不对,free(void *p)只有一个参数,我如何知道释放的chunk的size呢?怎么办?
我们通过在__wrap_malloc(size_t)的时候,分配8+size的chunk,也就是额外分配8字节,用起始的8字节存储该chunk的size,然后返回的是(char*)chunk + 8,也就是偏移8个字节地址,返回给调用malloc的应用程序。
这样在free的时候,传入参数void* p,我们把p往前移动8个字节,解引用就能得到该chunk的大小,而该大小值就是之前在__wrap_malloc的时候设置的size。
好了,我们真正做到记录各size的chunk数量了,它就存在于malloc_map[1M]的数组中,假设64个字节的chunk一直在被分配而没有被正确回收,最终会表现在malloc_map[size]数值一直在增长,我们觉得该size的chunk很有可能泄漏,那怎么定位到是哪里调用过来的呢?
如何记录调用链?
我们可以维护一个toplist数组,该数组假设有10个元素,它保存的是chunk数最大的10种size,这个很容易做到,通过对malloc_map取top 10就行。
然后我们在__wrap_malloc(size_t)里,测试该size是不是toplist之一,如果是的话,那我们通过glibc的backtrace把调用堆栈dump到log文件里去。
注意:这里不能再分配内存,所以你只能使用backtrace,而不能使用backtrace_symbols,这样你只能得到调用堆栈的符号地址,而不是符号名。
如何把符号地址转换成符号名,也就是对应到代码行呢?答案是addr2line。
addr2line
addr2line工具可以做到,你可以追查到调用链,进而定位到内存泄漏的问题。
至此,恭喜你,你已经get到了整个核心思想。
当然,实际项目中,我们做的更多,我们不仅仅记录了toplist size,还记录了各size chunk的增量toplist,会记录大块的malloc/free,会wrap更多的API。
总结
通过wrap malloc/free + backtrace + addr2line,你就可以定位到内存泄漏了。
美好的时间过得太快,又是时候说byebye!
实现模拟检查内存泄漏的样例代码:
#ifndef __MALLOC_MAP_H__
#define __MALLOC_MAP_H__
#ifndef NULL
#define NULL (void *)0
#endif
#define TOPLIST_SIZE (10)
struct __malloc_map {
size_t dump_boundary; /**< 出现dump时的动态内存块数量边界 */
struct __malloc_node *ptr_list; /**< 维护动态分配的内存链表 */
struct __malloc_node *toplist[TOPLIST_SIZE]; /**< 维护最大内存块数量top的前10个 */
};
extern void *__wrap_malloc(size_t size);
extern void *__wrap_calloc(size_t size);
extern void *__wrap_realloc(void *mem_address, size_t newsize);
extern void __wrap_free(void *ptr);
extern void malloc_map_dump(void);
extern void malloc_map_show_toplist(void);
extern int malloc_map_set_dump_boundary(size_t dump_boundary);
#define malloc(size) __wrap_malloc(size)
#define calloc(size) __wrap_calloc(size)
#define realloc(mem_address, newsize) __wrap_realloc(mem_address, newsize)
#define free(size) __wrap_free(size)
#endif /* __MALLOC_MAP_H__ */
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
struct __malloc_node {
struct __malloc_node *ptr_next;
size_t chunk_count;
size_t chunk_size;
};
#include "mallocmap.h"
#define BT_BUF_SIZE (100)
#define MALLOC_MAP_INIT \
{ .ptr_list = NULL, .dump_boundary = 0, .toplist = {NULL} }
static int sg_interrupted = 0;
static struct __malloc_map sg_malloc_map = MALLOC_MAP_INIT;
void *__real_malloc(size_t size);
void __real_free(void *ptr);
char *get_name_by_pid(pid_t pid, char *task_name)
{
FILE* fp = NULL;
char proc_pid_path[1024] = {0};
char buf[1024] = {0};
sprintf(proc_pid_path, "/proc/%d/status", pid);
fp = fopen(proc_pid_path, "r");
if (NULL == fp) {
return "";
}
if (NULL != fgets(buf, 1024 - 1, fp)) {
sscanf(buf, "%*s %s", task_name);
}
fclose(fp);
return task_name;
}
/* 若找到,则存放到ptr_cur指针中,返回ptr_cur的之前指针 */
static struct __malloc_node *malloc_node_find_chunk_size(struct __malloc_node *ptr_list,
size_t chunk_size, struct __malloc_node **ptr_cur)
{
struct __malloc_node *ptr_pre = ptr_list;
struct __malloc_node *ptr_tmp = ptr_list;
while (NULL != ptr_tmp) {
if (ptr_tmp->chunk_size == chunk_size) {
(*ptr_cur) = ptr_tmp;
break;
}
ptr_pre = ptr_tmp;
ptr_tmp = ptr_tmp->ptr_next;
}
return ptr_pre;
}
static int malloc_node_insert(struct __malloc_node **pptr_list,
size_t chunk_count, size_t chunk_size)
{
struct __malloc_node *ptr_node = NULL;
ptr_node = (struct __malloc_node *)__real_malloc(sizeof(struct __malloc_node));
if (NULL == ptr_node) {
return -1;
}
ptr_node->chunk_count = chunk_count;
ptr_node->chunk_size = chunk_size;
ptr_node->ptr_next = NULL;
(*pptr_list) = ptr_node;
return 0;
}
static void malloc_node_bubble_sort(struct __malloc_node *ptr_list)
{
struct __malloc_node *p = NULL;
struct __malloc_node *q = NULL;
struct __malloc_node tmp;
for (p = ptr_list; p->ptr_next != NULL; p = p->ptr_next){
for (q = p; q->ptr_next != NULL; q = q->ptr_next){
if (q->chunk_count < q->ptr_next->chunk_count){
tmp.chunk_count = q->chunk_count;
tmp.chunk_size = q->chunk_size;
q->chunk_count = q->ptr_next->chunk_count;
q->chunk_size = q->ptr_next->chunk_size;
q->ptr_next->chunk_count = tmp.chunk_count;
q->ptr_next->chunk_size = tmp.chunk_size;
}
}
}
}
void malloc_map_dump(void)
{
int idx = 0;
int nptrs = 0;
char task_name[1024] = {0};
char execmd[BT_BUF_SIZE] = {0};
void *buffer[BT_BUF_SIZE];
/* buffer里面存放的是每层调用函数的返回地址 */
nptrs = backtrace(buffer, BT_BUF_SIZE);
printf("******** malloc_map_dump (PID:%u) *********\n", (unsigned int)getpid());
for (idx = 0; idx < nptrs; idx++) {
printf("backtrace(%d) addresses: %p\n", idx, buffer[idx]);
snprintf(execmd, sizeof(execmd), "./addr2line -e %s %p -f", \
get_name_by_pid(getpid(), task_name), buffer[idx]);
system(execmd);
}
printf("******** malloc_map_dump (end) *********\n");
exit(-1);
return;
}
void malloc_map_dump_fd(int fd)
{
int nptrs = 0;
void *buffer[BT_BUF_SIZE];
/* buffer里面存放的是每层调用函数的返回地址 */
nptrs = backtrace(buffer, BT_BUF_SIZE);
if (nptrs > 0) {
backtrace_symbols_fd(buffer, BT_BUF_SIZE, fd);
}
return;
}
static int malloc_map_del_list(struct __malloc_map *ptr_map, size_t chunk_size)
{
struct __malloc_node *ptr_cur = NULL;
struct __malloc_node *ptr_pre = NULL;
ptr_pre = malloc_node_find_chunk_size(ptr_map->ptr_list, chunk_size, &ptr_cur);
if (NULL == ptr_cur || NULL == ptr_pre) {
fprintf(stderr, "ptr_cur or ptr_pre is null, code=%d (%s)", errno, strerror(errno));
return -1;
}
if (ptr_cur->chunk_count > 0) {
ptr_cur->chunk_count--;
}
if (0 == ptr_cur->chunk_count && NULL != ptr_pre) {
if (ptr_pre == ptr_cur) {
ptr_map->ptr_list = NULL;
} else {
ptr_pre->ptr_next = ptr_cur->ptr_next;
}
__real_free(ptr_cur);
}
return 0;
}
static int malloc_map_add_list(struct __malloc_map *ptr_map, size_t chunk_size)
{
struct __malloc_node *ptr_cur = NULL;
struct __malloc_node *ptr_pre = NULL;
if (NULL == ptr_map->ptr_list) {
malloc_node_insert(&(ptr_map->ptr_list), 1, chunk_size);
} else {
ptr_pre = malloc_node_find_chunk_size(ptr_map->ptr_list, chunk_size, &ptr_cur);
if (NULL != ptr_cur) { // 找到与chunk_size相同的指针
ptr_cur->chunk_count++;
// 分配的chunk数量大于dump_boundary则dump出栈调用流程
if (ptr_cur->chunk_count > ptr_map->dump_boundary) {
malloc_map_dump();
}
} else if (NULL != ptr_pre) { // 没找到插入到链表尾部
malloc_node_insert(&(ptr_pre->ptr_next), 1, chunk_size);
}
}
return 0;
}
static void malloc_map_update_toplist(struct __malloc_map *ptr_map)
{
int idx = 0;
struct __malloc_node *p = NULL;
malloc_node_bubble_sort(ptr_map->ptr_list);
for (idx = 0, p = ptr_map->ptr_list; idx < TOPLIST_SIZE && NULL != p; idx++, p = p->ptr_next) {
ptr_map->toplist[idx] = p;
//printf("__wrap_flush_toplist idx=%u, chunk_size=%u\n", idx, p->chunk_size);
}
}
void malloc_map_show_toplist(void)
{
int idx = 0;
char task_name[1024] = {0};
struct __malloc_map *ptr_map = NULL;
ptr_map = &sg_malloc_map;
if (ptr_map->ptr_list) {
system("clear");
printf("toplist(boundary:%d):\n", ptr_map->dump_boundary);
printf("PID\tADDRESS\tSIZE\tCOUNT\tPNAME\n");
for (idx = 0; idx < TOPLIST_SIZE; idx++) {
if (NULL != ptr_map->toplist[idx]) {
printf("%u\t%p\t%u\t%u\t%s\n", (int)getpid(), \
ptr_map->toplist[idx] + sizeof(size_t), \
ptr_map->toplist[idx]->chunk_size - sizeof(size_t), \
ptr_map->toplist[idx]->chunk_count,
get_name_by_pid(getpid(), task_name));
}
}
printf("\n");
}
}
int malloc_map_set_dump_boundary(size_t dump_boundary)
{
sg_malloc_map.dump_boundary = dump_boundary;
return dump_boundary;
}
void *__wrap_malloc(size_t size)
{
size_t chunk_size = 0;
void *ptr_chunk = NULL;
chunk_size = size + sizeof(size_t);
if (NULL == (ptr_chunk = __real_malloc(chunk_size))) {
fprintf(stderr, "ptr_chunk is null, code=%d (%s)", errno, strerror(errno));
return NULL;
}
memcpy(ptr_chunk, &size, sizeof(size_t));
//printf("__wrap_malloc size=%d, (size_t *)ptr_chunk=%d\n", size, *(size_t *)ptr_chunk);
malloc_map_add_list(&sg_malloc_map, chunk_size);
malloc_map_update_toplist(&sg_malloc_map);
return ptr_chunk + sizeof(size_t);
}
void *__wrap_calloc(size_t size)
{
void *ptr_data = NULL;
if (NULL == (ptr_data = __wrap_malloc(size))) {
return NULL;
}
memset(ptr_data, 0x00, size);
return ptr_data;
}
void *__wrap_realloc(void *mem_address, size_t newsize)
{
size_t real_size = 0;
void *ptr_data = NULL;
real_size = *(size_t *)(mem_address - sizeof(size_t));
if (NULL == (ptr_data = __wrap_malloc(newsize))) {
return NULL;
}
memcpy(ptr_data, mem_address, real_size);
return ptr_data;
}
void __wrap_free(void *ptr)
{
size_t chunk_size = 0;
void *ptr_chunk = NULL;
if (NULL != ptr) {
ptr_chunk = ptr - sizeof(size_t);
chunk_size = (*(size_t *)(ptr_chunk)) + sizeof(size_t);
__real_free(ptr_chunk);
malloc_map_del_list(&sg_malloc_map, chunk_size);
}
}
static void abnormal_andler(int signo)
{
printf("abnormal_andler signo:%d\n", signo);
sg_interrupted = 1;
malloc_map_dump();
exit(0);
}
//
void test_func_xxx(void)
{
int idx = 0;
char *ptr = NULL;
for (idx = 0; idx < 20; idx++) {
ptr = __wrap_malloc(1024 + idx);
if (3 == idx) {
__wrap_free(ptr);
}
if (1 == idx) {
__wrap_free(ptr);
}
}
}
void test_func_yyy(void)
{
int idx = 0;
char *ptr = NULL;
for (idx = 0; idx < 20; idx++) {
ptr = __wrap_malloc(1024 + idx);
if (5 == idx) {
__wrap_free(ptr);
}
if (1 == idx) {
__wrap_free(ptr);
}
}
}
int testmain(void)
{
printf("wrap_malloc main\n");
malloc_map_set_dump_boundary(8);
test_func_xxx();
test_func_yyy();
signal(SIGSEGV, abnormal_andler);
signal(SIGABRT, abnormal_andler);
while (!sg_interrupted) {
malloc_map_show_toplist();
test_func_xxx();
__wrap_malloc(1024);
usleep(1000*3000);
}
return 0;
}
//
CFLAGS := -g -Wall -D__GNU__ $(MYCFLAGS) -D_GNU_SOURCE -D__USE_XOPE -mfloat-abi=hard
WRAPFUNC := -rdynamic -funwind-tables -ffunction-sections -Wl,--wrap=malloc -Wl,--wrap=free -Wl,--wrap=calloc -Wl,--wrap=realloc
CFLAG_TARGET := $(CFLAGS)
CFLAG_TARGET += -Wl,-rpath-link $(TARGET_LIB_DIR)
CFLAGS += -I./include/
SOURCES := $(wildcard *.c) $(wildcard $(DIR_EXBOARD)/*.c $(DIR_DB)/*.c \
$(DIR_RS485)/*.c $(DIR_XYBASE)/*.c $(DIR_NET)/*.c $(DIR_READER)/*.c )
OBJS := $(patsubst %.c,%.o,$(SOURCES))
DEPS := $(patsubst %.o,%.d,$(OBJS))
MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))
MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \
$(patsubst %.d,%.cc,$(MISSING_DEPS)))
CPPFLAGS += -MD
TARGET := mallocmap
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAG_TARGET) -o $(TARGET_SERVICE_DIR)/$(TARGET) $(OBJS) $(WRAPFUNC) $(LIB)
cp $(TARGET_SERVICE_DIR)/$(TARGET) $(DEST_PATH)/$(TARGET)_$(LOGNAME) -rf
$(OBJS): %.o: %.c $(DEP)
$(CC) $(CFLAGS) $(WRAPFUNC) -c $< -o $(subst ../,,$@)
clean:
rm -rf $(subst ../,,$(OBJS))
rm -rf *.gcno
cleanall:
rm -rf $(subst ../,,$(OBJS))
rm -rf $(TARGET_SERVICE_DIR)/$(TARGET)
rm -rf *.h~
rm -rf *.c~
rm -rf *.d