内存泄漏检测原理

原创 2018年04月16日 00:05:18

        众所周知,c/c++语言的内存回收依赖于程序员,并没有python,java之类的自动回收。那么内存是申请释放就是个需要认真对待的问题。因为往往诸如服务器是需要长期运行的,即便轻微的内存泄漏也将可能带来严重问题。而且这种bug还存在着复现周期长,难以定位的问题。

        链接器有个选项–wrap,当查找某个符号时,它优先先解析__wrap_symbol, 解析不到才会去解析symbol函数。也就是比如我编译选项加入-Wl,-wrap,malloc。那么链接器会优先将malloc调用链接至你定义的__wrap_malloc函数,-Wl是指定链接器参数的意思。也就是说我有诸如以下代码,就可以监测程序内存申请释放。当然为了讨论问题本质,我们暂且忽略realloc/calloc等函数。

// wrap malloc
void *__real_malloc(size_t);
void __real_free(void *);

void *__wrap_malloc(size_t size)
{
	trace("malloc: %d bytes\n", size);
	return __real_malloc(size);
}
void __wrap_free(void *ptr)
{
	trace("free: addr %p\n", ptr);	
	return __real_free(ptr);
}

        但是问题来了,虽然我知道申请了多少内存,释放了哪块内存还是不够的,我还得知道哪里申请了内存。那么接下来的主角登场。

        backtrace函数用于获取当前函数的调用堆栈,返回地址信息存放在buffer中。buffer是一个存放void *型返回地址的二级指针,参数 size 指定buffer中可以保存void* 型返回地址的最大条数。函数返回值是buffer中存放的void*指针实际个数,最大不超过参数size大小。

        backtrace_symbols函数把从backtrace函数获取的返回地址转换为描述符号地址的字符串数组,每个地址的符号由函数名,十六进制偏移组成。需要注意的是返回的字符串数组是backtrace_symbols内部申请的内存,记得用完释放掉。

        同时还需要注意的是gcc的非零-O优化选项可能会导致非预期的地址信息丢失,因为会将一些简短函数内联掉。内联函数没有返回地址信息。尾调用优化也可能会导致非预期的返回地址信息丢失。编译选项也应该加入-rdynamic,这样backtrace_symbols才能将那些返回地址转换为字符串符号地址。

int backtrace(void **buffer, int size);

void backtrace_symbols_fd(void *const *buffer, int size, int fd);
// dump statck 
int dumptrace(uint16_t depth)
{
	int nptrs = 0;
	nullptr_t *buffer = malloc(sizeof(nullptr_t) * depth);
  	if( (nptrs = backtrace(buffer, depth)) <= 0)
		return -1;

  	char **strings = NULL;
 	if( (strings = backtrace_symbols(buffer, nptrs)) == NULL){
		return -1;
  	}

	int j = 0;
  	for (j = 0; j < nptrs; j++)
	  	printf("%s\n", strings[j]);

  	free(strings);
	free(buffer);
	return 0;
}

        那是不是说我只要在__wrap_malloc等函数里将没有释放的内存堆栈记录下来,这样就可以了呢?运行你会发现函数出现了递归。那是因为dumptrace里进行了内存申请。也就是说问题的关键在于,__wrap_malloc/__wrap_free内对内存操作记录期间不能继续记录内存操作。那么我们接着使用线程私有变量。ENTER_SAFERECORD先判断标志为0x00时可以记录内存的操作。进入内存操作记录时,将标志置0x01,离开内存操作记录时,再将标志置0x00。其实也就是意味着记录内存操作期间发生的内存申请释放不会被记录下来,所以记录内存操作越快越好。

static pthread_key_t saferecord_key;
void init_pthread(void)
{
	pthread_key_create(&saferecord_key, NULL);
}
void init_saferecord()
{
	static pthread_once_t once = PTHREAD_ONCE_INIT;
	pthread_once(&once, init_pthread);
}
void release_saferecord()
{
	pthread_key_delete(saferecord_key);
}

#define ENTER_SAFERECORD \
	if(pthread_getspecific(saferecord_key) == (void *) 0x00){ \
		pthread_setspecific(saferecord_key, (void *)0x01); 
#define LEAVE_SAFERECORD \
		pthread_setspecific(saferecord_key, (void *)0x00); \
	}
	
// wrap malloc
void *__real_malloc(size_t);
void __real_free(void *);

void *__wrap_malloc(size_t size)
{
	ENTER_SAFERECORD
		dumptrace(16);
	LEAVE_SAFERECORD
		
	return __real_malloc(size);
}
void __wrap_free(void *ptr)
{
	ENTER_SAFERECORD
		dumptrace(16);
	LEAVE_SAFERECORD
		
	return __real_free(ptr);
}

    完整代码如下所示

#include <stdio.h>
#include <execinfo.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <caca_types.h>
#include <pthread.h>

#define trace(fmt, args...) printf("%s:%d " fmt, __FUNCTION__, __LINE__, ##args)
typedef void * nullptr_t;

