美文网首页
C++不用工具,如何检测内存泄漏?

C++不用工具,如何检测内存泄漏?

作者: DayDayUpppppp | 来源:发表于2021-07-12 22:26 被阅读0次

    在知乎上面看到一个很有意思的问题,“C++不用工具,如何检测内存泄漏?”。年入百万的知乎科学家大概有四个思路,一种思路是重载operator new,一种思路是hook malloc,另外一种是使用内存池。

    关于内存管理的一些前置背景知识:
    https://github.com/zhaozhengcoder/CoderNoteBook/blob/master/note/c++%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md

    1. 重载operator new

    在C++中operator new是可以被重载的,一般有两种重载的方式,全局重载和在某个类内重载,这两种的区别就不在这里展开写的。通过重载一个全局的operator new的函数,在new的时候附加上参数文件名和行号。然后,定义一个新的宏,在new的时候带上申请内存的文件名和行号。这样当业务代码new的时候,会调用重载的operator new函数,这样就获得了申请内存的代码文件名和行号,甚至都可以带上backtrace做更加精确的定位。

    // libc中的定义
    void* operator new(std::size_t sz)
    
    // 定义一个宏,给new附带上两个参数 文件名和行号
    #define new new(__LINE__,__FILE__)
    
    // 重载operator new
    void* operator new (size_t size , const char *file , 
            unsigned int line ) {
        if (void *ptr = malloc (size)) 
        {
            // print backtrace();
            cout << endl << "new : " << file << " "<< line << endl ;
            return ptr ;
        }  
        throw std::bad_alloc () ;
    }
    
    // 重新实现operator delete (void *ptr) 
    // 由于c++lib中operator delete (void *ptr) 是一个弱符号的函数,所以可以重新实现一个,且不会出现重定义
    void operator delete (void *ptr) 
    {
        if (ptr == nullptr) return ;
    
        cout << ptr << "\tsource has been released !" << endl ;
        free (ptr);
        ptr = nullptr;
    }
    

    这种方案会两个小问题:

    1. new的宏定义只有include这个宏的地方才会生效,对于库函数或者无法include宏的地方就没有办法插入log。
    2. 只能记录内存是被哪些逻辑分配出去的,并没有非常准确的定位到是那块内存泄漏了,需要根据申请内存的日志去进一步分析。(比如一些通用的内存信息,有非常多的业务逻辑都会调用申请,那么知道了是这个类型的内存泄露只能缩小问题的范围,并没有办法准确定位)。

    对于问题1,可以重新实现operator new来解决。因为libc中默认的operator new是weak符号,可以重新实现一个strong符号去覆盖。对于没有include宏的地方,也可以进入void * operator new (size_t size)的函数中。

    void * operator new (size_t size) 
    { 
        cout << endl << "new size : " << size << endl ;
        void *p = malloc(size); 
        return p;
    } 
    
    int main()
    {
        {
            int * q1 = new int();
            delete q1;
        }
        {
            // 底层的内存分配算法也会走重新实现的operator new函数
            shared_ptr<A> a = make_shared<A>();
        }
    }
    

    对于问题2,见到过有个很有意思的实现方式,可以在分配内存的时候,额外多申请一块(head + 实际的内容),head中记录申请内存的文件名和行号,然后将head插入到一个全局的list中。在free的时候,根据free的指针,ptr-sizeof(head) 找到head的头部。把head从全局的list中释放掉。当程序结束的时候,全局的list中就维护了申请但是还没有释放的内存信息。执行效果如下:

    自己实现了一个乞丐版:
    https://github.com/zhaozhengcoder/CoderNoteBook/tree/master/example_code/memleak_check/mem_leak_tool


    2. hook malloc

    malloc函数和operator new不一样的是,它并不是一个weak的符号(但在glibc下是一个弱符号),没有办法想new一样,重新实现一个去覆盖。因此,会看到很多hook malloc的方案,常见的有:

    1. 重新实现一个malloc函数,通过preload 动态库的方式去覆盖默认的malloc函数;
    2. 另一种思路是使用 wrap 编译参数(好像也叫编译垫片)

    关于什么是弱符号和强符号,正常情况下我们定义的函数都是强符号,这样如果定义了两个相同强符号,就会出现重定义的报错。因此,很多库函数的实现思路是,定义为弱符号,这样链接的时候,如果出现了同名的强符号,就会被覆盖,而不是报错。关于什么是强符号和弱符号

    • preload动态库的思路是:
      malloc函数是在libc.so库中,loader在进行动态链接的时候,会将有相同符号名的符号覆盖成LD_PRELOAD指定的so文件中的符号。换句话说,可以用我们自己的so库中的函数替换原来库里有的函数,从而达到hook的目的。

      // main.cpp
      int main(int argc, char **argv)
      {
        // ...
      
        // hock strcmp函数  
        if (!strcmp(passwd, argv[1]))
        {
            printf("Correct Password!\n");
            return 0;
        }   
      
        printf("Invalid Password!\n");
        return 1;
      }
      
      // hack.cpp
      #define _GNU_SOURCE
      
      #include <stdio.h>
      #include <dlfcn.h>
      
      extern "C" int strcmp(const char *s1, const char *s2)
      {
        printf("hack function invoked. s1=<%s> s2=<%s>\n", s1, s2);
        return 0;
      }
      
      g++ -Wall -fPIC -shared -o hack.so hack.cpp -ldl
      g++ checkpasswd.cpp -o checkpasswd
      LD_PRELOAD=./hack.so ./checkpasswd 90
      
      // 系统strcmp函数已经被自己实现的strcmp hook
      

      假设要统计molloc被调用的次数,也可以利用preload的方式去hook。main.c的定义如下:

      #include <stdio.h>
      #include <stdlib.h>
      
      int main(int argc, char** argv) {
        int* p = (int *)malloc(sizeof(int));
        free(p);
        return 0;
      }
      

      利用 dlsym 编写如下文件 dlsym_test_preload.c:

      #include <stdio.h>
      #include <dlfcn.h>
      
      static unsigned int invoke_times = 0;
      
      void* malloc(size_t sz) { 
        void* (*my_malloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
        invoke_times += 1;
        printf("my malloc invoked\n");
        return my_malloc(sz);
      }
      

      同样可以把dlsym_test_preload.c:编译为一个动态库,然后preload这个动态库之后,运行main程序,实现hook malloc函数。

    1. 使用编译器选项wrap重载malloc函数

    ld链接选项wrap定义

    按照文档的说法,加上wrap的选项之后,调用malloc的时候,会调用wrap的版本。而系统的malloc被重命名为real_malloc,可以通过调用real_malloc来调用系统malloc函数

    extern "C" void * __real_malloc(size_t size);
    extern "C" void __real_free(void *ptr);
    
    extern "C" void * __wrap_malloc(size_t size) {
        printf("my malloc: %zu\n", size);
        return __real_malloc(size);
    }
    
    extern "C" void __wrap_free(void *ptr) {
        printf("my free: %p\n", ptr);
        __real_free(ptr);
    }
    
    // g++ main.cpp test1.cpp test2.cpp memory_op.cpp -Wl,-wrap=malloc -Wl,-wrap=free
    

    自己实现了一个乞丐版:
    https://github.com/zhaozhengcoder/CoderNoteBook/tree/master/example_code/memleak_check/mem_hook

    3. 引入内存池解决

    业务层实现内存池来进行分配,这样内存分配上面业务层有更高的权限,也可以做更多的事情来掌控内存的分配。比如定义malloc(size, type) ,业务层分配内存的时候,带上一个type,这样就很容易监控到某某type分配了多少块。
    http://www.almostinfinite.com/memtrack.html

    4. 使用工具

    内存泄漏检测工具
    valgrind、ASan、mtrace、ccmalloc、debug_new

    对于内存方面的常见错误,valgrind还是很好使用的。

    // 内存泄漏
    int mem_leak()
    {
        int * p = new int;
        int * arr = new int[100];
        return 0;
    }
    
    // 内存访问越界
    int mem_out_of_boundary()
    {
        const int size = 10;
        int * arr = new int[size];
    
        for (int i = 0; i <= size; i++)
        {
            arr[i] = i;
        }
        return 0;
    }
    
    // free后再次访问
    int mem_use_after_free()
    {
        const int size = 10;
        int * arr = new int[size];
    
        delete[] arr;
    
        arr[0] = 100;
        return 0;
    }
    
    // 多次free
    int mem_invalid_free()
    {
        const int size = 10;
        int * arr = new int[size];
        int * p = arr;
        delete arr;
        delete p;
        return 0;
    }
    
    # valgrind --tool=memcheck ./a.out
    

    其他补充:

    1. 强符号和弱符号的区别

    2. 两个静态库or动态库包含同样的符号,链接会怎么样
      https://zhuanlan.zhihu.com/p/352668522
      https://blog.csdn.net/Solstice/article/details/6423342

    (写不动了,后面再说吧。。

    相关文章

      网友评论

          本文标题:C++不用工具,如何检测内存泄漏?

          本文链接:https://www.haomeiwen.com/subject/gulcpltx.html