当前位置: 首页 > 科技观察

自己推出了一款内存泄漏检测工具,只用了两招

时间:2023-03-20 00:18:42 科技观察

转载请联系喵喵大程序公众号。你看我写了这么长时间的C++文章,但是我工作中已经一年多没有用到C++了。最近在做一个新的项目,终于又回到了C++的怀抱。我有点兴奋,也有点不自在。不管用什么语言,都要处理好内存问题,一定要有检测内存问题的方法论,所以我做了一个检测是否有泄漏的小工具,分享到这里。先贴一张效果图:实现方法众所周知。C++中new和delete关键字用于申请和释放内存:voidfunc(){A*a=newA();deleta;A*b=newint[4];delete[]b;}再次明确要求:if程序中存在内存泄漏,我们的目的是找出内存分配在哪里,如果能在代码中具体对应到哪个文件中的哪一行代码就最好了。好了,需求明确了,开始实现。我们不需要监视释放内存的位置。我们只需要检测在哪里申请了内存。如何检测?整体思路很简单:申请内存时在代码中记录内存的地址和申请内存的位置,销毁内存时删除该地址对应的记录,程序最后统计哪些记录没有被删除。如果还有记录没有被删除,说明存在内存泄漏。很多人应该都知道new关键字的下层是通过operatornew来申请内存的:void*operatornew(std::size_tsz),也就是一般情况下,C++都是通过operatornew(std::size_tsz)来申请内存的申请内存。我们可以重载这个运算符:void*operatornew(std::size_tsize,constchar*file,intline);void*operatornew[](std::size_tsize,constchar*file,intline);tip:new和new[]的区别我就不详细介绍了,太基础了。如果能在程序申请内存的时候调用重载函数,就可以记录申请内存的具体位置。底层程序在申请内存时如何调用重载的函数呢?这里可以对new使用一个宏定义:#definenewnew(__FILE__,__LINE__)有了这个宏定义,底层会在new一个(std::size_tsize,constchar*file,intline)函数的时候自动调用operatornew,到目前为止,我们已经实现了记录内存应用程序位置的目标。这里有两个问题:在哪里记录申请内存的位置等信息?如果在operatornew中申请了另一块内存来记录位置,是否需要记录新申请的那块内存?这不是递归调用吗??只有在新宏定义包的范围内应用内存才会记录内存。但是有些第三方库或者有些地方没有被新的宏定义包包裹,所以可能无法监控是否申请了内存?我们一一分解:具体信息存储在哪里?我们肯定不能让它递归调用,那么这些信息存储在哪里呢?这里每次申请内存可以一次性申请一块稍微大一点的内存,具体信息保存在额外的内存中,像这样:staticvoid*alloc_mem(std::size_tsize,constchar*file,intline,boolis_array){assert(line>=0);std::size_ts=size+ALIGNED_LIST_ITEM_SIZE;new_ptr_list_t*ptr=(new_ptr_list_t*)malloc(s);if(ptr==nullptr){std::unique_locklock(new_output_lock);printf("Outofmemorywhenallocating%lubytes\n",(unsignedlong)size);abort();}void*usr_ptr=(char*)ptr+ALIGNED_LIST_ITEM_SIZE;if(line){strncpy(ptr->file,file,_DEBUG_NEW_FILENAME_LEN-1)[_DEBUG_NEW_FILENAME_LEN-1]='\0';}else{ptr->addr=(void*)文件;}ptr->line=line;ptr->is_array=is_array;ptr->size=size;ptr->magic=DEBUG_NEW_MAGIC;{std::unique_locklock(new_ptr_lock);ptr->prev=new_ptr_list.prev;ptr->next=&new_ptr_list;new_ptr_list.prev->next=ptr;new_ptr_list.prev=ptr;}total_mem_alloc+=size;returnusr_ptr;}new_ptr_list_t结构定义如下:structnew_ptr_list_t{new_ptr_list_t*next;new_ptr_list_t*prev;std::size_tsize;union{charfile[200];void*addr;};unsignedline;};没有被新宏包裹的地方能检测出来吗?没有被new宏包裹的地方会调用operatornew(std::size_tsz)函数申请内存这里operatornew函数不仅可以重载,还可以重新定义其实现,不会报多定义错误。因为是弱符号,可以看我之前关于强符号和弱符号的文章:《谈谈程序链接及分段那些事》既然可以重新定义,那么可以这样:void*operatornew(std::size_tsize){returnoperatornew(size,nullptr,0);}这个有一个缺点,就是不能记录申请内存的具体代码位置,只能记录是否申请了内存,不过这个也很好,比一点感知都没有。其实这里也没有办法。虽然没有新建宏,无法获取到具体申请内存的代码位置,但是可以获取到调用栈信息,通过存储调用栈信息可以定位到大概位置。关于如何获取调用栈信息,可以研究一下libunwind库。释放内存时做什么?这里需要重新定义operatordelete(void*ptr)函数:链表、删除,具体定义如下:usr_ptr-ALIGNED_LIST_ITEM_SIZE);{std::unique_locklock(new_ptr_lock);total_mem_alloc-=ptr->size;ptr->magic=0;ptr->prev->next=ptr->next;ptr->next->prev=ptr->prev;}free(ptr);}如何检测是否有内存泄漏?遍历链表即可。每次new的时候都会把这块内存插入到链表中,delete的时候把这块内存从链表中移除。如果程序末尾的链表长度不为0,则存在内存泄漏。代码如下:intcheckLeaks(){intleak_cnt=0;intwhitelisted_leak_cnt=0;new_ptr_list_t*ptr=new_ptr_list.next;while(ptr!=&new_ptr_list){constchar*constusr_ptr=(char*)ptr+ALIGNED_LIST_ITEM_SIZE;printf("Leakedobjectat%p(size%lu,",usr_ptr,(unsignedlong)ptr->size);if(ptr->line!=0){print_position(ptr->file,ptr->line);}else{print_position(ptr->addr,ptr->line);}printf(")\n");ptr=ptr->next;++leak_cnt;}returnleak_cnt;}ps:关于重定义operatornew的操作,最近看了别人的代码才发现,所以参考针对别人的代码,我做了一个代码检测小工具,希望大家有所收获!