// dump statck 
int dumptrace(uint16_t depth)
{
	int nptrs = 0;
	nullptr_t *buffer = malloc(sizeof(nullptr_t) * depth);
  	if( (nptrs = backtrace(buffer, depth)) <= 0)
		return -1;

  	char **strings = NULL;
 	if( (strings = backtrace_symbols(buffer, nptrs)) == NULL){
		return -1;
  	}

	int j = 0;
  	for (j = 0; j < nptrs; j++)
	  	printf("%s\n", strings[j]);

  	free(strings);
	free(buffer);
	return 0;
}

// 
static pthread_key_t saferecord_key;
void init_pthread(void)
{
	pthread_key_create(&saferecord_key, NULL);
}
void init_saferecord()
{
	static pthread_once_t once = PTHREAD_ONCE_INIT;
	pthread_once(&once, init_pthread);
}
void release_saferecord()
{
	pthread_key_delete(saferecord_key);
}

#define ENTER_SAFERECORD \
	if(pthread_getspecific(saferecord_key) == (void *) 0x00){ \
		pthread_setspecific(saferecord_key, (void *)0x01); 
#define LEAVE_SAFERECORD \
		pthread_setspecific(saferecord_key, (void *)0x00); \
	}
	
// wrap malloc
void *__real_malloc(size_t);
void __real_free(void *);

void *__wrap_malloc(size_t size)
{
	ENTER_SAFERECORD
		dumptrace(16);
	LEAVE_SAFERECORD
		
	return __real_malloc(size);
}
void __wrap_free(void *ptr)
{
	ENTER_SAFERECORD
		dumptrace(16);
	LEAVE_SAFERECORD
		
	return __real_free(ptr);
}

int main(int argc, char *argv[])
{
	init_saferecord();
	
	char *ptr = malloc(2);
	free(ptr);

	release_saferecord();
	return 0;
}

        完整makefile和执行结果如下所示

obj = $(patsubst %.c, %.o, $(notdir $(wildcard *.c)))
CFLAGS   = -Wall -g  -pthread -rdynamic -Wl,-wrap,malloc -Wl,-wrap,free
CC = gcc
TARGET = main

$(TARGET): $(obj)
	$(CC) $(CFLAGS) -o $@ $^

$(obj): %.o: %.c
	$(CC) $(CFLAGS) -c  $< -o $@

.PHONY: clean
clean:
	-rm -f *.o $(TARGET)
 

C++内存泄露检测原理

转自:http://hi.baidu.com/jasonlyy/item/9ca0cecf2c8f113a99b4981c 本文针对 linux 下的 C++ 程序的内存泄漏的检测方...
  • nodeathphoenix
  • nodeathphoenix
  • 2014-04-29 21:48:27
  • 1118

检测内存泄露的原理

检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,我们就能跟踪每一块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内...
  • windows_nt
  • windows_nt
  • 2013-03-08 21:05:29
  • 3642

Android 内存泄露原理和检测

Android进程的内存管理分析
  • gao878280390
  • gao878280390
  • 2017-02-16 10:53:18
  • 710

检测内存泄漏检测内存泄漏

  • 2010年06月30日 18:25
  • 13KB
  • 下载

一张图看懂memwatch内存泄漏检测原理

  • liyongming1982
  • liyongming1982
  • 2013-09-08 09:45:20
  • 1330

深入剖析Android内存泄露原理

Android 内存泄露分析1. 什么是内存泄露OOM:outOfMemery dvm只有10M,如加载大图片,堆内存空间。2. JVM垃圾回收机制和算法3. 常见的内存泄露场景 非静态内部类的错误使...
  • lovewaterman
  • lovewaterman
  • 2016-12-26 17:53:46
  • 419

[Lua]Lua内存泄露检测原理

lua内存泄露 首先第一点,lua中的内存泄露和我们所说的c/c++中的内存泄露本质上是不一样的。 lua中有垃圾回收机制(GC),所以理论上是不会有内存泄露的。当它进行GC的时候,会从根部开始扫描所...
  • ouyangshima
  • ouyangshima
  • 2015-02-13 15:11:46
  • 3879

IOS内存泄漏检查原理和检查工具介绍

1        概要 由于在测试过程中手机的测试项目越来越多,作者要学习一下IOS环境手机开发的平台的一些相关知识,同时也需要对测试项目做一些代码走读工作。本文是从白盒代码检测的角度介绍一下IOS...
  • liguanshu
  • liguanshu
  • 2013-11-10 20:49:32
  • 1321

valgrind内存泄漏检测详解

一、valgrind介绍 Valgrind是一套Linux下,开放源代码(GPL V2)的仿真调试工具的集合。Valgrind由内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架...
  • justdoithai
  • justdoithai
  • 2016-08-28 20:48:13
  • 912

一个小项目 --- C++实现内存泄漏检查器

思路: 1.内存泄露产生于 new/new[] 操作进行后没有执行 delete/delete[] 2.最先被创建的对象, 其析构函数是最后执行的 解决方法: 1.重载operator new/new...
  • ssopp24
  • ssopp24
  • 2017-08-17 00:27:53
  • 983
收藏助手
不良信息举报
您举报文章:内存泄漏检测原理
举报原因:
原因补充:

(最多只允许输入30个字)