我撸了个内存泄漏检测工具,只用了两招

开发 开发工具
不管使用什么语言,一定要处理好内存问题,要有检测内存问题的方法论,于是撸了个检测是否有泄漏的小工具,这里分享一波。

 [[389657]]

本文转载自微信公众号「程序喵大人」,作者程序喵大人。转载本文请联系程序喵大人公众号。

大家看我写了这么长时间C++文章,殊不知我在工作中已经一年多没有用过C++了,最近做一个新项目,终于又回到C++的怀抱了,有点激动,也有点不适应。

不管使用什么语言,一定要处理好内存问题,要有检测内存问题的方法论,于是撸了个检测是否有泄漏的小工具,这里分享一波。

先贴个效果图:

实现方法

众所周知C++中申请和释放内存使用的是new和delete关键字:

  1. void func() { 
  2.     A* a = new A(); 
  3.     delete a; 
  4.     A* b = new int[4]; 
  5.     delete[] b; 

再明确下需求:如果程序中存在内存泄漏,我们的目的是找到这些内存是在哪里分配的,如果能够具体对应到代码中哪一个文件的那一行代码最好。好了需求明确了,开始实现。

内存在哪里释放的我们没必要监测,只需要检测出内存是在哪里申请的即可,如何检测呢?

整体思路很简单:在申请内存时记录下该内存的地址和在代码中申请内存的位置,在内存销毁时删除该地址对应的记录,程序最后统计下还有哪条记录没有被删除,如果还有没被删除的记录就代表有内存泄漏。

很多人应该都知道new关键字更底层是通过operator new来申请内存的:

  1. void* operator new(std::size_t sz) 

也就是正常情况下C++都是通过operator new(std::size_t sz)来申请内存,而这个操作符我们可以重载:

  1. void* operator new(std::size_t size, const char* file, int line); 
  2. void* operator new[](std::size_t size, const char* file, int line); 

tip:new和new[]的区别我就不具体介绍了,太基础。

如果能让程序申请内存时调用重载的这个函数,就可以记录下内存申请的具体位置啦。

怎么能够让底层程序申请内存时调用重载的这个函数呢?这里可以对new使用宏定义:

  1. #define new new (__FILE__, __LINE__) 

有了这个宏定义后,在new A的时候底层就会自动调用operator new(std::size_t size, const char* file, int line)函数,至此达到了我们记录内存申请位置的目的。

这里有两个问题:

  1. 在哪里记录内存申请的位置等信息呢?如果在operator new内部又申请了一块内存,用于记录位置,那新申请的这块内存需要记录不?这岂不是递归调用了?
  2. 只有在new宏定义包裹范围内申请了内存才会被记录,然而某些第三方库或者某些地方没有被new宏定义包裹,可能就无法被监测是否申请了内存吧?

下面逐个击破:

哪里存储具体信息?

我们肯定不能让它递归调用啊,那这些信息存储在哪里呢?这里可以在每次申请内存时,一次性申请一块稍微大点的内存,具体信息存储在多余的那块内存里,像这样:

  1. static void* alloc_mem(std::size_t size, const char* file, int line, bool is_array) { 
  2.     assert(line >= 0); 
  3.  
  4.     std::size_t s = size + ALIGNED_LIST_ITEM_SIZE; 
  5.     new_ptr_list_t* ptr = (new_ptr_list_t*)malloc(s); 
  6.     if (ptr == nullptr) { 
  7.         std::unique_lock<std::mutex> lock(new_output_lock); 
  8.         printf("Out of memory when allocating %lu bytes\n", (unsigned long)size); 
  9.         abort(); 
  10.     } 
  11.     void* usr_ptr = (char*)ptr + ALIGNED_LIST_ITEM_SIZE; 
  12.  
  13.     if (line) { 
  14.         strncpy(ptr->file, file, _DEBUG_NEW_FILENAME_LEN - 1)[_DEBUG_NEW_FILENAME_LEN - 1] = '\0'
  15.     } else { 
  16.         ptr->addr = (void*)file; 
  17.     } 
  18.  
  19.     ptr->line = line; 
  20.     ptr->is_array = is_array; 
  21.     ptr->size = size
  22.     ptr->magic = DEBUG_NEW_MAGIC; 
  23.     { 
  24.         std::unique_lock<std::mutex> lock(new_ptr_lock); 
  25.         ptr->prev = new_ptr_list.prev; 
  26.         ptr->next = &new_ptr_list; 
  27.         new_ptr_list.prev->next = ptr; 
  28.         new_ptr_list.prev = ptr; 
  29.     } 
  30.     total_mem_alloc += size
  31.     return usr_ptr; 

new_ptr_list_t结构体定义如下:

  1. struct new_ptr_list_t { 
  2.     new_ptr_list_t* next
  3.     new_ptr_list_t* prev; 
  4.     std::size_t size
  5.     union { 
  6.         char file[200]; 
  7.  
  8.         void* addr; 
  9.     }; 
  10.     unsigned line; 
  11. }; 

没有被new宏包裹的地方可以检测的到吗?

没有被new宏包裹的地方是会调用operator new(std::size_t sz)函数来申请内存的。这里operator new函数不只可以重载,还可以重新定义它的实现,而且不会报multi definition的错误哦。因为它是一个weak symbol,有关strong symbol和weak symbol的知识点可以看我之前的一篇文章:《谈谈程序链接及分段那些事》

既然可以重定义,那就可以这样:

  1. void* operator new(std::size_t size) {  
  2.     return operator new(size, nullptr, 0);  

这样有个缺点,就是不能记录内存申请的具体代码位置,只能记录下来是否申请过内存,不过这也挺好,怎么也比没有任何感知强的多。

其实这里不是没有办法,尽管没有了new宏,获取不到具体申请内存的代码位置,但是可以获取到调用栈信息,把调用栈信息存储起来,还是可以定位大体位置。关于如何获取调用栈信息,大家可以研究下libunwind库看看。

释放内存时怎么办?

这里需要重定义operator delete(void* ptr)函数:

  1. void operator delete(void* ptr) noexcept {  
  2.     free_pointer(ptr, nullptr, false);  

free_pointer函数的大体思路就是在链表中找到要对应节点,删除掉,具体定义如下:

  1. static void free_pointer(void* usr_ptr, void* addr, bool is_array) { 
  2.     if (usr_ptr == nullptr) { 
  3.         return
  4.     } 
  5.     new_ptr_list_t* ptr = (new_ptr_list_t*)((char*)usr_ptr - ALIGNED_LIST_ITEM_SIZE); 
  6.     { 
  7.         std::unique_lock<std::mutex> lock(new_ptr_lock); 
  8.         total_mem_alloc -= ptr->size
  9.         ptr->magic = 0; 
  10.         ptr->prev->next = ptr->next
  11.         ptr->next->prev = ptr->prev; 
  12.     } 
  13.     free(ptr); 

如何检测是否有内存泄漏?

遍历链表即可,每次new时候会把这段内存插入链表,delete时候会把这段内存从链表中移出,如果程序最后链表长度不为0,即为有内存泄漏,代码如下:

  1. int checkLeaks() { 
  2.     int leak_cnt = 0; 
  3.     int whitelisted_leak_cnt = 0; 
  4.     new_ptr_list_t* ptr = new_ptr_list.next
  5.  
  6.     while (ptr != &new_ptr_list) { 
  7.         const char* const usr_ptr = (char*)ptr + ALIGNED_LIST_ITEM_SIZE; 
  8.         printf("Leaked object at %p (size %lu, ", usr_ptr, (unsigned long)ptr->size); 
  9.         if (ptr->line != 0) { 
  10.             print_position(ptr->file, ptr->line); 
  11.         } else { 
  12.             print_position(ptr->addr, ptr->line); 
  13.         } 
  14.         printf(")\n"); 
  15.         ptr = ptr->next
  16.         ++leak_cnt; 
  17.     } 
  18.     return leak_cnt; 

ps:关于可以重定义operator new这个操作,我也是最近看到别人代码后才发现,于是参考别人代码小撸了个代码检测工具,希望大家有所收获!

 

责任编辑:武晓燕 来源: 程序喵大人
相关推荐

2021-11-05 08:29:13

数据校验Spring

2017-05-24 17:25:44

2024-07-03 11:28:15

2013-08-02 09:52:14

AndroidApp内存泄漏

2021-12-23 11:10:38

税收大数据大数据税收

2017-09-07 16:52:23

2019-09-09 11:35:21

GitHub工具浏览

2020-11-04 07:56:19

工具Linux 翻译

2015-04-17 10:35:51

c++c++程序内存泄漏检测代码

2020-03-06 08:15:54

新人技术主管陈琦

2016-09-12 16:01:28

Android内存泄露内存管理

2011-08-15 10:16:55

内存泄露

2024-04-19 08:00:00

2024-04-19 15:55:01

系统设计绘图工具

2023-07-17 07:06:51

电脑管家C盘文件

2009-02-01 09:42:00

2022-03-25 14:11:11

Java死锁线程

2015-05-18 09:23:04

JavaScript数组

2018-05-23 16:56:40

戴尔

2009-08-07 12:12:00

C#读取Excel文件
点赞
收藏

51CTO技术栈公众号