当前位置 博文首页 > kwanson的博客:内存泄漏检测原理

    kwanson的博客:内存泄漏检测原理

    作者:[db:作者] 时间:2021-09-17 15:25

    ????? ? 众所周知,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)
    
    ?
    cs