内存泄露 手写内存泄露检测工具

目录

引言

一.内存泄露是什么?

1.内存泄露检测概述

2.内存泄露判断方法

二.我们介绍代码的实现来检测内存泄露

1.宏定义方式检测本文件中是否有内存泄露发生

2.通过hook的方式来是实现malloc和free检测内存泄露 适用于多文件和单文件


引言

作为C/C++相关开发者通常会面对一个诟病那就是内存泄露,我们通常可以写一块检测内存泄露的工具来帮助我们去找到哪个文件中哪一行发生了内存泄露

一.内存泄露是什么?

1.内存泄露检测概述

①.内存泄露在面试和工作中常见,尤其是C和C++项目。

扫描二维码关注公众号,回复: 17596817 查看本文章

②.内存泄露的核心原因是调用了malloc但没有调用free,导致堆内存无法被释放。

③.内存泄露的判断方法和泄露位置的定位是解决内存泄露的关键。

2.内存泄露判断方法

①.通过htop等工具观察虚拟内存增长,推测内存泄露。

②.通过打印信息判断内存分配和释放情况。

③.通过hook机制和自定义内存分配函数来精确判断内存泄露。

二.我们介绍代码的实现来检测内存泄露

#define _GNU_SOURCE //这个b玩意要放所有头文件最上面
#include <dlfcn.h>
#include<link.h>

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>



int flag = 1;
#if 0
void * nMalloc(size_t size,const char *filename,int line){
    void *p =malloc(size);

    if(flag){
        char buff[128]={0};
        snprintf(buff,128,"./mem/%p.mem",p);
     //  使用 snprintf 将文件路径格式化到 buff 中,
     //文件路径包括内存地址(p),并将该内存地址存储在“./mem/”目录下,以.p作为扩展名
    
        FILE *fp =fopen(buff,"w");
        if(!fp){ 
            free(p);
            return NULL;
        }
        //如果文件成功打开,写入内存地址和分配大小的信息到文件中
        fprintf(fp,"[+]%s:%d,addr:%p,size:%ld\n",filename,line,p,size);
        fflush(fp);//刷新文件内容到磁盘
        fclose(fp);  
    }

    //printf("nMalloc: %p,size: %ld\n",p,size);
    return p;
}

void nFree(void * ptr){
    if(flag){
        char buff[128]={0};
        snprintf(buff,128,"./mem/%p.mem",ptr);
    
        if(unlink(buff)<0){
            printf("double free :%p",ptr);
            return ;
        }
        //unlink 是一个用于删除文件的系统调用。它会删除路径为 buff 的文件,即在 buff 中存储的文件路径
    }

    //printf("nFree:%p,\n",ptr);
    return free(ptr);
}



#define malloc(size)  nMalloc(size,__FILE__,__LINE__)
#define free(ptr)   nFree(ptr)
//针对单文件


#else

//hook
typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;

typedef void *(*free_t)(void *ptr);
free_t free_f = NULL;

int malloc_enable = 1;
int free_enable = 1;



void *ConvertToELF(void *addr) {

	Dl_info info;
	struct link_map *link;
	
	dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP);

	return (void *)((size_t)addr - link->l_addr);
}





void * malloc(size_t size){
    void*p=NULL;
    if(malloc_enable){
        malloc_enable=0;
        p=malloc_f(size);

        void  *caller =__builtin_return_address(0);//0返回上一级 //1 返回上一级的上一级
        //gcc自带    

        char buff[128]={0};
        snprintf(buff,128,"./mem/%p.mem",p);
    
        FILE *fp =fopen(buff,"w");
        if(!fp){ 
            free(p);
            return NULL;
        }
        // fprintf(fp,"[+]%p,addr:%p,size:%ld\n",caller,p,size);//适合16.4.0版本的ubuntu
        fprintf(fp,"[+]%p,addr:%p,size:%ld\n",ConvertToELF(caller),p,size);
        fflush(fp); 

        malloc_enable=1;    
    }else{
        p=malloc_f(size);
    }
    return p;
}

//★addr2line工具
//addr2line -f -e ./memlead -a 地址

void free(void*ptr){

    if(free_enable){
        free_enable=0;
        char buff[128]={0};
        snprintf(buff,128,"./mem/%p.mem",ptr);
    
        if(unlink(buff)<0){
            printf("double free :%p",ptr);
            return ;
        }

        free_f(ptr); 
        free_enable=1;
    }else{
        free_f(ptr); 
    } 
    return ;
}

void init_hook(void){
    if(!malloc_f){
        malloc_f=(malloc_t)dlsym(RTLD_NEXT,"malloc");//夺舍malloc
    }
    if(!free_f){
        free_f=(free_t)dlsym(RTLD_NEXT,"free");//夺舍free
    }
}

