本人在逛知乎的時候,看到一個問題
<>
, 不乏很多高手的回答。我正好也寫了幾篇通過工具去分析內(nèi)存泄露的文章,那先說說工具的方法原理:- 對內(nèi)存的分配的監(jiān)測: 記錄內(nèi)存申請時候函數(shù)調(diào)用棧。一種方法是通過gflag配置讓程序在分配內(nèi)存的時候,記錄函數(shù)調(diào)用棧;還有一種就是通過hook的方式去獲取申請內(nèi)存時候函數(shù)調(diào)用時候的位置。
- 對比程序運行時兩個不同時間點的內(nèi)存分配狀況,通過對比找到較多的內(nèi)存分配點對應(yīng)的函數(shù)調(diào)用棧
知其然知其所以然
。微軟Debug CRT庫檢測C 內(nèi)存泄露原理?
我們先來解決上述的兩個問題。問題一: 如何獲取函數(shù)調(diào)用棧?
那么你首先要知道什么時候申請了內(nèi)存?在C 中也就關(guān)鍵字new
或者函數(shù)malloc
,等等。那如何感知到呢?我們知道hook
大致可以理解為就是改變原有的函數(shù)調(diào)用地址,改為你自己實現(xiàn)的函數(shù)。是不是有點類似于python
中的裝飾器了,在自定義的函數(shù)內(nèi)部實現(xiàn)一些邏輯。不過本文要講的不是hook,而是宏替換。以malloc
為例,我們是不是可以通過宏定義,將malloc
更改為my_malloc
,然后在my_malloc
中記錄這次內(nèi)存申請的信息。然后記錄的信息要包括:- 申請的內(nèi)存信息,比如申請的內(nèi)存狀態(tài)
- 申請內(nèi)存時候函數(shù)調(diào)用棧,一般來說可以通過
StackWalk
獲取。不過本文講解的微軟DBUG的CRT庫采用的是另外的方式,記錄內(nèi)存申請時候文件名和行號等信息。這樣雖然沒有函數(shù)調(diào)用棧精確,但是也基本可以用于定位問題了。
Visual Studio 2017
)中,選擇工程的默認的Debug
模式,并且工程配置宏定義_CRTDBG_MAP_ALLOC
, 此時將宏定義替換malloc
為_malloc_dbg
。注意看新的函數(shù)會傳入文件名字__FILE__
和所在行__LINE__
#define?malloc(s)?_malloc_dbg(s,?_NORMAL_BLOCK,?__FILE__,?__LINE__)
那么malloc
做的事情和_malloc_dbg
有什么區(qū)別呢? 在Release
版本中malloc
底層其實就直接調(diào)用HeapAlloc
申請內(nèi)存(VS2017中)。而_malloc_dbg
會申請額外的空間用來做調(diào)試用。如下圖所示: 在_malloc_dbg
中在實際要用的內(nèi)存UserPtr
前面還加了一段_CrtMemBlockHeader
用于記錄內(nèi)存申請的相關(guān)信息,而No Main's Land
部分為一個4個字節(jié)填充了0xFDFDFDFD
,主要用來校驗內(nèi)存是否溢出或者破壞,這個不是本文的重點。接下來看看_CrtMemBlockHeader
是如何記錄調(diào)用相關(guān)的信息的呢? 我們看下它的結(jié)構(gòu)便一目了然。其是一個雙向鏈表
的節(jié)點
,有前后指針,還有文件名
,行號
等。struct?_CrtMemBlockHeader
{
????_CrtMemBlockHeader*?_block_header_next;
????_CrtMemBlockHeader*?_block_header_prev;
????char?const*?????????_file_name;
????int?????????????????_line_number;
????int?????????????????_block_use;
????size_t??????????????_data_size;
????long????????????????_request_number;
????unsigned?char???????_gap[no_mans_land_size];
????//?Followed?by:
????//?unsigned?char????_data[_data_size];
????//?unsigned?char????_another_gap[no_mans_land_size];
};
那么當(dāng)申請了內(nèi)存后,這些內(nèi)存的關(guān)系是如何的呢,如下圖:那通過以上方法我們便可以對每一個內(nèi)存申請做記錄了,而這個記錄則存儲在全局的鏈表中__acrt_first_block
。那么內(nèi)存釋放的時候,是如何進行釋放的呢?同樣的free
也會通過宏替換為_free_dbg
,這里在進行內(nèi)存釋放的時候,會根據(jù)UserPtr
尋找到對應(yīng)的_CrtMemBlockHeader
, 也就知道了鏈表節(jié)點的位置,雙向鏈表,也便于我們刪除節(jié)點。看到這里可能有同學(xué)會發(fā)現(xiàn)了,那還有C 的關(guān)鍵字new
和delete
呢。首先我們要知道new是C 的關(guān)鍵字,對于有構(gòu)造函數(shù)的類一般做了以下兩個事情:- 申請對象所需的內(nèi)存空間。而這個時候內(nèi)部其實調(diào)用的是函數(shù)
operator new
或者operator new[]
- 調(diào)用對象的構(gòu)造函數(shù)
????void*?__CRTDECL?operator?new(
????????size_t?const?size,
????????int?const????block_use,
????????char?const*??file_name,
????????int?const????line_number
????????)
本人沒有找到哪個頭文件直接定義了宏替換,那么我們可以自己寫一個宏進行替換如下:#define?new?new(_NORMAL_BLOCK,?__FILE__,?__LINE__)?
那么不難理解其他的內(nèi)存操作函數(shù)如何去做替換了吧。問題二: 對比不同時間點的內(nèi)存分配情況
那么我們?nèi)绾稳Ρ饶??我先寫了一個樣例程序:#define?_CRTDBG_MAP_ALLOC
#include?
#include?
#define?new???new(_NORMAL_BLOCK,?__FILE__,?__LINE__)?
int?main()
{
?//_CRTDBG_REPORT_FLAG:表示獲取當(dāng)前的標(biāo)示位
????//_CRTDBG_LEAK_CHECK_DF:表示檢測內(nèi)存泄露
?_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)?|?_CRTDBG_LEAK_CHECK_DF);
?int?iSize?=?100;
?char?*?pStr?=?new?char?[iSize];
?pStr?=?(char*)malloc(iSize);
?strcpy_s(pStr,?iSize,?"Memory?Leak!");
?_CrtDumpMemoryLeaks();
?return?0;
}
因為這個是一個簡單的樣例程序,但是足以說明是如何檢測的。- 一種方式是自己在程序中主動打印出來可能泄露的內(nèi)存。這個時候其實就是遍歷上述的雙向鏈表,查看正在使用的內(nèi)存,并將其打印到Visual Studio的output窗口中。
- 另一種方式就是設(shè)置
_CRTDBG_LEAK_CHECK_DF
這個標(biāo)記位,則在main函數(shù)退出后,在Debug的CRT庫中主動調(diào)用了_CrtDumpMemoryLeaks
。其實和方法1原理一樣,只是時間點不同。
Output
窗口中,如下圖所示。總結(jié)
簡單總結(jié)下,微軟Debug CRT庫的實現(xiàn),完全可以在項目中自己實現(xiàn)。就是通過在申請的內(nèi)存頭部記錄當(dāng)前分配內(nèi)存的相關(guān)信息,比如文件名
和行號
,并且通過雙向鏈表將所有申請的節(jié)點串起來。然后在合適的時間點(比如感知到內(nèi)存泄露的情況下)打印出可能的內(nèi)存泄露的內(nèi)存關(guān)聯(lián)的信息。這種做法簡單,但只針對小型的項目,適合采用這種方法,而且對于第三方庫的內(nèi)存泄露無法進行檢測。本文旨在通過分析微軟Debug CRT庫的實現(xiàn)的檢測內(nèi)存泄露的方式,從而闡述自我實現(xiàn)簡易C 內(nèi)存泄露檢測的思想。若平時分析內(nèi)存泄露問題,建議還是采用本文開頭提到的幾篇文章的方法。參考
- Walking the callstack:https://www.codeproject.com/Articles/11132/Walking-the-callstack-2
- C 不用工具,如何檢測內(nèi)存泄漏?:https://www.zhihu.com/question/29859828
- new vs operator new in C :*https://www.geeksforgeeks.org/new-vs-operator-new-in-cpp/
- EOF -