3.2.5 手写内存泄漏检测组件

1.内存泄漏(a.是否有内存泄漏 b.在哪里有内存泄漏)
2.try-catch

调用malloc 没有调用free

#include 
#include 

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

    free(p1);
    free(p3);

    return 0;
}

上面这个程序造成了内存泄漏

void *_malloc(size_t size) {
    printf("_malloc\n");
}

void _free(void *ptr) {
    printf("_free\n");
}

#define malloc(size) _malloc(size)
#define free(ptr) _free(ptr)

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

    free(p1);
    free(p3);

    return 0;
}

可以根据打印值判断malloc和free个数不匹配
为了得到更详细的信息,我们可以将文件名和行号打印出来:

void *_malloc(size_t size, const char *filename, int line) {
    void *ptr = malloc(size);

    printf("[+]_malloc: %p, filename: %s, line: %d\n", ptr, filename, line);
    
    return ptr;
}

void _free(void *ptr, const char *filename, int line) {

    printf("[-]_free, filename: %s, line: %d\n", filename, line);
    
    free(ptr);
}

#define malloc(size) _malloc(size, __FILE__, __LINE__)
#define free(ptr) _free(ptr, __FILE__, __LINE__)

为了输出信息更直观,我们可以在malloc时创建一个文件,free时删除这个文件,最后看看是否有文件生成就可知道是否发生内存泄漏

void *_malloc(size_t size, const char *filename, int line) {
    void *ptr = malloc(size);

    char buff[128] = {0};
    sprintf(buff, "./mem/%p.mem", ptr);
    
    FILE *fp = fopen(buff, "w");
    fprintf(fp, "[+]addr: %p, filename: %s, line: %d\n", ptr, buff, line);

    fflush(fp);
    fclose(fp);
    //printf("[+]_malloc: %p, filename: %s, line: %d\n", ptr, filename, line);
    
    return ptr;
}

void _free(void *ptr, const char *filename, int line) {
    char buff[128] = {0};
    sprintf(buff, "./mem/%p.mem", ptr);

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

    //printf("[-]_free, filename: %s, line: %d\n", filename, line);
    
    return free(ptr);
}

进入mem目录,可以看到0x5601fae8f4b0.mem
查看文件内容:
在这里插入图片描述
可以看到更详细的泄漏内容
也可以使用hook技术

typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;
typedef void (*free_t)(void *ptr);
free_t free_f = NULL;

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

void free(void *ptr) {
    printf("free \n");
}

void init_hook(void) {
    if (!malloc_f) {
        malloc_f = dlsym(RTLD_NEXT, "malloc");
    }
    if (!free_f) {
        free_f = dlsym(RTLD_NEXT, "free");
    }
}

int main()
{
    init_hook();
    void *p1 = malloc(5);
    void *p2 = malloc(10);
    void *p3 = malloc(15);

    free(p1);
    free(p3);

    return 0;
}

但是运行该程序会发现发生了segmentation fault,这时因为printf本身也会调用malloc,造成了递归调用。

int enable_malloc_hook = 1;
int enable_free_hook = 1;

void *malloc(size_t size) {
    void *ptr = NULL;
    if (enable_malloc_hook) {
        enable_malloc_hook = 0;

        printf("malloc \n");
        ptr = malloc_f(size);

        enable_malloc_hook = 1;
    } else {
        ptr = malloc_f(size);
    }
    return ptr;
}

void free(void *ptr) {
    if (enable_free_hook) {
        enable_free_hook = 0;

        printf("free \n");

        enable_free_hook = 1;
    } else {
        free_f(ptr);
    }
}

可以做一个修改,让printf调用只调用一次。这样就不会递归调用了。
我们仍然可以在malloc时创建一个文件,在free时删除相应的文件:

void *malloc(size_t size) {
    void *ptr = NULL;
    if (enable_malloc_hook) {
        enable_malloc_hook = 0;

        printf("malloc \n");
        ptr = malloc_f(size);

        char filename[128] = {0};
        sprintf(filename, "./mem/%p.mem", ptr);

        FILE *fp = fopen(filename, "w");
        fprintf(fp, "[+]addr: %p, filename: %s, line: %d", ptr, __FILE__, __LINE__);

        enable_malloc_hook = 1;
    } else {
        ptr = malloc_f(size);
    }
    return ptr;
}

