我擼了個內存泄漏檢測工具,只用了兩招
本文轉載自微信公眾號「程序喵大人」,作者程序喵大人。轉載本文請聯(lián)系程序喵大人公眾號。
大家看我寫了這么長時間C++文章,殊不知我在工作中已經(jīng)一年多沒有用過C++了,最近做一個新項目,終于又回到C++的懷抱了,有點激動,也有點不適應。
不管使用什么語言,一定要處理好內存問題,要有檢測內存問題的方法論,于是擼了個檢測是否有泄漏的小工具,這里分享一波。
先貼個效果圖:
實現(xiàn)方法
眾所周知C++中申請和釋放內存使用的是new和delete關鍵字:
- void func() {
- A* a = new A();
- delete a;
- A* b = new int[4];
- delete[] b;
- }
再明確下需求:如果程序中存在內存泄漏,我們的目的是找到這些內存是在哪里分配的,如果能夠具體對應到代碼中哪一個文件的那一行代碼最好。好了需求明確了,開始實現(xiàn)。
內存在哪里釋放的我們沒必要監(jiān)測,只需要檢測出內存是在哪里申請的即可,如何檢測呢?
整體思路很簡單:在申請內存時記錄下該內存的地址和在代碼中申請內存的位置,在內存銷毀時刪除該地址對應的記錄,程序最后統(tǒng)計下還有哪條記錄沒有被刪除,如果還有沒被刪除的記錄就代表有內存泄漏。
很多人應該都知道new關鍵字更底層是通過operator new來申請內存的:
- void* operator new(std::size_t sz)
也就是正常情況下C++都是通過operator new(std::size_t sz)來申請內存,而這個操作符我們可以重載:
- void* operator new(std::size_t size, const char* file, int line);
- void* operator new[](std::size_t size, const char* file, int line);
tip:new和new[]的區(qū)別我就不具體介紹了,太基礎。
如果能讓程序申請內存時調用重載的這個函數(shù),就可以記錄下內存申請的具體位置啦。
怎么能夠讓底層程序申請內存時調用重載的這個函數(shù)呢?這里可以對new使用宏定義:
- #define new new (__FILE__, __LINE__)
有了這個宏定義后,在new A的時候底層就會自動調用operator new(std::size_t size, const char* file, int line)函數(shù),至此達到了我們記錄內存申請位置的目的。
這里有兩個問題:
- 在哪里記錄內存申請的位置等信息呢?如果在operator new內部又申請了一塊內存,用于記錄位置,那新申請的這塊內存需要記錄不?這豈不是遞歸調用了?
- 只有在new宏定義包裹范圍內申請了內存才會被記錄,然而某些第三方庫或者某些地方?jīng)]有被new宏定義包裹,可能就無法被監(jiān)測是否申請了內存吧?
下面逐個擊破:
哪里存儲具體信息?
我們肯定不能讓它遞歸調用啊,那這些信息存儲在哪里呢?這里可以在每次申請內存時,一次性申請一塊稍微大點的內存,具體信息存儲在多余的那塊內存里,像這樣:
- static void* alloc_mem(std::size_t size, const char* file, int line, bool is_array) {
- assert(line >= 0);
- std::size_t s = size + ALIGNED_LIST_ITEM_SIZE;
- new_ptr_list_t* ptr = (new_ptr_list_t*)malloc(s);
- if (ptr == nullptr) {
- std::unique_lock<std::mutex> lock(new_output_lock);
- printf("Out of memory when allocating %lu bytes\n", (unsigned long)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*)file;
- }
- ptr->line = line;
- ptr->is_array = is_array;
- ptr->size = size;
- ptr->magic = DEBUG_NEW_MAGIC;
- {
- std::unique_lock<std::mutex> lock(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;
- return usr_ptr;
- }
new_ptr_list_t結構體定義如下:
- struct new_ptr_list_t {
- new_ptr_list_t* next;
- new_ptr_list_t* prev;
- std::size_t size;
- union {
- char file[200];
- void* addr;
- };
- unsigned line;
- };
沒有被new宏包裹的地方可以檢測的到嗎?
沒有被new宏包裹的地方是會調用operator new(std::size_t sz)函數(shù)來申請內存的。這里operator new函數(shù)不只可以重載,還可以重新定義它的實現(xiàn),而且不會報multi definition的錯誤哦。因為它是一個weak symbol,有關strong symbol和weak symbol的知識點可以看我之前的一篇文章:《談談程序鏈接及分段那些事》
既然可以重定義,那就可以這樣:
- void* operator new(std::size_t size) {
- return operator new(size, nullptr, 0);
- }
這樣有個缺點,就是不能記錄內存申請的具體代碼位置,只能記錄下來是否申請過內存,不過這也挺好,怎么也比沒有任何感知強的多。
其實這里不是沒有辦法,盡管沒有了new宏,獲取不到具體申請內存的代碼位置,但是可以獲取到調用棧信息,把調用棧信息存儲起來,還是可以定位大體位置。關于如何獲取調用棧信息,大家可以研究下libunwind庫看看。
釋放內存時怎么辦?
這里需要重定義operator delete(void* ptr)函數(shù):
- void operator delete(void* ptr) noexcept {
- free_pointer(ptr, nullptr, false);
- }
free_pointer函數(shù)的大體思路就是在鏈表中找到要對應節(jié)點,刪除掉,具體定義如下:
- static void free_pointer(void* usr_ptr, void* addr, bool is_array) {
- if (usr_ptr == nullptr) {
- return;
- }
- new_ptr_list_t* ptr = (new_ptr_list_t*)((char*)usr_ptr - ALIGNED_LIST_ITEM_SIZE);
- {
- std::unique_lock<std::mutex> lock(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,即為有內存泄漏,代碼如下:
- int checkLeaks() {
- int leak_cnt = 0;
- int whitelisted_leak_cnt = 0;
- new_ptr_list_t* ptr = new_ptr_list.next;
- while (ptr != &new_ptr_list) {
- const char* const usr_ptr = (char*)ptr + ALIGNED_LIST_ITEM_SIZE;
- printf("Leaked object at %p (size %lu, ", usr_ptr, (unsigned long)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;
- }
- return leak_cnt;
- }
ps:關于可以重定義operator new這個操作,我也是最近看到別人代碼后才發(fā)現(xiàn),于是參考別人代碼小擼了個代碼檢測工具,希望大家有所收獲!