内存泄漏检测组件的分析与实现(linux c)-mtrace工具使用

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 内存泄漏产生原因在堆上使用malloc/remalloc/calloc分配了内存空间,但是没有使用free释放对应的空间。

基本知识

内存泄漏产生原因

在堆上使用malloc/remalloc/calloc分配了内存空间,但是没有使用free释放对应的空间。


内存泄漏危害

计算机的堆内存是一定的,一段时间后,由于只存在内存的分配,不存在释放操作,会导致无法在堆上分配一块合适的内存(即是说产生了内存泄漏),从而导致程序的崩溃。


如何防止内存方式泄漏

(1)事前预防内存泄漏

(2)实现内存泄露组件检测是否有内存泄漏。

针对(1)个人认为,如果不是一个非常有经验而且足够细心的程序员,一般很难写出没有内存泄漏的程序,那么最好的办法,就是实现一个检测内存是否泄漏的组件。


内存泄漏检测组件

实现一个内存泄漏组件主要包括:(1)如何知道内存发生了泄漏。(2)如何定位代码哪一行引起了内存泄漏。

针对(1),我们可以使用链表或者其他数据结构,每次分配内存时,就将分配的内存地址和大小等信息存入链表中,释放时根据内存地址和大小对其相应的节点进行释放,最后在检测链表是否为空来判断是否存在内存泄漏。但是数据结构的缺点是内存发生泄漏时不能明显的展示出来。如果使用文件的方式来表示是否发生了内存泄漏,具体假如使用一个单独的文件夹来存放内存检测组件生成的所有文件,运行程序时先清空文件夹的文件,系统调用一次malloc会生成一个文件,以malloc生成的内存地址为文件名,free时释放malloc对应生成的文件,最后如果文件夹存在文件时,就说明存在内存泄漏(malloc和free不匹配造成的)。

针对(2)可以使用C语言的__FILE__、FUNCTION、__LINE__宏定义或者builtin_return_address()API定位是哪一行引起了内存泄漏。


内存泄漏检测工具

常用内存泄漏检测工具包括:valgrind,mtrace等


代码运行环境

系统:Ubuntu16.04

编译环境:gcc 5.4

为什么这里要提到代码的运行环境,可能存在不同的情况,可能最新的gcc编译器已经不支持重写系统API的情况,可能存在重定义的错误,这时候需要借助hook来解决这种问题。


内存泄漏检测组件的实现

我们主要介绍3种内存泄漏检测组件的实现方式:重写malloc/free函数、宏定义、mtrace的实现方式。

我们的文件名为memleak.c,可运行文件名为memleak


重写malloc、free函数

为了说明重写malloc和free的最终版本中的enable_malloc_hook和enable_free_hook变量的作用,我们先给出一个最简单的代码,然后说明其错误原因。

递归错误代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <malloc.h>
#include <mcheck.h>
void *malloc(size_t size){
  printf("malloc");
  return NULL;
}
void free(void *p){
  // printf("free");
}
int main(){
  void *p1 = malloc(10);
  void *p2 = malloc(20);
  free(p1);
  return 0;
}

递归代码分析

如果编译器支持重写系统的malloc和free,那么main函数就会调用我们自已定义的malloc和free,如果不支持,请使用hook来实现,如果不知道hook的朋友,可以看看关于死锁的hook描述。如果不编译,我们很可能就认为会打印出”malloc”,但是实际上结果如下:

8a197270d3a57344160fd3b79a9881f1_1ed88cc2be90403a81a97497e5e506a6.png

我们的malloc和free函数只有一句printf(“malloc”),难道这句会发生”段报错”?我们使用gdb调试,会发现printf()内部会调用malloc函数,malloc被我们重写,这样就导致malloc和printf的相互调用,从而形成递归。