但是,这时你会发现,我们打印的行号信息始终不变,而之前是宏定义,会在使用的地方展开,但是这里是一个函数,所以行号不会跟随使用的地方发生变化。
解决这个问题可以使用__builtin_return_address

void *malloc(size_t size) {
    void *ptr = NULL;
    if (enable_malloc_hook) {
        enable_malloc_hook = 0;

        printf("malloc \n");
        ptr = malloc_f(size);

        //0参数表示上一级 1向上反二级
        //main -> f1() -> f2() -> f3() {}
        void *caller = __builtin_return_address(0);

        char filename[128] = {0};
        sprintf(filename, "./mem/%p.mem", ptr);

        FILE *fp = fopen(filename, "w");
        fprintf(fp, "[+]caller: %p, addr: %p, size: %ld", caller, ptr, size);

        fflush(fp);

        enable_malloc_hook = 1;
    } else {
        ptr = malloc_f(size);
    }
    return ptr;
}

void free(void *ptr) {
    if (enable_free_hook) {
        enable_free_hook = 0;

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

        free_f(ptr);
        enable_free_hook = 1;
    } else {
        free_f(ptr);
    }
    
}

这时再查看文件内容:
在这里插入图片描述
虽然打印出了地址,但还是不直观,这里用到一个工具addr2line
可以通过addr2line -f -e ./memleak -a 0x5595d3a06bcc查看文件和行号
还有一种通过libc的方式:

extern void *__libc_malloc(size_t size);
extern void __libc_free(void *ptr);

int enable_malloc_hook = 1;
int enable_free_hook = 1;

void *malloc(size_t size) {
    void *ptr = NULL;
    if (enable_malloc_hook) {
        enable_malloc_hook = 0;

        printf("malloc \n");
        ptr = __libc_malloc(size);

        //0参数表示上一级 1向上反二级
        //main -> f1() -> f2() -> f3() {}
        void *caller = __builtin_return_address(0);

        char filename[128] = {0};
        sprintf(filename, "./mem/%p.mem", ptr);

        FILE *fp = fopen(filename, "w");
        fprintf(fp, "[+]caller: %p, addr: %p, size: %ld", caller, ptr, size);

        fflush(fp);

        enable_malloc_hook = 1;
    } else {
        ptr = __libc_malloc(size);
    }
    return ptr;
}

void free(void *ptr) {
    if (enable_free_hook) {
        enable_free_hook = 0;

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

        __libc_free(ptr);
        enable_free_hook = 1;
    } else {
        __libc_free(ptr);
    }
    
}

try-catch

#include 
#include 

jmp_buf env;

void func(int idx) {
    printf("func --> idx: %d\n", idx);
    longjmp(env, idx);
}

int main()
{
    int count = setjmp(env);
    if (count == 0) {
        printf("count -> %d\n", count);
        func(++count);
    } else if (count == 1) {
        printf("count -> %d\n", count);
        func(++count);
    } else if (count == 2) {
        printf("count -> %d\n", count);
        func(++count);
    } else if (count == 3) {
        printf("count -> %d\n", count);
        func(++count);
    }

    printf("other count : %d\n", count);

    return 0;
}

程序输出:

count -> 0
func --> idx: 1
count -> 1
func --> idx: 2
count -> 2
func --> idx: 3
count -> 3
func --> idx: 4
other count : 4
#include 
#include 

#define TRY int count = setjmp(env); if (count == 0)
#define CATCH(x)      else if (count == (x))
#define THROW(idx) longjmp(env, (idx))
#define FINALLY

jmp_buf env;

void func(int idx) {
    printf("func --> idx: %d\n", idx);
    THROW(idx);
}

int main()
{
    TRY {
        printf("count -> %d\n", count);
        func(++count);
    } CATCH(1) {
        printf("count -> %d\n", count);
        func(++count);
    } CATCH(2) {
        printf("count -> %d\n", count);
        func(++count);
    } CATCH(3) {
        printf("count -> %d\n", count);
        func(++count);
    } FINALLY {
        printf("other count : %d\n", count);
    }
    return 0;
}

但同时存在两个问题:

  1. try/catch嵌套
    用单链表头插法
  2. 线程安全问题
    pthread_key_t

文章参考于<零声教育>的C/C++linux服务期高级架构系统教程学习:https://ke.qq.com/course/417774?flowToken=1020253

你可能感兴趣的:(零声教育,内存泄漏)