初識Visual Leak Detector
靈活自由是C/C++語言的一大特點,而這也爲C/C++程序員出了一個難題。當程序愈來愈複雜時,內存的管理也會變得越加複雜,稍有不慎就會出現內存問題。內存泄漏是最多見的內存問題之一。內存泄漏若是不是很嚴重,在短期內對程序不會有太大的影響,這也使得內存泄漏問題有很強的隱蔽性,不容易被發現。然而無論內存泄漏多麼輕微,當程序長時間運行時,其破壞力是驚人的,從性能降低到內存耗盡,甚至會影響到其餘程序的正常運行。另外內存問題的一個共同特色是,內存問題自己並不會有很明顯的現象,當有異常現象出現時已時過境遷,其現場已非出現問題時的現場了,這給調試內存問題帶來了很大的難度。
Visual Leak Detector是一款用於Visual C++的免費的內存泄露檢測工具。能夠在
[url]http://www.codeproject.com/tools/visualleakdetector.asp[/url] 下載到。相比較其它的內存泄露檢測工具,它在檢測到內存泄漏的同時,還具備以下特色: 一、 能夠獲得內存泄漏點的調用堆棧,若是能夠的話,還能夠獲得其所在文件及行號; 二、 能夠獲得泄露內存的完整數據; 三、 能夠設置內存泄露報告的級別; 四、 它是一個已經打包的lib,使用時無須編譯它的源代碼。而對於使用者本身的代碼,也只須要作很小的改動; 五、 他的源代碼使用GNU許可發佈,並有詳盡的文檔及註釋。對於想深刻了解堆內存管理的讀者,是一個不錯的選擇。 可見,從使用角度來說,Visual Leak Detector簡單易用,對於使用者本身的代碼,惟一的修改是#include Visual Leak Detector的頭文件後正常運行本身的程序,就能夠發現內存問題。從研究的角度來說,若是深刻Visual Leak Detector源代碼,能夠學習到堆內存分配與釋放的原理、內存泄漏檢測的原理及內存操做的經常使用技巧等。 本文首先將介紹Visual Leak Detector的使用方法與步驟,而後再和讀者一塊兒初步的研究Visual Leak Detector的源代碼,去了解Visual Leak Detector的工做原理。 使用Visual Leak Detector(1.0) 下面讓咱們來介紹如何使用這個小巧的工具。 首先從網站上下載zip包,解壓以後獲得vld.h, vldapi.h, vld.lib, vldmt.lib, vldmtdll.lib, dbghelp.dll等文件。將.h文件拷貝到Visual C++的默認include目錄下,將.lib文件拷貝到Visual C++的默認lib目錄下,便安裝完成了。由於版本問題,若是使用windows 2000或者之前的版本,須要將dbghelp.dll拷貝到你的程序的運行目錄下,或其餘能夠引用到的目錄。 接下來須要將其加入到本身的代碼中。方法很簡單,只要在包含入口函數的.cpp文件中包含vld.h就能夠。若是這個cpp文件包含了stdafx.h,則將包含vld.h的語句放在stdafx.h的包含語句以後,不然放在最前面。以下是一個示例程序: #include <vld.h> void main() { … } 接下來讓咱們來演示如何使用Visual Leak Detector檢測內存泄漏。下面是一個簡單的程序,用new分配了一個int大小的堆內存,並無釋放。其申請的內存地址用printf輸出到屏幕上。 #include <vld.h> #include <stdlib.h> #include <stdio.h> void f() { int *p = new int(0x12345678); printf("p=%08x, ", p); } void main() { f(); } 編譯運行後,在標準輸出窗口獲得: p=003a89c0 在Visual C++的Output窗口獲得: WARNING: Visual Leak Detector detected memory leaks! ---------- Block 57 at 0x003A89C0: 4 bytes ---------- --57號塊0x003A89C0地址泄漏了4個字節 Call Stack: --下面是調用堆棧 d:\test\testvldconsole\testvldconsole\main.cpp (7): f --表示在main.cpp第7行的f()函數 d:\test\testvldconsole\testvldconsole\main.cpp (14): main –雙擊以引導至對應代碼處 f:\rtm\vctools\crt_bld\self_x86\crt\src\crtexe.c (586): __tmainCRTStartup f:\rtm\vctools\crt_bld\self_x86\crt\src\crtexe.c (403): mainCRTStartup 0x7C816D4F (File and line number not available): RegisterWaitForInputIdle Data: --這是泄漏內存的內容,0x12345678 78 56 34 12 xV4..... ........ Visual Leak Detector detected 1 memory leak. 第二行表示57號塊有4字節的內存泄漏,地址爲0x003A89C0,根據程序控制臺的輸出,能夠知道,該地址爲指針p。程序的第7行,f()函數裏,在該地址處分配了4字節的堆內存空間,並賦值爲0x12345678,這樣在報告中,咱們看到了這4字節一樣的內容。 能夠看出,對於每個內存泄漏,這個報告列出了它的泄漏點、長度、分配該內存時的調用堆棧、和泄露內存的內容(分別以16進制和文本格式列出)。雙擊該堆棧報告的某一行,會自動在代碼編輯器中跳到其所指文件的對應行。這些信息對於咱們查找內存泄露將有很大的幫助。 這是一個很方便易用的工具,安裝後每次使用時,僅僅須要將它頭文件包含進來從新build就能夠。並且,該工具僅在build Debug版的時候會鏈接到你的程序中,若是build Release版,該工具不會對你的程序產生任何性能等方面影響。因此盡能夠將其頭文件一直包含在你的源代碼中。 Visual Leak Detector工做原理 下面讓咱們來看一下該工具的工做原理。 在這以前,咱們先來看一下Visual C++內置的內存泄漏檢測工具是如何工做的。Visual C++內置的工具CRT Debug Heap工做原來很簡單。在使用Debug版的malloc分配內存時,malloc會在內存塊的頭中記錄分配該內存的文件名及行號。當程序退出時CRT會在main()函數返回以後作一些清理工做,這個時候來檢查調試堆內存,若是仍然有內存沒有被釋放,則必定是存在內存泄漏。從這些沒有被釋放的內存塊的頭中,就能夠得到文件名及行號。 這種靜態的方法能夠檢測出內存泄漏及其泄漏點的文件名和行號,可是並不知道泄漏到底是如何發生的,並不知道該內存分配語句是如何被執行到的。要想了解這些,就必需要對程序的內存分配過程進行動態跟蹤。Visual Leak Detector就是這樣作的。它在每次內存分配時將其上下文記錄下來,當程序退出時,對於檢測到的內存泄漏,查找其記錄下來的上下文信息,並將其轉換成報告輸出。 初始化 Visual Leak Detector要記錄每一次的內存分配,而它是如何監視內存分配的呢?Windows提供了分配鉤子(allocation hooks)來監視調試堆內存的分配。它是一個用戶定義的回調函數,在每次從調試堆分配內存以前被調用。在初始化時,Visual Leak Detector使用_CrtSetAllocHook註冊這個鉤子函數,這樣就能夠監視今後以後全部的堆內存分配了。 如何保證在Visual Leak Detector初始化以前沒有堆內存分配呢?全局變量是在程序啓動時就初始化的,若是將Visual Leak Detector做爲一個全局變量,就能夠隨程序一塊兒啓動。可是C/C++並無約定全局變量之間的初始化順序,若是其它全局變量的構造函數中有堆內存分配,則可能沒法檢測到。Visual Leak Detector使用了C/C++提供的#pragma init_seg來在某種程度上減小其它全局變量在其以前初始化的機率。根據#pragma init_seg的定義,全局變量的初始化分三個階段:首先是compiler段,通常c語言的運行時庫在這個時候初始化;而後是lib段,通常用於第三方的類庫的初始化等;最後是user段,大部分的初始化都在這個階段進行。Visual Leak Detector將其初始化設置在compiler段,從而使得它在絕大多數全局變量和幾乎全部的用戶定義的全局變量以前初始化。 記錄內存分配 一個分配鉤子函數須要具備以下的形式: int YourAllocHook( int allocType, void *userData, size_t size, int blockType, long requestNumber, const unsigned char *filename, int lineNumber); 就像前面說的,它在Visual Leak Detector初始化時被註冊,每次從調試堆分配內存以前被調用。這個函數須要處理的事情是記錄下此時的調用堆棧和這次堆內存分配的惟一標識——requestNumber。 獲得當前的堆棧的二進制表示並非一件很複雜的事情,可是由於不一樣體系結構、不一樣編譯器、不一樣的函數調用約定所產生的堆棧內容略有不一樣,要解釋堆棧並獲得整個函數調用過程略顯複雜。不過windows提供一個StackWalk64函數,能夠得到堆棧的內容。StackWalk64的聲明以下: BOOL StackWalk64( DWORD MachineType, HANDLE hProcess, HANDLE hThread, LPSTACKFRAME64 StackFrame, PVOID ContextRecord, PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine, PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAcce***outine, PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine, PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress ); STACKFRAME64結構表示了堆棧中的一個frame。給出初始的STACKFRAME64,反覆調用該函數,即可以獲得內存分配點的調用堆棧了。 // Walk the stack. while (count < _VLD_maxtraceframes) { count++; if (!pStackWalk64(architecture, m_process, m_thread, &frame, &context, NULL, pSymFunctionTableAccess64, pSymGetModuleBase64, NULL)) { // Couldn't trace back through any more frames. break; } if (frame.AddrFrame.Offset == 0) { // End of stack. break; } // Push this frame's program counter onto the provided CallStack. callstack->push_back((DWORD_PTR)frame.AddrPC.Offset); } 那麼,如何獲得初始的STACKFRAME64結構呢?在STACKFRAME64結構中,其餘的信息都比較容易得到,而當前的程序計數器(EIP)在x86體系結構中沒法經過軟件的方法直接讀取。Visual Leak Detector使用了一種方法來得到當前的程序計數器。首先,它調用一個函數,則這個函數的返回地址就是當前的程序計數器,而函數的返回地址能夠很容易的從堆棧中拿到。下面是Visual Leak Detector得到當前程序計數器的程序: #if defined(_M_IX86) || defined(_M_X64) #pragma auto_inline(off) DWORD_PTR VisualLeakDetector::getprogramcounterx86x64 () { DWORD_PTR programcounter; __asm mov AXREG, [BPREG + SIZEOFPTR] // Get the return address out of the current stack frame __asm mov [programcounter], AXREG // Put the return address into the variable we'll return return programcounter; } #pragma auto_inline(on) #endif // defined(_M_IX86) || defined(_M_X64) 獲得了調用堆棧,天然要記錄下來。Visual Leak Detector使用一個相似map的數據結構來記錄該信息。這樣能夠方便的從requestNumber查找到其調用堆棧。分配鉤子函數的allocType參數表示這次堆內存分配的類型,包括_HOOK_ALLOC, _HOOK_REALLOC, 和 _HOOK_FREE,下面代碼是Visual Leak Detector對各類狀況的處理。 switch (type) { case _HOOK_ALLOC: visualleakdetector.hookmalloc(request); break; case _HOOK_FREE: visualleakdetector.hookfree(pdata); break; case _HOOK_REALLOC: visualleakdetector.hookrealloc(pdata, request); break; default: visualleakdetector.report("WARNING: Visual Leak Detector: in allochook(): Unhandled allocation type (%d).\n", type); break; } 這裏,hookmalloc()函數獲得當前堆棧,並將當前堆棧與requestNumber加入到相似map的數據結構中。hookfree()函數從相似map的數據結構中刪除該信息。hookrealloc()函數依次調用了hookfree()和hookmalloc()。 檢測內存泄露 前面提到了Visual C++內置的內存泄漏檢測工具的工做原理。與該原理相同,由於全局變量以構造的相反順序析構,在Visual Leak Detector析構時,幾乎全部的其餘變量都已經析構,此時若是仍然有未釋放之堆內存,則必爲內存泄漏。 分配的堆內存是經過一個鏈表來組織的,檢查內存泄漏則是檢查此鏈表。可是windows沒有提供方法來訪問這個鏈表。Visual Leak Detector使用了一個小技巧來獲得它。首先在堆上申請一塊臨時內存,則該內存的地址能夠轉換成指向一個_CrtMemBlockHeader結構,在此結構中就能夠得到這個鏈表。代碼以下: char *pheap = new char; _CrtMemBlockHeader *pheader = pHdr(pheap)->pBlockHeaderNext; delete pheap; 其中pheader則爲鏈表首指針。 報告生成 前面講了Visual Leak Detector如何檢測、記錄內存泄漏及其其調用堆棧。可是若是要這個信息對程序員有用的話,必須轉換成可讀的形式。Visual Leak Detector使用SymGetLineFromAddr64()及SymFromAddr()生成可讀的報告。 // Iterate through each frame in the call stack. for (frame = 0; frame < callstack->size(); frame++) { // Try to get the source file and line number associated with // this program counter address. if (pSymGetLineFromAddr64(m_process, (*callstack)[frame], &displacement, &sourceinfo)) { ... } // Try to get the name of the function containing this program // counter address. if (pSymFromAddr(m_process, (*callstack)[frame], &displacement64, pfunctioninfo)) { functionname = pfunctioninfo->Name; } else { functionname = "(Function name unavailable)"; } ... } 歸納講來,Visual Leak Detector的工做分爲3步,首先在初始化註冊一個鉤子函數;而後在內存分配時該鉤子函數被調用以記錄下當時的現場;最後檢查堆內存分配鏈表以肯定是否存在內存泄漏並將泄漏內存的現場轉換成可讀的形式輸出。有興趣的讀者能夠閱讀Visual Leak Detector的源代碼。 總結 在使用上,Visual Leak Detector簡單方便,結果報告一目瞭然。在原理上,Visual Leak Detector針對內存泄漏問題的特色,可謂對症下藥——內存泄漏不是不容易發現嗎?那就每次內存分配是都給記錄下來,程序退出時算總帳;內存泄漏現象出現時不是已時過境遷,並不是當時泄漏點的現場了嗎?那就把現場也記錄下來,清清楚楚的告訴使用者那塊泄漏的內存就是在如何一個調用過程當中泄漏掉的。 Visual Leak Detector是一個簡單易用內存泄漏檢測工具。如今最新的版本是1.9a,採用了新的檢測機制,並在功能上有了不少改進。讀者不妨體驗一下。