vs 2010 下使用VLD工具
名詞解釋:
1、stack trace:調用堆棧信息
2、debug heap:調試堆
3、Allocation Hook:向調試堆注冊的回調函數,當申請內存時,調試堆即調用此回調函數
?
前言
VC++提供內建的內存泄漏檢測,但是其功能簡陋。本文介紹的工具Visual Leak Detector(以下稱VLD)它提被用于替代vc++內建的檢測工具,供一些特性:
1、對每個泄漏內存塊提供stack trace,包括源碼文件名及行數信息。
2、提供泄漏內存塊的完全數據診斷(dump),包括16進制與2進制表示。
3、對于泄漏報告的細節可定制
?
vc++下可以使用的還有一些商業化的內存檢測工具,例如Purify或BoundsChecker都受到人們的歡迎,但價格不菲。有相當多的免費替代品,但通常是不可靠的、有局限性的。VLD相比于其他的免費替代品有如下的優勢:
1、VLD被打包成易于使用的類庫。你不需要編譯它的源碼,只需要在你的項目中整合少許代碼。
2、額外的提供stack trace,包括源碼文件名,行數,和函數名,并且能提供數據診斷(data dumps)
3、支持c/c++程序(兼容new/delete 和 malloc/free)
4、提供完整的、文檔化的源碼,所以,你可以輕松的定制它
?
使用VLD
本節簡要介紹VLD的使用基礎知識。對于更深入的討論,例如:配置選項,API,更多的高級使用場景(例如在DLL中使用),請參見位于壓縮包內的完整文檔。
?
欲在你的項目中使用VLD,順序執行如下幾個簡單步驟即可:
1、拷貝VLD lib文件至Visual C++安裝目錄下的lib子文件夾內
2、拷貝VLD頭文件(vld.h and vldapi.h)至Visual C++安裝目錄下的"include"子文件夾
3、在程序入口點所在的源文件內包含vld.h。最好將此頭文件包含在其他頭文件之前,stdafx.h之后,但這并不是必須的。如果這個源文件包含了stdafx.h,那么vld.h應該在其后包含。
4、如果運行環境是windows2000或更新,則需要拷貝dbghelp.dll至被調試的可執行文件目錄下。
5、編譯debug版本的project
?
在vc++中使用調試器運行debug版本的程序時,VLD將會啟動執行。在程序結束后,內存泄漏檢測報告將會顯示在vc++調試信息輸出窗口。雙擊報告中的源碼行數信息,vc++將會跳轉至對應的源碼處。
?
注意:在release版本下,VLD并不鏈接到可執行文件。所以對于release版本可安全的與VLD分離。這種方式保證了不會有任何的性能下降和不良開銷。
?
創建VLD
VLD的目標是成為VC++內置檢測器的更好的替代品??紤]到這一點,我們使用VC++內置檢測器所使用的方法,即CRT調試堆(CRT Debug Heap)。但是VLD更強大的是擁有完全的stack trace功能,它可以盡可能的幫助你找到和修正泄漏。
?
vc++內置檢測器
內置檢測器非常簡單。當程序退出,在main返回之后,CRT執行一堆清理代碼。如果內置檢測器被啟用,則會在清理過程中執行一些泄漏檢測。泄漏檢測簡單的查看debug heap:如果有用戶分配的內存塊還存在于調試堆上,那么必然是泄漏。
調試版本的malloc調用時,會分配一個內存塊頭結構(block's header),其中存儲著源文件名和行數。內存檢測器就是簡單的從頭結構中取出文件名和行數,來標示一個內存泄漏信息,并將信息報告給調試器顯示出來。
?
注意:內置檢測器對分配和釋放內存沒有任何的監控。它只是簡單的在進程終止前為堆生成“快照”,并且基于“快照”確定是否有泄漏發生。堆的“快照”只告訴你是否泄漏了,而不能告訴你是什么導致了泄漏。當然,要確定“是什么導致了泄漏”,我們需要得到stack trace。然而,要得到stack trace,需要在運行時監控每一次內存分配操作。這就是VLD和內置檢測器的區別。
?
Allocation Hooking
幸運的是,微軟提供了一種簡單的方式,用于監控每一次內存分配(從調試堆中):Allocation Hook。它是一個用戶提供的回調函數,此函數會在內存分配前被調用。微軟提供了_CrtSetAllocHook函數,用于注冊回調函數至調試堆。
調試堆調用回調函數時,會傳遞一個參數,參數實際是一個唯一的串號,用于標示此次分配。串號并不能為我們提供關于block's header的任何信息,但是我們可以以串號作為key,去映射對應的內存塊,以記錄我們想要記錄的信息。
?
調用堆棧遍歷(Walking the Stack)
現在我們已經可以在每次分配內存時獲得通知,以及獲得串號,那么現在要做的就是記錄調用堆棧信息了。我們可以嘗試使用內聯匯編進行棧展開(unwind the stack)。但是棧幀(stack frames)的產生可能源于不同的方式,其依賴于編譯器的優化和調用約定。
幸好,微軟提供了函數StackWalk64,這個函數被稱之為調用堆棧遍歷。它在dbghelp.dll中導出。調用StackWalk64后,其會填充用戶傳入的STACKFRAME64結構。它可以被循環的調用,直到到達堆棧的底部。
?
初始化
現在VLD有了良好的開端。我們可以監視每一次內存分配,并且擁有stack trace。
現在只需要確保在程序啟動時就為debug heap注冊好回調函數。當然,這可以簡單的通過創建一個全局的C++對象實例(稱VLD對象)來實現,VLD對象會在程序初始化時構造。在構造時,調用_CrtSetAllocHook注冊回調函數。
等等,如果程序中有其他的全局對象在構造時申請了內存,我們將如何能確保VLD對象的構造被最先調用呢?(譯者注:只有VLD對象最先被調用,才能監控到其他對象的內存申請操作,包括全局對象)遺憾的是,c++規范中并沒有詳述任何關于全局對象構造順序的事宜。所以,不能保證VLD對象會被最先構造。
但我們必須盡量滿足這一點,我們利用一個特別的編譯器預處理指令,告訴編譯器,讓VLD對象盡快的構造,這個指令是:#pragma init_seg (compiler)。這條指令告訴編譯器,將VLD對象置入compiler段(compiler segment)。在這個段內的對象將被最先構造,接著是libray段(library segment)的對象被構造,最后是User段(user segment)的對象被構造。用戶定義的全局對象默認就是置于User段。一般來說,普通的用戶定義的對象是不會放入compiler段的。所以,這基本可以使我們的VLD對象在其他全局對象前構造。
?
檢測內存泄漏
介于 全局對象的銷毀順序與構造順序相反,我們的VLD對象也會在其他全局對象之后銷毀?,F在我們就可以像內建檢測器那樣檢查內存泄漏了。
如果我們發現了某個內存塊沒有被釋放,那便是一個泄漏,我們能夠利用掛鉤函數返回給我們的串號,來檢查stack trace。STL中的map恰好合用,它可以映射串號和其stack trace。但是VLD并沒有使用STL map,這是希望對舊版本的vc++保持兼容性,因為舊版本的STL并不兼容于新版本,所以不能使用它。這恰好是一個模擬STL map的好機會,并且可以在其中做特定的優化。
還記得前面提及的,內建檢測器會在內存塊頭部取得源文件名和行數信息嗎?好的,我們現在所擁有的stack trace,只是一組地址而已。將這些信息輸出到調試器并不完全夠用。為了讓這些地址更直觀,需要將它們轉換為可讀的信息:文件名與行數(也需要函數名)。再一次,微軟帶來了合適的工具幫助我們解決難題,如同StackWalk64,它們也是Debug Help Library的一部分。它們是:
1、SymGetLineFromAddr64:將給定的地址轉換為源文件名和行數
2、SymFromAddr:將給定的地址轉換為函數名(symbol name)
?
源碼中的關鍵點
考慮到你可能厭倦并且跳過了前述,我將在這里進行總結。
一言以蔽之,VLD的工作過程如下:
1、首先,一個全局對象被自動創建。這個對象被最早創建。在對象的構造函數中,向調試堆注冊了我們的回調函數。
2、之后,每次申請內存時都會引發回調函數被調用,回調函數中獲得并記錄了stack trace。這些信息被記錄于類似于STL map這樣的結構中。
3、最后,程序終止,這個全局對象最后被銷毀。它檢查調試堆并識別泄漏。泄漏的內存塊在map中被查找到,其stack trace經過處理后發送至調試器并顯示出來。
?
步驟1:注冊Allocation Hook
這是VisualLeakDetector類的構造函數。
注意_CrtSetAllocHook的調用,allochook是我們的Allocation Hook。
linkdebughelplibrary完成了dbghelp.dll的動態鏈接。由于VLD自身就是一個library,隱式鏈接dbghelp.lib將使VLD庫鏈接時依賴dbghelp.lib,而dbghelp.lib并非在所有的機器上都存在,同時,也是不可再發行的(not redistributable)。因此,隱式鏈接是不可行的。我們需要采取運行時動態鏈接來繞過lib。
? // Constructor - Dynamically links with the Debug Help Library and installs the // allocation hook function so that the C runtime's debug heap manager will // call the hook function for every heap request. VisualLeakDetector::VisualLeakDetector () { // Initialize private data. m_mallocmap = new BlockMap; m_process = GetCurrentProcess(); m_selftestfile = __FILE__; m_status = 0x0; m_thread = GetCurrentThread(); m_tlsindex = TlsAlloc(); if (_VLD_configflags & VLD_CONFIG_SELF_TEST) { // Self-test mode has been enabled. // Intentionally leak a small amount of // memory so that memory leak self-checking can be verified. strncpy(new char [21], "Memory Leak Self-Test", 21); m_selftestline = __LINE__; } if (m_tlsindex == TLS_OUT_OF_INDEXES) { report("ERROR: Visual Leak Detector:" " Couldn't allocate thread local storage.\n"); } else if (linkdebughelplibrary()) { // Register our allocation hook function with the debug heap. m_poldhook = _CrtSetAllocHook(allochook); report("Visual Leak Detector " "Version "VLD_VERSION" installed ("VLD_LIBTYPE").\n"); reportconfig(); if (_VLD_configflags & VLD_CONFIG_START_DISABLED) { // Memory leak detection will initially be disabled. m_status |= VLD_STATUS_NEVER_ENABLED; } m_status |= VLD_STATUS_INSTALLED; return; } report("Visual Leak Detector is NOT installed!\n"); } ?步驟2:調用堆棧遍歷
這個函數承擔了獲取stack trace的責任,這也許是整個程序中最棘手的部分。第一次調用StackWalk64前的準備工作尤為棘手。開始之前,StackWalk64需要確切的知道從棧上的何處開始遍歷,因為它并不默認從當前的棧幀(stack frame)開始遍歷。這就需要我們提供當前棧幀地址以及當前程序地址(MSDN解釋:此地址正是EIP中存儲的地址)??梢酝ㄟ^GetThreadContext函數獲取線程上下文,其中便包含這兩個地址。但是正如MSDN的解釋,GetThreadContext不能在線程運行時獲取到有效的信息(據MSDN:調用前必須調用SuspendThread掛起線程)。那就是說,GetThreadContext在這里并不適用。更好的辦法是直接取得所需的地址,欲達到這種效果,唯一的途徑是使用內聯匯編。?
獲取當前棧幀地址很簡單:直接從CPU的EBP寄存器中讀取。
而獲取程序地址則有一些困難。盡管EIP寄存器中存儲了當前程序地址,但是在X86下,它不能被軟件讀取。那么,我們采取一種間接的方式來實現:調用另一個函數,并從此函數中獲取返回地址,原理是被調用者返回地址就是調用者地址。因此,我們創建了一個特別的函數getprogramcounterx86x64。既然我們已經使用了內聯匯編,那么完全可以使用匯編寫一個函數調用,但是考慮到可讀性,還是使用C++。
在以下的代碼中,pStackWalk64、pSymFunctionTableAccess64和pSymGetModuleBase64都是函數指針,指向dbghelp.dll中的對應的API。
// getstacktrace - Traces the stack, starting from this function, as far // back as possible. // - callstack (OUT): Pointer to an empty CallStack to be populated with // entries from the stack trace. // Return Value: // None. void VisualLeakDetector::getstacktrace (CallStack *callstack) { DWORD architecture; CONTEXT context; unsigned int count = 0; STACKFRAME64 frame; DWORD_PTR framepointer; DWORD_PTR programcounter; // Get the required values for initialization of the STACKFRAME64 structure // to be passed to StackWalk64(). Required fields are AddrPC and AddrFrame. #if defined(_M_IX86) || defined(_M_X64) architecture = X86X64ARCHITECTURE; programcounter = getprogramcounterx86x64(); __asm mov [framepointer], BPREG // Get the frame pointer (aka base pointer) #else // If you want to retarget Visual Leak Detector to another processor // architecture then you'll need to provide architecture-specific code to // retrieve the current frame pointer and program counter in order to initialize // the STACKFRAME64 structure below. #error "Visual Leak Detector is not supported on this architecture." #endif // defined(_M_IX86) || defined(_M_X64) // Initialize the STACKFRAME64 structure. memset(&frame, 0x0, sizeof(frame)); frame.AddrPC.Offset = programcounter; frame.AddrPC.Mode = AddrModeFlat; frame.AddrFrame.Offset = framepointer; frame.AddrFrame.Mode = AddrModeFlat; // 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); } } ? ? // getprogramcounterx86x64 - Helper function that retrieves the program counter // for getstacktrace() on Intel x86 or x64 architectures. // // Note: Inlining of this function must be disabled. The whole purpose of this // function's existence depends upon it being a *called* function. // Return Value: // Returns the caller's program address. #if defined(_M_IX86) || defined(_M_X64) #pragma auto_inline(off) DWORD_PTR VisualLeakDetector::getprogramcounterx86x64 () { DWORD_PTR programcounter; // Get the return address out of the current stack frame __asm mov AXREG, // Put the return address into the variable we'll return __asm mov [programcounter], AXREG return programcounter; } #pragma auto_inline(on) #endif // defined(_M_IX86) || defined(_M_X64) ?步驟3:產生更好的內存泄漏報告
最后,下面的這個函數將會轉換堆棧遍歷時獲取的程序地址至函數名。注意“地址-函數名”的轉換只發生在內存泄漏被檢測到的時候。避免了在程序運行時查找符號表,這將會帶來巨大的額外的開銷,更不必存儲符號名,因為已經存儲了地址,再存儲符號名是沒有意義的。
關于已分配的內存塊鏈表的訪問權獲取,CRT并沒有公布相關文檔。這個鏈表正是被內建檢測器用以確定是否存在內存泄漏。
我已經想出了關于獲取鏈表訪問權的方法。原理是:無論何時申請新的內存塊,那么這個內存塊都將被放置鏈表的頭部。那么,如果要獲得鏈表的頭部,只需要臨時申請一個內存塊,這個臨時內存塊的地址可以被轉換成包含_CrtMemBlockHeader結構的地址,并且擁有了鏈表頭指針。
在以下的代碼中,pSymSetOptions、pSymInitialize、pSymGetLineFromAddr64和pSymFromAddr都是函數指針,指向dbghelp.dll中導出的API。而report函數就類似于OutputDebugString這樣的輸出調試信息函數。
這個函數相當長,為了更好的可讀性,我省略了所有的瑣碎部分,以突出重點。關于函數的完全實現,請參見源碼。
// reportleaks - Generates a memory leak report when the program terminates if // leaks were detected. The report is displayed in the debug output window. // Return Value: // None. void VisualLeakDetector::reportleaks () { ... // Initialize the symbol handler. We use it for obtaining source file/line // number information and function names for the memory leak report. symbolpath = buildsymbolsearchpath(); pSymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME); if (!pSymInitialize(m_process, symbolpath, TRUE)) { report("WARNING: Visual Leak Detector: The symbol handler" " failed to initialize (error=%lu).\n" " Stack traces will probably not be available" " for leaked blocks.\n", GetLastError()); } ... #ifdef _MT _mlock(_HEAP_LOCK); #endif // _MT pheap = new char; pheader = pHdr(pheap)->pBlockHeaderNext; delete pheap; while (pheader) { ... callstack = m_mallocmap->find(pheader->lRequest); if (callstack) { ... // 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)"; } ... } ... } pheader = pheader->pBlockHeaderNext; } #ifdef _MT _munlock(_HEAP_LOCK); #endif // _MT ... } ?已知的BUG和限制
以下是最新版本的已知BUG和限制:
1、VLD不能檢測COM的泄漏,out-of-process資源泄漏,或者其他一些與CRT堆無關的泄漏。簡單的說,VLD只能檢測new或malloc所產生的泄漏。請記住VLD的目的就是取代內建檢測器,而內建檢測器只檢測new或malloc引起的泄漏。
2、VLD不兼容6.5版本的dbghelp.dll。建議是使用6.3版本。6.3版本已經包含在源碼包內。
3、源碼包內自帶的預編譯好的lib可能與vs2005不兼容。如果你的環境是vs2005,建議使用VLD源碼在VS2005下重新編譯。
轉載于:https://www.cnblogs.com/zd_ad/archive/2013/02/24/2923971.html
總結
以上是生活随笔為你收集整理的vs 2010 下使用VLD工具的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 搭建你的Spring.Net+Nhibe
- 下一篇: Core Java 第三章 Java基本