Linux下C语言内存泄漏检测组件的实现

目录

一、内存泄漏

        1、内存泄漏的定义

        2、如何检测内存泄漏

二、内存泄漏检测的实现(宏定义版)

三、内存泄漏检测的实现(函数指针版)

四、一些问题

1、线上内存泄漏怎么办

2、malloc都有日志,这样性能是不是会很低


一、内存泄漏

        1、内存泄漏的定义

        内存泄漏(Memory Leak)是编程中的一个常见问题,特别是在使用如C或C++这类需要手动管理内存的语言时。它指的是程序中已分配的内存由于某种原因程序未释放或无法释放,导致系统无法再次使用这部分内存。随着时间的推移,内存泄漏可能会导致程序占用的内存越来越多,最终可能耗尽系统资源,影响程序的性能,甚至导致程序崩溃。

        2、如何检测内存泄漏

        当我们只运行程序,不会有任何反馈给我们内存泄漏的信息,因此需要我们自己进行寻找。举个简单的例子。

//利用宏定义来使用我们自己定义的函数
#define malloc(size) nmalloc(size)
#define free(ptr) nfree(ptr)

void *nmalloc(size_t size)
{
    void * ptr = malloc(size);    
    printf("malloc\n");    //每次开辟都要输出信息
    return ptr;
}

void nfree(void * ptr)
{
    free(ptr);
    printf("free\n");    //每次free掉都要打印信息
}

int main()
{
    size_t size =5;
    void * p1 = malloc(size);
    void * p2 = malloc(size * 2);
    void * p3 = malloc(size * 3);

    free(p1);
    free(p3);
}
//当我们通过这个简单的例子可以看出,哪个没有被释放掉

二、内存泄漏检测的实现(宏定义版)

        对于上面的简单例子,我们可以得知,如果我们将每次开辟空间的信息存放起来,将他们开辟的空间地址放到文件中,然后每次释放内存的时候都将这个文件信息进行删除操作。这样我们就可以得到哪个内存没有被释放。

        具体实现不难,我们主要是将这块内存,他的地址在哪,他在哪个函数中开辟的,他在程序中是第几行,他的大小是多少,主要是讲这些信息存放起来。

//利用宏定义来使用我们自己定义的函数
#define malloc(size) nmalloc(size)
#define free(ptr) nfree(ptr)

void *nmalloc(size_t size,const char * filename,const char * funcname,int lins)
{
    void * ptr = malloc(size);
    char  buff[128]={0};
    snprintf(buff,128,"./bloc/%p.mem",ptr);    //首先创建一个目录,然后以这个信息为名称作为文件放入到目录中去。

    FILE * fp = fopen(buff,"w");
    if(!fp)
    {
        free(ptr);    //如果开辟失败就free掉
        return NULL;
    }
    //下面就是存放具体的信息,返回所在函数,具体行数等等,通过这些我们就可以定位内存泄漏的位置
    fprintf(fp,"[+] malloc [%s:%s:%d] %p,size = %ld\n",filename,funcname,lins,ptr,size);
    fflush(fp); //write
    fclose(fp);
    return ptr;
}

void nfree(void * ptr,const char * filename,const char * funcname,int lins)
{
    char  buff[128]={0};
    snprintf(buff,128,"./bloc/%p.mem",ptr);

    if(unlink(buff)>0)   //通过搜寻,并且删除的操作
    {
        printf("double free: %p\n", ptr);
        return ;
    }

    return free(ptr);
}

三、内存泄漏检测的实现(函数指针版)

        当然我们可以使用函数指针实现,并且具体的操作与宏定义大都相同,一些细节需要注意。

        首先我们要重写malloc这个函数,让系统调用我们的malloc,然后通过函数指针指向系统的malloc(会在后面列出)。

        但是此时有个情况发生,我们通过malloc来调用这个函数的时候,会经过printf这个函数,但是当系统第一次调用printf的时候会在printf内部中通过调用malloc来开辟一块内存,这样就成递归操作了,程序会挂掉。

void * malloc(size_t size)
{
    printf("malloc");
}