#endif



int main(){
    init_hook();
#if 1
    void *p1 =malloc(5);
    void *p2 =malloc(10);//定位到这
    void *p3 =malloc(15);
    void *p4 =malloc(10);
    

    free(p1);
    free(p3);
    free(p4);
#else

void *p1 =nMalloc(5);
void *p2 =nMalloc(10);
void *p3 =nMalloc(15);

nFree(p1);
nFree(p3);

#endif
// getchar();会产生一个1024的内存
}

1.宏定义方式检测本文件中是否有内存泄露发生

int flag = 1;
#if 1
void * nMalloc(size_t size,const char *filename,int line){
    void *p =malloc(size);

    if(flag){
        char buff[128]={0};
        snprintf(buff,128,"./mem/%p.mem",p);
     //  使用 snprintf 将文件路径格式化到 buff 中,
     //文件路径包括内存地址(p),并将该内存地址存储在“./mem/”目录下,以.p作为扩展名
    
        FILE *fp =fopen(buff,"w");
        if(!fp){ 
            free(p);
            return NULL;
        }
        //如果文件成功打开,写入内存地址和分配大小的信息到文件中
        fprintf(fp,"[+]%s:%d,addr:%p,size:%ld\n",filename,line,p,size);
        fflush(fp);//刷新文件内容到磁盘
        fclose(fp);  
    }

    //printf("nMalloc: %p,size: %ld\n",p,size);
    return p;
}

void nFree(void * ptr){
    if(flag){
        char buff[128]={0};
        snprintf(buff,128,"./mem/%p.mem",ptr);
    
        if(unlink(buff)<0){
            printf("double free :%p",ptr);
            return ;
        }
        //unlink 是一个用于删除文件的系统调用。它会删除路径为 buff 的文件,即在 buff 中存储的文件路径
    }

    //printf("nFree:%p,\n",ptr);
    return free(ptr);
}



#define malloc(size)  nMalloc(size,__FILE__,__LINE__)
#define free(ptr)   nFree(ptr)
//针对单文件

当检测到内存泄露的时候 会生成文本文件 里面打印了内存泄露的文件名和对应的行号

缺点就是只能使用于单文件 在多文件中无法使用

2.通过hook的方式来是实现malloc和free检测内存泄露 适用于多文件和单文件

void * malloc(size_t size){
    void*p=NULL;
    if(malloc_enable){
        malloc_enable=0;
        p=malloc_f(size);

        void  *caller =__builtin_return_address(0);//0返回上一级 //1 返回上一级的上一级
        //gcc自带    

        char buff[128]={0};
        snprintf(buff,128,"./mem/%p.mem",p);
    
        FILE *fp =fopen(buff,"w");
        if(!fp){ 
            free(p);
            return NULL;
        }
        // fprintf(fp,"[+]%p,addr:%p,size:%ld\n",caller,p,size);//适合16.4.0版本的ubuntu
        fprintf(fp,"[+]%p,addr:%p,size:%ld\n",ConvertToELF(caller),p,size);
        fflush(fp); 

        malloc_enable=1;    
    }else{
        p=malloc_f(size);
    }
    return p;
}

//★addr2line工具
//addr2line -f -e ./memlead -a 地址

void free(void*ptr){

    if(free_enable){
        free_enable=0;
        char buff[128]={0};
        snprintf(buff,128,"./mem/%p.mem",ptr);
    
        if(unlink(buff)<0){
            printf("double free :%p",ptr);
            return ;
        }

        free_f(ptr); 
        free_enable=1;
    }else{
        free_f(ptr); 
    } 
    return ;
}

当我们调用打印相关函数的时候也会调用malloc函数 容易产生递归 所以我们要定义标志位enable来结束递归从而达到我们想要的效果

void  *caller =__builtin_return_address(0);//0返回上一级 //1 返回上一级的上一级

这个接口可以让我们找到上一级的地址

0:返回上一级

1:返回上一级的上一级

以此类推

void *ConvertToELF(void *addr) {

	Dl_info info;
	struct link_map *link;
	
	dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP);

	return (void *)((size_t)addr - link->l_addr);
}

在高版本的ubuntu中我们还需要实现一个地址转换接口 把十六进制的地址转换成addr2line工具可以识别的十六进制地址

编译指令

执行之后生成了一个文本文件

我们拿取转换后可以被识别的地址进行追踪

获取结果

结果显示我们在memleak.c文件中的第156行出现了内存泄露 我们定位追踪到该行然后修改bug即可