• 手写内存泄漏检测组件


    前言

      本文介绍内存泄漏检测的核心需求以及注意点,一共4个版本的代码层层迭代。本文4个版本源码git地址:内存泄漏检测组件

      常用的内存泄漏检测工具有valgrind和mtrace。我们使用这两个工具的时候一般是已经发现了内存泄漏的现象了再去检测,那么有没有一种方法在内存使用的时候,就发现内存泄漏呢。

      本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。

    内存泄漏

    内存泄漏的现象与危害

      内存泄漏只有不带gc垃圾回收机制的语言才有,比如go和java,它们都自带gc,所以它们不会有内存泄漏。 但是像c和c++是不带gc的,所以很可能发生内存泄漏的情况。这里我们已c语言举例,c在分配内存时调用malloc/calloc/realloc(本文全部以malloc举例),释放内存时调用free。

      那么内存泄漏的核心原因就很简单了,内存分配与内存释放没有做到匹配。换言之,调用了多少次malloc,就free多少次,那么就不会产生内存泄漏。如果malloc和free的次数不对等,那么一定是有问题的。

    int main() {
        void *p1 = malloc(10);
        void *p2 = malloc(20);
        free(p1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

      以上的代码,分配了两块内存,只释放了p1,p2没有被释放,那么这个程序就产生了内存泄漏。

      内存泄漏的危害:随着工程代码量越来越多,自然内存泄漏的排查就成为了一个很头疼的问题。有分配没有释放,自然会使得进程堆的内存会越来越少,直到耗尽。会造成后面的运行时代码不能成功分配内存。分配失败我们的程序就不能继续的往下执行。

    内存泄漏检测组件的两个核心需求点

      现在知道了内存泄漏和危害和内存泄漏的原因,那么内存泄漏如何解决?内存泄漏是没有自动 gc 的编程语言所产生的,解决方案一,引入 gc。这是根治内存泄漏的最好的方案。但是这样的方案有失去了 c/c++语言的优势。方案二,当发生内存泄漏的时候,能够精准的定位代码哪一行所引起的。这也是我们实现内存泄漏检测的如何核心实现需求。那么本文就是围绕着下面两个需求展开的。

    1. 能够检测出来发生了内存泄漏
    2. 能够判断定位代码哪一行引起内存泄漏

      对于第一个需求,我们在下文中介绍。对于定位代码这里提前介绍两个方案。

    # 宏
    __FILE__,__FUNC__,__LINE__ 
    # 编译器提供的函数,返回第N层调用函数地址, addr2line是一个工具
    builtin_return_address(N)  +  addr2line
    
    • 1
    • 2
    • 3
    • 4
    func1->func2->func3->func4{ cnt= builtin_return_address(0) }   cnt=func3
    func1->func2->func3->func4{ cnt= builtin_return_address(1) }   cnt=func2
    func1->func2->func3->func4{ cnt= builtin_return_address(2) }   cnt=func1
    
    • 1
    • 2
    • 3

    第一版:__libc_malloc, __libc_malloc 与 __builtin_return_address,addr2line

    hook malloc与free出现的问题

      我们运行下面的代码,发现出现段错误,并不符合我们的预期

    //
    // Created by 68725 on 2022/8/13.
    //
    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    
    typedef void *(*malloc_t)(size_t);
    
    malloc_t malloc_f;
    
    typedef void (*free_t)(void *);
    
    free_t free_f;
    
    static int init_hook() {
        malloc_f = dlsym(RTLD_NEXT, "malloc");
        free_f = dlsym(RTLD_NEXT, "free");
    }
    
    void *malloc(size_t size) {
        printf("In malloc\n");
        return NULL;
    }
    
    void free(void *ptr) {
        printf("In free\n");
    
    }
    
    int main() {
    	init_hook();
        void *p1 = malloc(10);
        void *p2 = malloc(20);
        free(p1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o first first.c -ldl 
    root@wxf:/tmp/tmp.d4vz2dOyJP# ./first 
    Segmentation fault (core dumped)
    
    • 1
    • 2
    • 3

      我们的代码看起来明明这么合理,为什么会段错误?我们进入gdb看一看,我们打印23行看一看,发现程序确确实实是走到了23行。但是我们发现,它递归的进入了printf这个函数,并且第一次malloc(size=10),而后面malloc (size=1024),这说明什么?说明printf里面也调用了malloc函数,而这个malloc函数被我们hook了,导致递归进入我们hook的函数里面了。那么我们下面就要去破坏这个递归。

    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o first first.c -ldl -g
    root@wxf:/tmp/tmp.d4vz2dOyJP# gdb ./first 
    Reading symbols from ./first...done.
    (gdb) b 23
    Breakpoint 1 at 0x741: file first.c, line 23.
    (gdb) r
    Starting program: /tmp/tmp.d4vz2dOyJP/first 
    
    Breakpoint 1, malloc (size=10) at first.c:23
    23	    printf("In malloc\n");
    (gdb) c
    Continuing.
    
    Breakpoint 1, malloc (size=1024) at first.c:23
    23	    printf("In malloc\n");
    (gdb) c
    Continuing.
    
    Breakpoint 1, malloc (size=1024) at first.c:23
    23	    printf("In malloc\n");
    (gdb) c
    Continuing.
    
    Breakpoint 1, malloc (size=1024) at first.c:23
    23	    printf("In malloc\n");
    (gdb) 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

      如何破坏递归呢?我们让第一次进入函数的部分执行我们的流程,而递归进去的算第二次进入函数,那么我们直接调用系统原来的函数即可。这里介绍两个函数__libc_malloc和__libc_free,它们是malloc和free底层调用的函数。可以看到下面代码注释的地方,我们直接改成这两个函数效果是一样的。不过既然我们都用hook了,那有何必再调用别的函数呢?这里讲__libc_malloc和__libc_free是为了引出malloc底层调用__libc_malloc,free底层调用__libc_free。

    //
    // Created by 68725 on 2022/8/13.
    //
    #define _GNU_SOURCE
    
    #include 
    #include 
    #include 
    
    extern void *__libc_malloc(size_t size);
    
    extern void __libc_free(void *ptr);
    
    typedef void *(*malloc_t)(size_t);
    
    int enable_malloc_hook = 1;
    
    malloc_t malloc_f;
    
    typedef void (*free_t)(void *);
    
    int enable_free_hook = 1;
    
    free_t free_f;
    
    static int init_hook() {
        malloc_f = dlsym(RTLD_NEXT, "malloc");
        free_f = dlsym(RTLD_NEXT, "free");
    }
    
    void *malloc(size_t size) {
        if (enable_malloc_hook) {
            enable_malloc_hook = 0;
    
            void *p = malloc_f(size);
            //void *p = __libc_malloc(size);
    
            printf("malloc--->ptr:%p size:%zu\n", p, size);
    
    
            enable_malloc_hook = 1;
            return p;
        }
        else {
            return malloc_f(size);
            //return __libc_malloc(size);
        }
    }
    
    void free(void *ptr) {
        if (enable_free_hook) {
            enable_free_hook = 0;
            printf("free  --->ptr:%p\n", ptr);
    
            free_f(ptr);
            //__libc_free(ptr);
    
            enable_free_hook = 1;
        }
        else {
            return free_f(ptr);
            //return __libc_free(ptr);
        }
    }
    
    int main() {
        init_hook();
    
        void *p1 = malloc(10);
        void *p2 = malloc(20);
        free(p1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

      我们现在就能正常执行程序了,并且我们肉眼可见的能够分析出哪个指针没有被释放。我们现在只知道是哪个指针没有被释放,但是我们并不能定位到是代码的哪一行。所以我们接着再进行优化。

    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o first first.c -ldl -g
    root@wxf:/tmp/tmp.d4vz2dOyJP# ./first 
    malloc--->ptr:0x55e80ef45260 size:10
    malloc--->ptr:0x55e80ef45690 size:20
    free  --->ptr:0x55e80ef45260
    
    • 1
    • 2
    • 3
    • 4
    • 5

      下面三个宏在try-catch那篇文章我们其实已经用过了,所以现在我们来使用builtin_return_address看看。上面__libc_malloc和__libc_free相关的代码我就删掉了,其作用与malloc_f和free_f一致。

    # 宏
    __FILE__,__FUNC__,__LINE__ 
    # 编译器提供的函数,返回第N层调用函数
    builtin_return_address(N)
    
    • 1
    • 2
    • 3
    • 4

    使用addr2line定位代码

      我们在malloc函数里面加上caller的打印,来看看它的值是什么。这里先提出一点,我在__builtin_return_address函数外面套了一层ConvertToVMA。目的是把返回的内存地址转换成VMA地址。至于什么是VMA如果有机会再拎起一篇详细介绍,这里不做这介绍,原因主要是这里写builtin_return_address的目的是,介绍builtin_return_address函数和addr2line工具,其实我是不推荐使用这种方案的,因为用上面三个宏去定位文件函数行号更方便,也不需要借助工具去分析。另一方面是有些linux系统返回的内存地址就是VMA地址,有些不是,比如我的机器就不是,具体原理我也不是很懂,如果有知道的读者可以在评论区说一下。

    void *caller = ConvertToVMA(__builtin_return_address(0));
    printf("[+%p]--->ptr:%p size:%zu\n", caller, p, size);
    
    • 1
    • 2
    //
    // Created by 68725 on 2022/8/13.
    //
    #define _GNU_SOURCE
    
    #include 
    #include 
    #include 
    #include 
    
    typedef void *(*malloc_t)(size_t);
    
    int enable_malloc_hook = 1;
    
    malloc_t malloc_f;
    
    typedef void (*free_t)(void *);
    
    int enable_free_hook = 1;
    
    free_t free_f;
    
    static int init_hook() {
        malloc_f = dlsym(RTLD_NEXT, "malloc");
        free_f = dlsym(RTLD_NEXT, "free");
    }
    
    void *ConvertToVMA(void *addr) {
        Dl_info info;
        struct link_map *link_map;
        dladdr1((void *) addr, &info, (void **) &link_map, RTLD_DL_LINKMAP);
        return addr - link_map->l_addr;
    }
    
    void *malloc(size_t size) {
        if (enable_malloc_hook) {
            enable_malloc_hook = 0;
            void *p = malloc_f(size);
    
            void *caller = ConvertToVMA(__builtin_return_address(0));
            printf("[+%p]--->ptr:%p size:%zu\n", caller, p, size);
            char command[256];
            Dl_info info;
            dladdr(malloc, &info);
            snprintf(command, sizeof(command), "addr2line -f -e %s -a %p >1.txt", info.dli_fname, caller);
            printf("%s\n", command);
    
            system(command);
    
            enable_malloc_hook = 1;
            return p;
        }
        else {
            return malloc_f(size);
        }
    }
    
    void free(void *ptr) {
        if (enable_free_hook) {
            enable_free_hook = 0;
            void *caller = __builtin_return_address(0);
            printf("[-%p]--->ptr:%p\n", caller, ptr);
    
            free_f(ptr);
    
            enable_free_hook = 1;
        }
        else {
            return free_f(ptr);
        }
    }
    
    int main() {
        init_hook();
    
        void *p1 = malloc(10);
        void *p2 = malloc(20);
        free(p1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

      我们可以看到,__builtin_return_address返回的第一个0xb44,它其实是代码段上的一个地址,通过这个地址,我们使用addr2line可以计算出来是在哪个函数,哪个文件,哪行。只不过我这里偷懒在代码里用system直接执行了,一般来说我们是在bash里面通过log记录的地址,再去使用addr2line的。至于addr2line的用法,直接百度搜好了,很简单。

    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o first first.c -ldl -g
    root@wxf:/tmp/tmp.d4vz2dOyJP# ./first 
    [+0xb44]--->ptr:0x558bf404f260 size:10
    addr2line -f -e ./first -a 0xb44
    0x0000000000000b44
    main
    /tmp/tmp.d4vz2dOyJP/first.c:76
    [+0xb52]--->ptr:0x558bf404f690 size:20
    addr2line -f -e ./first -a 0xb52
    0x0000000000000b52
    main
    /tmp/tmp.d4vz2dOyJP/first.c:77
    [-0x558bf1ea0b62]--->ptr:0x558bf404f260
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    root@wxf:/tmp/tmp.d4vz2dOyJP# addr2line -f -e ./first -a 0xb44
    0x0000000000000b44
    main
    /tmp/tmp.d4vz2dOyJP/first.c:76
    
    • 1
    • 2
    • 3
    • 4

      那么我们现在已经也解决了定位的问题,下面我们再来看看怎么做内存检测。从上面我们其实已经可以看到第一个+的prt和最后-的ptr地址是一样的,也就是说malloc的地址被free掉了,而第二个没有被free(-),因为我们没有看到减号。

    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o first first.c -ldl -g
    root@wxf:/tmp/tmp.d4vz2dOyJP# ./first 
    [+0xb44]--->ptr:0x558bf404f260 size:10
    [+0xb52]--->ptr:0x558bf404f690 size:20
    [-0x558bf1ea0b62]--->ptr:0x558bf404f260
    
    • 1
    • 2
    • 3
    • 4
    • 5

      那也就意味着,我们现在需要设计一种方案,在malloc的时候把ptr加进去,free的时候把对应的ptr去掉,在程序结束之后我们可以看到有哪些ptr还存在,这些存在的ptr就是没有被free的,如此一来,就能检测到内存泄漏了。

    检测内存泄漏的最佳方案

      读者在这里可以思考一下上面说的方案怎么做最好,其实如果用map,用链表,我个人感觉都不好,因为在程序中用这两个数据结构,那么数据还是保存在堆栈上的,在程序结束之前需要打印出来。那如果用文件的方法呢?malloc的时候,以ptr内存地址为文件名,把<文件,函数,行号>写入文件,free的时候,把对应的文件删除。那么我们只需要通过ls即可清楚的看到哪些内存被泄露了,用cat看一下文件就能定位。下面我们对代码再次优化。

      还记得上面我们偷懒使用的system吗?这里就派上用场了,在拼接字符串的时候,我们用> 将标准输出定位到文件即可,在删除的时候用unlink删除。

    //malloc
    snprintf(command, sizeof(command), "addr2line -f -e %s -a %p > ./mem/%p.mem", info.dli_fname, caller, p);
    system(command);
    //free
    char buff[128] = {0};
    sprintf(buff, "./mem/%p.mem", ptr);
    if (unlink(buff) < 0) {
        printf("double kill:%p\n",ptr);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o first first.c -ldl -g
    root@wxf:/tmp/tmp.d4vz2dOyJP# ./first 
    [+0xc3a]--->ptr:0x55f613d27260 size:10
    [+0xc48]--->ptr:0x55f613d27690 size:20
    [-0xc58]--->ptr:0x55f613d27260
    root@wxf:/tmp/tmp.d4vz2dOyJP# ls
    first  first.c	mem
    root@wxf:/tmp/tmp.d4vz2dOyJP# cd mem/
    root@wxf:/tmp/tmp.d4vz2dOyJP/mem# ls
    0x55f613d27690.mem
    root@wxf:/tmp/tmp.d4vz2dOyJP/mem# cat 0x55f613d27690.mem 
    0x0000000000000c48
    main
    /tmp/tmp.d4vz2dOyJP/first.c:82
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

      至此,我们的第一版内存泄漏检测组件的代码就完成了,完整代码可去前言的源码超链接中获取。现在再来回顾一下两个核心需求

    1. 能够检测出来发生了内存泄漏 
    2. 能够判断定位代码哪一行引起内存泄漏
    
    • 1
    • 2

      我们现在通过mem文件夹里面的文件就可以看出来有没有发生内存泄漏,因为只有没有被free的地址才会有文件。有文件就说明发生了内存泄漏。怎么定位代码呢,我们这里用的是__builtin_return_address 和 addr2line。其实内存泄漏检测没有想象中的这么恐怖,在我初知内存泄漏的时候,我感觉那些内存泄漏的检测软件很厉害,那么在本文抽丝剥茧之后,这种对于未知的恐惧就消失了,甚至于本文下面还能接着优化。万变不离其宗,在写内存泄漏组件的时候,围绕着上面两个需求去做就好了。

      使用__builtin_return_address 和 addr2line的第一版代码不知道读者有没有感觉到这里十分的麻烦,下面第二版代码我们使用简洁的宏定义来做。

    第二版:采用宏定义

    巧用宏机制

      在我们的第一版代码实现中,更多的是想向读者介绍一些函数。而且我们在使用hook的时候还遇到了递归的问题,以及__builtin_return_address的VMA的问题,对于后面这个函数,我们使用系统提供的三个宏即可解决。现在我们想一下,这个hook用在这里真的合适吗?

      我们知道函数预编译的时候,会把对应宏下面的内容全部替换掉,那么我们是否可以定义malloc的宏呢?例如下面两段代码,main中的malloc被替换成了malloc_def,而malloc_def中的malloc却没有被替换。我们使用这个机制,就可以避开hook的风险。并且我们可以在宏定义的上下加个开关,如果代码想要进行内存泄漏检测就打开,不想就走原来的系统调用即可。

    void *malloc_def(size_t size, const char *file, const char *func, int line) {
    	void *p = malloc(size);
    }
    
    #define malloc(size) malloc_def(size,__FILE__,__FUNCTION__ ,__LINE__)
    
    int main() {
        void *p1 = malloc(10);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    void *malloc_def(size_t size, const char *file, const char *func, int line) {
    	void *p = malloc(size);
    }
    
    #define malloc(size) malloc_def(size,__FILE__,__FUNCTION__ ,__LINE__)
    
    int main() {
        void *p1 = malloc_hook(10,second.c,main,15);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    #define check_mem_leak
    
    #ifdef check_mem_leak
    #define malloc(size) malloc_def(size,__FILE__,__FUNCTION__ ,__LINE__)
    #define free(p) free_def(p,__FILE__,__FUNCTION__ ,__LINE__)
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

      短短50行,我们就实现了比第一版更为优雅的内存泄漏检测组件。可以看到,在第一个需求如何检测内存泄漏,我们使用的都是统一的一个方案,malloc的时候创建一个文件,free的时候删除一个文件。在定位代码的时候有两个解决方法,这里比较推荐的就是宏的方法。

    优雅的代码

    //
    // Created by 68725 on 2022/8/13.
    //
    #include 
    #include 
    #include 
    #include 
    
    #define check_mem_leak
    
    
    void *malloc_def(size_t size, const char *file, const char *func, int line) {
        void *p = malloc(size);
    
        char buff[128] = {0};
        sprintf(buff, "./mem/%p.mem", p);
    
        FILE *fp = fopen(buff, "w");
        fprintf(fp, "[+%s:%s:%d] --> addr:%p, size:%ld\n", file, func, line, p, size);
        fflush(fp);
        fclose(fp);
    
        return p;
    }
    
    void free_def(void *p, const char *file, const char *func, int line) {
        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);
    }
    
    
    #ifdef check_mem_leak
    #define malloc(size) malloc_def(size,__FILE__,__FUNCTION__ ,__LINE__)
    #define free(p) free_def(p,__FILE__,__FUNCTION__ ,__LINE__)
    #endif
    
    int main() {
    
        void *p1 = malloc(10);
        void *p2 = malloc(20);
        void *p3 = malloc(30);
        void *p4 = malloc(40);
        free(p1);
        free(p2);
        free(p4);
        free(p4);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o second second.c 
    root@wxf:/tmp/tmp.d4vz2dOyJP# ./second 
    double free: 0x5571e5c55500
    root@wxf:/tmp/tmp.d4vz2dOyJP# cd mem
    root@wxf:/tmp/tmp.d4vz2dOyJP/mem# ls
    0x5571e5c554d0.mem
    root@wxf:/tmp/tmp.d4vz2dOyJP/mem# cat 0x5571e5c554d0.mem 
    [+second.c:main:48] --> addr:0x5571e5c554d0, size:30
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第三版:借助 malloc.h 里面 __malloc_hook

    malloc.h里面的__malloc_hook

      我们需要知道malloc的调用流程malloc->__libc_malloc->__malloc_hook,而这个__malloc_hook是个函数指针,所以我们可以在这上面做手脚。

    /* Hooks for debugging and user-defined versions. */
    extern void (*__MALLOC_HOOK_VOLATILE __free_hook)(void *__ptr, const void *)__MALLOC_DEPRECATED;
    
    extern void *(*__MALLOC_HOOK_VOLATILE __malloc_hook)(size_t __size, const void *)__MALLOC_DEPRECATED;
    
    extern void *(*__MALLOC_HOOK_VOLATILE __realloc_hook)(void *__ptr,size_t __size,const void *)__MALLOC_DEPRECATED;
    
    extern void *(*__MALLOC_HOOK_VOLATILE __memalign_hook)(size_t __alignment,size_t __size,const void *)__MALLOC_DEPRECATED;
    
    extern void (*__MALLOC_HOOK_VOLATILE __after_morecore_hook)(void);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    trace机制

      在main中我们启动trace,那么就代表着我们要将malloc内部调用的流程,变成我们写的函数,也就是说让malloc执行我们写的函数,在我们写的函数之后,我们再untrace,随后执行malloc让它分配内存,在结尾再进入trace。如此一来,即解决了第一版代码中hook的递归问题,提供了两个api接口给用户,交由用户自己判断是否需要内存泄漏检测。

    typedef void *(*malloc_hook_t)(size_t size, const void *caller);
    
    malloc_hook_t malloc_f;
    
    typedef void (*free_hook_t)(void *p, const void *caller);
    
    free_hook_t free_f;
    
    int replaced = 0;
    
    void mem_trace(void);
    void mem_untrace(void);
    
    
    void *malloc_hook_f(size_t size, const void *caller) {
        mem_untrace();
    
        void *ptr = malloc(size);
        printf("+%p: addr[%p]\n", caller, ptr);
    
        mem_trace();
        return ptr;
    }
    
    void free_hook_f(void *p, const void *caller) {
        mem_untrace();
        printf("-%p: addr[%p]\n", caller, p);
    
        free(p);
        mem_trace();
    }
    
    //replaced=1代表正在trace,__malloc_hook走我们自己写的函数
    //replaced=0代表没有trace,__malloc_hook走系统自己的函数
    void mem_trace(void) { //mtrace
        replaced = 1;
        //让malloc_f指向系统的
        malloc_f = __malloc_hook;
        //free_f
        free_f = __free_hook;
        //让系统的hook指向我们写的函数
        __malloc_hook = malloc_hook_f;
        __free_hook = free_hook_f;
    }
    
    void mem_untrace(void) {
        //让系统的hook指向系统的malloc和free
        __malloc_hook = malloc_f;
        __free_hook = free_f;
        replaced = 0;
    }
    int main() {
        mem_trace();
    
        void *p1 = malloc(10);
        void *p2 = malloc(20);
        void *p3 = malloc(30);
        void *p4 = malloc(40);
        free(p1);
        free(p2);
        free(p4);
        free(p4);
    
        mem_untrace();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    定位代码

      但是这里我们就不能使用宏了,那么我们就接着使用addr2line的方法

    //
    // Created by 68725 on 2022/8/16.
    //
    #define _GNU_SOURCE
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    typedef void *(*malloc_hook_t)(size_t size, const void *caller);
    
    malloc_hook_t malloc_f;
    
    typedef void (*free_hook_t)(void *p, const void *caller);
    
    free_hook_t free_f;
    
    int replaced = 0;
    
    void mem_trace(void);
    
    void mem_untrace(void);
    
    const void *ConvertToVMAToSystem_addr2line(const void *addr, const void *ptr) {
        Dl_info info;
        struct link_map *link_map;
        dladdr1((void *) addr, &info, (void **) &link_map, RTLD_DL_LINKMAP);
        const void *caller = addr - link_map->l_addr;
    
        char command[256];
        snprintf(command, sizeof(command), "addr2line -f -e %s -a %p > ./mem/%p.mem", info.dli_fname, caller, ptr);
        system(command);
    
        return caller;
    }
    
    void *malloc_hook_f(size_t size, const void *caller) {
        mem_untrace();
    
        void *ptr = malloc(size);
    
        caller = ConvertToVMAToSystem_addr2line(caller, ptr);
        printf("[+%p]--->ptr:%p size:%zu\n", caller, ptr, size);
    
        mem_trace();
        return ptr;
    }
    
    void free_hook_f(void *ptr, const void *caller) {
        mem_untrace();
    
        caller = ConvertToVMAToSystem_addr2line(caller, ptr);
        printf("[-%p]--->ptr:%p\n", caller, ptr);
    
        char buff[128] = {0};
        sprintf(buff, "./mem/%p.mem", ptr);
        if (unlink(buff) < 0) {
            printf("double kill:%p\n", ptr);
        }
        free(ptr);
    
        mem_trace();
    }
    
    //replaced=1代表正在trace,__malloc_hook走我们自己写的函数
    //replaced=0代表没有trace,__malloc_hook走系统自己的函数
    void mem_trace(void) { //mtrace
        replaced = 1;
        //让malloc_f指向系统的
        malloc_f = __malloc_hook;
        //free_f
        free_f = __free_hook;
        //让系统的hook指向我们写的函数
        __malloc_hook = malloc_hook_f;
        __free_hook = free_hook_f;
    }
    
    void mem_untrace(void) {
        //让系统的hook指向系统的malloc和free
        __malloc_hook = malloc_f;
        __free_hook = free_f;
        replaced = 0;
    }
    
    int main() {
        mem_trace();
    
        void *p1 = malloc(10);
        void *p2 = malloc(20);
        void *p3 = malloc(30);
        void *p4 = malloc(40);
        free(p1);
        free(p2);
        free(p4);
    
        mem_untrace();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o three three.c -ldl -g
    root@wxf:/tmp/tmp.d4vz2dOyJP# ./three 
    [+0xc34]--->ptr:0x55b41c3ad260 size:10
    [+0xc42]--->ptr:0x55b41c3ad690 size:20
    [+0xc50]--->ptr:0x55b41c3ad6b0 size:30
    [+0xc5e]--->ptr:0x55b41c3ad6e0 size:40
    [-0xc6e]--->ptr:0x55b41c3ad260
    [-0xc7a]--->ptr:0x55b41c3ad690
    [-0xc86]--->ptr:0x55b41c3ad6e0
    root@wxf:/tmp/tmp.d4vz2dOyJP# cd mem/
    root@wxf:/tmp/tmp.d4vz2dOyJP/mem# cat 0x55b41c3ad6b0.mem 
    0x0000000000000c50
    main
    /tmp/tmp.d4vz2dOyJP/three.c:93
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    第四版:第三方库mtrace的使用与我们的meme_trace差异

      我们使用第三方库mtrace看看与我们第三版代码使用上有什么区别,我们发现其实没什么区别,只不过它是把所有的malloc和free写到一个文件,而我们的方案是malloc就创建一个文件,free就删除一个文件,大同小异。

    //
    // Created by 68725 on 2022/8/16.
    //
    #include 
    #include 
    
    int main() {
        mtrace();
    
        void *p1 = malloc(10);
        void *p2 = malloc(20);
        void *p3 = malloc(30);
        void *p4 = malloc(40);
        free(p1);
        free(p2);
        free(p4);
    
        muntrace();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    root@wxf:/tmp/tmp.d4vz2dOyJP# export MALLOC_TRACE=./mtrace.log
    root@wxf:/tmp/tmp.d4vz2dOyJP# gcc -o mtrace mtrace.c 
    root@wxf:/tmp/tmp.d4vz2dOyJP# ./mtrace 
    root@wxf:/tmp/tmp.d4vz2dOyJP# ls
    first	  mem	     memleak_self.c  mtrace.c	 readme.md  second.c  three
    cmake-build-debug  CMakeLists.txt	 first.c  memleak.c  mtrace	     mtrace.log  second     test.c    three.c
    root@wxf:/tmp/tmp.d4vz2dOyJP# cat mtrace.log 
    = Start
    @ ./mtrace:[0x558e233f3731] + 0x558e240bf6a0 0xa
    @ ./mtrace:[0x558e233f373f] + 0x558e240bf6c0 0x14
    @ ./mtrace:[0x558e233f374d] + 0x558e240bf6e0 0x1e
    @ ./mtrace:[0x558e233f375b] + 0x558e240bf710 0x28
    @ ./mtrace:[0x558e233f376b] - 0x558e240bf6a0 
    @ ./mtrace:[0x558e233f3777] - 0x558e240bf6c0
    @ ./mtrace:[0x558e233f3783] - 0x558e240bf710
    = End
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    全文总结

      本文一共介绍了__libc_malloc, __libc_malloc ,__builtin_return_address,addr2line, __malloc_hook,mtrace。更多的是了解以下这些函数,那么对于内存泄漏检测来说,最核心的两个需求我们也解决了。

    1. 能够检测出来发生了内存泄漏
    2. 能够判断定位代码哪一行引起内存泄漏
    • 如果检测内存泄漏?我们采取malloc创建一个文件,在free的时候删除对应文件

    • 如果定位代码?我们可以使用__builtin_return_address + addr2line,但是更推荐3个宏定义的方法。

  • 相关阅读:
    vulnhub靶场之PYLINGTON: 1
    Golang log包的源码分析
    Spring Cloud Gateway集成sentinel进行网关限流
    “SonarQube requires Java 11+ to run“ for java 1.8.0_221
    麦芽糖-刀豆球蛋白A,maltose-ConcanavalinA,刀豆球蛋白A-PEG-麦芽糖
    13_Nginx_listen指令
    The method toList() is undefined for the type Stream
    Unsupervised Medical Image Translation with Adversarial Diffusion Models
    邻居好说话——冒泡排序
    python使用numpy的loadtext函数读取指定文本文件内容、读取后的数据格式为ndarray
  • 原文地址:https://blog.csdn.net/qq_42956653/article/details/126325059