int main()
{
    void * p1 = malloc(5);
    void * p2 = malloc(5);
    void * p3 = malloc(5);
}

//如果程序正常运行的话 p1的地址是0100
//然而调用一次printf后 p2的地址为4300
//但是p3的地址为4400,也就是说p2和p3之间是连续的,但是p1和p2之间并不是
//因为其中包含了printf的一次malloc

         因此我们需要通过自己来判断是否为第一次调用printf,避免成为递归。通过int  enable_malloc = 1;来判断,当然free中也是相同情况。下面只列举malloc。

int  enable_malloc = 1;

void * malloc(size_t size)
{
    if(enable_malloc)    //这里为1
    {
        enable_malloc = 0;    //设置为0
        ptr = mallock_f(size);        //开辟所需要的空间
        printf("ptr:%p\n",ptr);       //然后第一次调用printf,他的内部会再走一边这个malloc,但是这里的enable_malloc为0,会走else的函数。
        enable_malloc = 1;            //最后恢复
    }
    else        //当enable_malloc 为0 会走这里
    {
        ptr = mallock_f(size);    //然后为printf开辟内存
        //printf("ptr:%p\n",ptr);
    }
    return ptr;
}

        下面就是全部的代码,和宏定义版几乎没有任何区别,也就是多了判断。但是其中还有个新东西:__builtin_return_address。这是为了得到当前函数返回地址,即此函数被别的函数调用,然后此函数执行完毕后,返回,所谓返回地址就是那时候的地址。

        举例:现在main中调用f1函数,f1中调用f2函数,那么在f2中使用__builtin_return_address(0),那么返回的地址是f2的上一层f1的地址。

int  enable_malloc = 1;
int  enable_free = 1;

//设置函数指针
typedef void *(*mallock_t)(size_t size);
typedef void (*free_t)(void * ptr);
mallock_t mallock_f = NULL;
free_t free_f = NULL;

void * malloc(size_t size) 
{
    if(!mallock_f)
    {
        mallock_f = dlsym(RTLD_NEXT,"malloc");    //通过dlsym将回调函数设置为系统调用的malloc
    }
    
    void * ptr = NULL;

    if(enable_malloc)
    {
        enable_malloc = 0;
        ptr = mallock_f(size);
        //printf("ptr:%p\n",ptr);

        //得到当前函数返回地址,即此函数被别的函数调用,然后此函数执行完毕后,返回,所谓返回地址就是那时候的地址。
        void *caller = __builtin_return_address(0); //用于返回上几级函数:main->f1()->f2()->f3()  在f3里面使用 0 这个,那么返回上面的f2

        char  buff[128]={0};
        snprintf(buff,128,"./bloc/%p.mem",ptr);

        FILE * fp = fopen(buff,"w");
        if(!fp)
        {
            free(ptr);
            return NULL;
        }

        fprintf(fp,"[+] malloc [%p] %p,size = %ld\n",caller,ptr,size);
        
        fflush(fp); //write
        fclose(fp);

        //printf("malloc\n");
        enable_malloc = 1;
    }
    else
    {
        ptr = mallock_f(size);
        //printf("ptr:%p\n",ptr);
    }
    return ptr;

}

void free(void * ptr)
{
    if(!free_f)
    {
        free_f = dlsym(RTLD_NEXT,"free");
    }

    char  buff[128]={0};
    snprintf(buff,128,"./bloc/%p.mem",ptr);

    if(unlink(buff)>0)   //delete
    {
        printf("double free: %p\n", ptr);
        return ;
    }

    //printf("free\n");
    return free_f(ptr);
}

四、一些问题

1、线上内存泄漏怎么办

        我们可以通过实现内存泄漏模块,通过类似于nginx的热更新操作,然后定位内存泄漏的位置。将问题解决之后,在夜深人静的时候将项目重新启动。

2、malloc都有日志,这样性能是不是会很低

我们可以每隔一段时间输出一次,不用一直输出,这样选择性打开,可以提高性能。

内存泄漏的代码就这一些,感谢大家观看!https://xxetb.xetslk.com/s/2D96kH

  • 19
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值