示例代码(单线程)

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <syslog.h>
#include <unistd.h>
extern void *__libc_malloc(size_t size); //malloc.h里面定义的
int enable_malloc_hook = 1;              //终止递归的变量(具体参考malloc函数说明)
extern void __libc_free(void *p);
int enable_free_hook = 1;
void *malloc(size_t size)
{
    if (enable_malloc_hook)
    { //调用系统
        enable_malloc_hook = 0;
        void *p = __libc_malloc(size); //分配内存,系统的malloc实际上也是调用的这个api进行内存分配。
        //返回malloc调用完成时的地址,可以结合addr2line命令定位到哪一行内存泄露。
        void *caller = __builtin_return_address(0);
        char buff[128] = {0};
        sprintf(buff, "./mem/%p.mem", p); //使用malloc返回的地址作为文件名(p.mem)
        FILE *fp = fopen(buff, "w");
        fprintf(fp, "[+%p] --> addr:%p, size:%ld\n", caller, p, size);
        fflush(fp);
        //fclose(fp);//注意不能close文件
        //printf函数内部会调用malloc,如果不用enable_malloc_hook变量会导致递归malloc的使用
        printf("malloc :%p\n", p);
        //保证下次调用malloc进入到if,注意,多线程不是线程安全的
        enable_malloc_hook = 1;
        return p;
    }
    else
    { //如果是其他API(比如printf)调用了malloc,会直接调用__lib_malloc(size_t size)进行分配,从而使得递归得以退出
        return __libc_malloc(size);
    }
}
void free(void *p)
{
    if (enable_free_hook)
    { //调用free
        enable_free_hook = 0;
        __libc_free(p);
        char buff[128] = {0};
                sprintf(buff, "./mem/%p.mem", p);
        if (unlink(buff) < 0) //删除文件,如果返回小于0,说明释放了2次
        {
            printf("double free:%p\n", p);
        }
        printf("free:%p\n", p);
        enable_free_hook = 1; //保证下次free,还走if
    }
    else
    { //其他系统API调用会直接调用__libc_free.
        __libc_free(p);
    }
}
int main()
{
    void *p1 = malloc(10);
    void *p2 = malloc(20);
    free(p1);
    return 0;
}

结果分析

运行结果如下:

bc5ce93c7bc7601d4d67342848805136_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_17,color_FFFFFF,t_70,g_se,x_16.png

对上图结果进行说明:首先保证文件夹mem存在。


1.编译c文件(带-g参数):gcc -o memleak memleak.c -g

2.删除mem文件夹里面的文件(如果没有mem文件夹,先创建mem文件夹,保证mem文件夹和memleak.c在同一目录下):rm -rf mem/*

3.运行程序:./memleak

通过运行程序可以发现malloc 2次,free了一次,按照预期,在mem文件夹下应该新产生了0x9a7680.mem的文件,我们使用cat看看里面的内容:[+0x400a29] --> addr:0x9a7680, size:20

4.使用addr2line命令来查看内存泄漏的具体位置:addr2line -f -e memleak -a 0x400a29得到内存泄漏发生在main函数77行位置。


结论:通过代码运行结果,可以检查出具体是哪一行出现了问题,但是可以很容易看出,**代码不是线程安全的,**enable_malloc_hook和enable_free_hook都是全局变量,如果是多线程同时malloc,当一个线程malloc时,会出现enable_malloc_hook起初就为0的情况,此时会走malloc的else,这样就会出现malloc分配了空间,却没添加文件的情况,就会出现”double free”的假象,同样,当多个线程同时free的情况,会出现enable_free_hook起初为0的情况,此时就会导致本来已经释放了空间,但是还存在分配的文件,会被误认为内存分配后没有被释放的情况。当然还有其他的复杂情况,这里就不分析了。


我也考虑过使用锁和条件变量来控制,但是锁造成的死锁(递归造成多次对锁加锁)问题不知道如何解决。如果可以,可以在调用malloc函数的前后对其加锁(可以自己使用一个条件语句,如果是调试模式,则加锁,否则就使用系统的malloc),如果大家有什么好办法可以跟我说说。


其他情况说明:使用__builtin_return_address(LEVEL) API 和addr2line命令结合使用在很多实验环境下都出现了如下的现象(出现了??):

7ecf6af733c2f156687726d8f6271025_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_17,color_FFFFFF,t_70,g_se,x_16.png

有的环境会出现这种情况,目前我不知道是什么原因。

要注意这里__builtin_return_address()的参数,如果传递0,代表调用malloc函数的直接函数的调用处 ,在我的代码就是main函数,如果假如你代码结构是main->func->malloc,这时候如果传递1,就会找到main函数调用func函数说明出现了内存泄漏。


宏定义(推荐)

代码实现

为了防止递归条件的发生,使用了enable_malloc_hook和enable_free_hook变量来作为退出递归的条件,具体的使用参考如下代码:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <syslog.h>
#include <unistd.h>
void *malloc_hook(size_t size, const char *file, const char *func, int line)
{
    void *p = malloc(size);
    char str[256] = {0};
    sprintf(str, "./mem/%p.mem", p);
    FILE *fp = fopen(str, "w");
    fprintf(fp, "[info:]file:%s,func:%s,line:%d, addr:%p,size:%ld\n", file, func, line, p, size);
    fflush(fp);
    fclose(fp);
    return p;
}
void free_hook(void *p, const char *file, const char *func, int line)
{
    free(p);
    char str[256] = {0};
    sprintf(str, "./mem/%p.mem", p);
    if (unlink(str) < 0)
    {
        printf("double free.\n");
    }
}
//宏定义要定义在函数的下面,因为malloc_hook里面使用了malloc
//如果宏定义在函数的前面,会导致递归
#define malloc(size) malloc_hook(size, __FILE__, __FUNCTION__, __LINE__)
#define free(p) free_hook(p, __FILE__, __FUNCTION__, __LINE__);
#endif
int main()
{
    void *p1 = malloc(10);
    void *p2 = malloc(20);
    free(p1);
    return 0;
}

实验结果

ea57c6ee83748c99cf7db30e29726715_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_17,color_FFFFFF,t_70,g_se,x_16.png

通过宏定义和__FILE__,LINE,__FUNCTION__可以很容易知道内存泄漏的具体位置,比如该结果为main函数的99行出现了内存泄漏问题。


hook方式(mtrace方式)

完整代码

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <syslog.h>
#include <unistd.h>
typedef void *(*malloc_hook_t)(size_t size, const void *caller); //系统的__malloc_hook实际的函数类型
malloc_hook_t malloc_f;                                          //用于保存系统默认的__malloc_hook函数地址
typedef void (*free_hook_t)(void *p, const void *caller);        //系统的__free_hook的实际函数类型
free_hook_t free_f;                                              //用于保存系统默认的__free_hook函数指针地址
int replaced = 0;                                                //如果为1,malloc/free指向我们自定义的函数
void mem_trace(void);                                            //让其malloc指向我们自己定义的函数
void mem_untrace(void);                                          //让其free指向我们自己定义的函数
//自定义的malloc函数,与系统的__malloc_hook保持一致
//caller参数代表调用该函数的地址(__builtin_return_address(0)返回的地址就是这个地址)
void *malloc_hook_f(size_t size, const void *caller)
{
    //防止递归-如果不加这句,会让下面的malloc继续执行malloc_hook_f,从而造成递归
    //我们只要得到caller指针这个值就可以了。
    mem_untrace();
    void *ptr = malloc(size);
    //printf("+%p: addr[%p]\n", caller, ptr);
    char buff[128] = {0};
    sprintf(buff, "./mem/%p.mem", ptr);
    FILE *fp = fopen(buff, "w");
    fprintf(fp, "[+%p] --> addr:%p, size:%ld\n", caller, ptr, size);
    fflush(fp);
    fclose(fp);  //free
    mem_trace(); //保证下次malloc还是用我们自定义的
    return ptr;
}
void free_hook_f(void *p, const void *caller)
{
    mem_untrace(); //防止free函数递归
    //printf("-%p: addr[%p]\n", caller, p);
    char buff[128] = {0};
    sprintf(buff, "./mem/%p.mem", p);
    if (unlink(buff) < 0)
    { // no exist
        printf("double free: %p\n", p);
        return;
    }
    free(p);
    mem_trace();
}
void mem_trace(void)
{ //mtrace
    replaced = 1;
    malloc_f = __malloc_hook;      //__malloc_hook是系统本身提供的函数指针(会在malloc调用时初始化)
    free_f = __free_hook;          //__free_hook是系统本身提供的(free调用时会初始化)
    __malloc_hook = malloc_hook_f; //指向我们自定义函数,malloc会调用我们定义的函数
    __free_hook = free_hook_f;     //指向我们自定义函数,free会调用我们自定义的函数
}
//
void mem_untrace(void)
{
    __malloc_hook = malloc_f;
    __free_hook = free_f;
    replaced = 0;
}
int main()
{
    mem_trace();
    void *p1 = malloc(10);
    void *p2 = malloc(20);
    free(p1);
    mem_untrace();
    return 0;
}

实验结果

35e2fc3e859ddd99ecf57c0841384e19_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_17,color_FFFFFF,t_70,g_se,x_16.png

实验结果得到代码189行出分配了内存,没有进行内存释放,其操作见2.2.3章节。

实现方式比较

个人比较推荐宏定义的方式来实现内存泄漏组件的实现方式,实现方式最简单,而且应该是支持多线程的,malloc_hook里面的系统本身的malloc/free函数是线程安全的,也就是进入到malloc_hook和free_hook函数会被阻塞,当然这是我的个人理解,不知道是否正确?

mtrace工具使用

产生日志文件

命令:

(1)生成mtrace的环境变量MALLOC_TRACE(用于指定mtrace的日志路径)

export MALLOC_TRACE=./test.log

(2)删除已有的日志文件(可选,如果存在日志文件的话,建议提前删除日志文件)

rm -rf test.log

代码

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <malloc.h>
#include <mcheck.h>
int main()
{
  mtrace();
  void *p1 = malloc(10);
  void *p2 = malloc(20); //
  free(p1);
  muntrace();
    return 0;
}

实验结果

64804838ad8da871a35258eb9ee77213_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_16,color_FFFFFF,t_70,g_se,x_16.png

文档说明

本文使用了3种方式来实现内存泄漏检测组件,可以用来检测自己和第三方库的内存泄漏(我们在代码中使用了第3方库的地方,如果调用了malloc/free,那么实际上也是调用了我们这里的malloc/free)情况,另外remalloc/calloc的代码可以自己一样去实现。

代码来源于腾讯课堂-零声学院king老师

ps:如果有什么不懂的可以直接给我留言。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
打赏
0
0
0
0
2
分享
相关文章
PCIe 以太网芯片 RTL8125B 的 spec 和 Linux driver 分析备忘
本文详细介绍了 Realtek RTL8125B PCIe 以太网芯片的规格以及在 Linux 中的驱动安装和配置方法。通过深入分析驱动源码,可以更好地理解其工作原理和优化方法。在实际应用中,合理配置和优化驱动程序可以显著提升网络性能和稳定性。希望本文能帮助您更好地使用和管理 RTL8125B,以满足各种网络应用需求。
71 33
Splunk Enterprise 9.4.1 (macOS, Linux, Windows) 发布 - 机器数据管理和分析
Splunk Enterprise 9.4.1 (macOS, Linux, Windows) 发布 - 机器数据管理和分析
12 0
Splunk Enterprise 9.4.1 (macOS, Linux, Windows) 发布 - 机器数据管理和分析
Python图像处理中的内存泄漏问题:原因、检测与解决方案
在Python图像处理中,内存泄漏是常见问题,尤其在处理大图像时。本文探讨了内存泄漏的原因(如大图像数据、循环引用、外部库使用等),并介绍了检测工具(如memory_profiler、objgraph、tracemalloc)和解决方法(如显式释放资源、避免循环引用、选择良好内存管理的库)。通过具体代码示例,帮助开发者有效应对内存泄漏挑战。
31 1
Linux--深入理与解linux文件系统与日志文件分析
深入理解 Linux 文件系统和日志文件分析,对于系统管理员和运维工程师来说至关重要。文件系统管理涉及到文件的组织、存储和检索,而日志文件则记录了系统和应用的运行状态,是排查故障和维护系统的重要依据。通过掌握文件系统和日志文件的管理和分析技能,可以有效提升系统的稳定性和安全性。
69 7
启用Linux防火墙日志记录和分析功能
为iptables启用日志记录对于监控进出流量至关重要
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
102 1
|
4月前
|
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
80 3
Linux内核中的调度策略优化分析####
本文深入探讨了Linux操作系统内核中调度策略的工作原理,分析了不同调度算法(如CFS、实时调度)在多核处理器环境下的性能表现,并提出了针对高并发场景下调度策略的优化建议。通过对比测试数据,展示了调度策略调整对于系统响应时间及吞吐量的影响,为系统管理员和开发者提供了性能调优的参考方向。 ####
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
713 1
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等