閱讀457 返回首頁    go 阿裏雲 go 技術社區[雲棲]


CRT 調試堆

本節內容

內存管理和調試堆
描述堆函數的“Debug”版本。這些函數解決兩個最難處理的內存分配問題:改寫已分配緩衝區的結尾和內存泄漏(當不再需要分配後未能釋放它們)。
調試堆中的塊類型
描述在調試堆中內存塊所分配到的五種分配類型。出於泄漏檢測和狀態報告的目的,以不同方式對這些分配類型進行跟蹤和報告。
調試堆
提供有關使用調試堆的信息。信息包括:哪些調用用於“Debug”版本,釋放內存塊時將發生什麼,哪些調試功能必須從代碼內部進行訪問,更改 _crtDbgFlag 位域以創建標誌的新狀態的步驟,以及一個闡釋如何打開自動泄漏檢測和如何關閉 _CRT_BLOCK 類型塊的檢查的代碼示例。
C++ 中的調試堆
討論 C++ newdelete 運算符的“Debug”版本和使用 _CRTDBG_MAP_ALLOC 的效果。
堆狀態報告函數
描述 _CrtMemState 結構,可以使用它來捕捉堆狀態的摘要快照。本主題還列出一些 CRT 函數,這些函數報告堆的狀態和內容並使用這些信息來幫助檢測內存泄漏和其他問題。
跟蹤堆分配請求
包含用於標識出錯的特定堆分配調用的方法。

相關章節

CRT 調試技術
鏈接到用於 C 運行時庫的調試技術,包括:使用 CRT 調試庫、用於報告的宏、malloc_malloc_dbg 之間的差異、編寫調試掛鉤函數以及 CRT 調試堆(參考msnd)。

內存管理和調試堆

程序員遇到的兩種最常見而又難處理的問題是,改寫已分配緩衝區的末尾以及內存泄漏(未能在不再需要某些分配後將其釋放)。調試堆提供功能強大的工具來解決這類內存分配問題。

堆函數的“Debug”版本

堆函數的“Debug”版本調用“Release”版本中使用的標準版本或基版本。當請求內存塊時,調試堆管理器從基堆分配略大於所請求的塊的內存塊,並返回指向該塊中屬於您的部分的指針。例如,假定應用程序包含調用:malloc( 10 )。在“Release”版本中, 將調用基堆分配例程以請求分配 10 個字節。但在“Debug”版本中,malloc 將調用 ,該函數接著調用基堆分配例程以請求分配 10 個字節加上大約 36 個字節的額外內存。調試堆中產生的所有內存塊在單個鏈接列表中連接起來,按照分配時間排序。

調試堆例程分配的附加內存的用途為:存儲簿記信息,存儲將調試內存塊鏈接在一起的指針,以及形成數據兩側的小緩衝區(用於捕捉已分配區域的改寫)。

當前,用於存儲調試堆的簿記信息的塊頭結構在 DBGINT.H 頭文件中聲明如下:

typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before this one:
struct _CrtMemBlockHeader *pBlockHeaderNext;
// Pointer to the block allocated just after this one:
struct _CrtMemBlockHeader *pBlockHeaderPrev;
char *szFileName; // File name
int nLine; // Line number
size_t nDataSize; // Size of user block
int nBlockUse; // Type of block
long lRequest; // Allocation number
// Buffer just before (lower than) the user's memory:
unsigned char gap[nNoMansLandSize];
} _CrtMemBlockHeader;

/* In an actual memory block in the debug heap,
* this structure is followed by:
* unsigned char data[nDataSize];
* unsigned char anotherGap[nNoMansLandSize];
*/

該塊的用戶數據區域兩側的 NoMansLand 緩衝區當前大小為 4 個字節,並用調試堆例程所使用的已知字節值填充,以驗證尚未改寫用戶內存塊限製。調試堆還用已知值填充新的內存塊。如果選擇在堆的鏈接列表中保留已釋放塊(如下文所述),則這些已釋放塊也用已知值填充。當前,所用的實際字節值如下所示:

NoMansLand (0xFD)(deFencde Data)
應用程序所用內存兩側的“NoMansLand”緩衝區當前用 0xFD 填充。
已釋放塊 (0xDD)(Dead Data)
設置 _CRTDBG_DELAY_FREE_MEM_DF 標誌後,調試堆的鏈接列表中保留未使用的已釋放塊當前用 0xDD 填充。
新對象 (0xCD) (Cleared Data)
分配新對象時,這些對象用 0xCD 填充。
 

調試堆中的塊類型

調試堆中的每個內存塊都分配以五種分配類型之一。出於泄漏檢測和狀態報告目的對這些類型進行不同地跟蹤和報告。可以指定塊的類型,方法是使用對其中一個調試堆分配函數(如 )的直接調用來分配塊。調試堆中的五種內存塊類型(在 _CrtMemBlockHeader 結構的 nBlockUse 成員中設置)如下所示:

_NORMAL_BLOCK
對 或 的調用將創建“普通”塊。如果打算隻使用“普通”塊而不需要“客戶端”塊,則可能想要定義 ,它導致所有堆分配調用映射到它們在“Debug”版本中的調試等效項。這將允許將關於每個分配調用的文件名和行號信息存儲到對應的塊頭中。
_CRT_BLOCK
由許多運行時庫函數內部分配的內存塊被標記為 CRT 塊,以便可以單獨處理這些塊。結果,泄漏檢測和其他操作不需要受這些塊影響。分配永不可以分配、重新分配或釋放任何 CRT 類型的塊。
_CLIENT_BLOCK
出於調試目的,應用程序可以專門跟蹤一組給定的分配,方法是使用對調試堆函數的顯式調用將它們作為該類型的內存塊進行分配。例如,MFC 以“客戶端”塊類型分配所有的 CObjects;其他應用程序則可能在“客戶端”塊中保留不同的內存對象。還可以指定“客戶端”塊的子類型以獲得更大的跟蹤粒度。若要指定“客戶端”塊子類型,請將該數字向左移 16 位,並將它與 _CLIENT_BLOCK 進行 OR 運算。例如:
#define MYSUBTYPE 4
freedbg(pbData, _CLIENT_BLOCK|(MYSUBTYPE<<16));

客戶端提供的掛鉤函數(用於轉儲在“客戶端”塊中存儲的對象)可以使用 進行安裝,然後,每當調試函數轉儲“客戶端”塊時均會調用該掛鉤函數。同樣,對於調試堆中的每個“客戶端”塊,可以使用 來調用應用程序提供的給定函數。

_FREE_BLOCK
通常,所釋放的塊將從列表中移除。為了檢查並未仍在向已釋放的內存寫入數據,或為了模擬內存不足情況,可以選擇在鏈接列表上保留已釋放塊,將其標記為“可用”,並用已知字節值(當前為 0xDD)填充。
_IGNORE_BLOCK
有可能在一段時間內關閉調試堆操作。在該時間段內,內存塊保留在列表上,但被標記為“忽略”塊。

若要確定給定塊的類型和子類型,請使用 函數以及 _BLOCK_TYPE_BLOCK_SUBTYPE 宏。宏的定義(在 crtdbg.h 中)如下所示:

#define _BLOCK_TYPE(block)          (block & 0xFFFF)
#define _BLOCK_SUBTYPE(block)       (block >> 16 & 0xFFFF)

調試堆

對堆函數(如 mallocfreecallocreallocnew delete)的所有調用均解析為這些函數在調試堆中運行的“Debug”版本。當釋放內存塊時,調試堆自動檢查已分配區域兩側的緩衝區的完整性,如果發生改寫,將發出錯誤報告。

使用調試堆

  • 用 C 運行時庫的“Debug”版本鏈接應用程序的調試版本。

從代碼內部訪問的調試堆功能

_CrtCheckMemory
許多調試堆功能必須從代碼內訪問。例如,可以使用對 的調用來檢查堆在任意點的完整性。該函數檢查堆中的每個內存塊,驗證內存塊頭信息有效,並確認尚未修改緩衝區。
_CrtSetDbgFlag
可以使用內部標誌 來控製調試堆跟蹤分配的方式,該標誌可使用 函數進行讀取和設置。通過更改該標誌,可以指示調試堆在程序退出時檢查內存泄漏,並報告檢測到的所有泄漏。類似地,可以指定不將已釋放的內存塊從鏈接列表移除,以模擬內存不足情況。當檢查堆時,將完全檢查這些已釋放的塊,以確保它們未受打擾。

_crtDbgFlag 標誌包含下列位域:

位域 默認值 說明
_CRTDBG_ALLOC_MEM_DF On 打開調試分配。當該位為 off 時,分配仍鏈接在一起,但它們的塊類型為 _IGNORE_BLOCK
_CRTDBG_DELAY_FREE_MEM_DF Off 防止實際釋放內存,與模擬內存不足情況相同。當該位為 on 時,已釋放塊保留在調試堆的鏈接列表中,但標記為 _FREE_BLOCK,並用特殊字節值填充。
_CRTDBG_CHECK_ALWAYS_DF Off 導致每次分配和釋放時均調用 _CrtCheckMemory。這將減慢執行,但可快速捕捉錯誤。
_CRTDBG_CHECK_CRT_DF Off 導致將標記為 _CRT_BLOCK 類型的塊包括在泄漏檢測和狀態差異操作中。當該位為 off 時,在這些操作期間將忽略由運行時庫內部使用的內存。
_CRTDBG_LEAK_CHECK_DF Off 導致在程序退出時通過調用 來執行泄漏檢查。如果應用程序未能釋放其所分配的所有內存,將生成錯誤報告。

更改一個或多個 _crtDbgFlag 位域並創建標誌的新狀態

  1. newFlag 參數設置為 _CRTDBG_REPORT_FLAG 的情況下調用 _CrtSetDbgFlag(以獲得當前的 _crtDbgFlag 狀態),並在一個臨時變量中存儲返回值。
  2. 打開任何位,對臨時變量與相應位屏蔽(在應用程序代碼中由清單常數表示)進行 OR 運算(按位 | 符號)。
  3. 關閉其他位,對該變量與相應位屏蔽的 NOT(按位 ~ 符號)進行 AND 運算(按位 & 符號)。
  4. newFlag 參數設置為臨時變量中存儲的值的情況下調用 _CrtSetDbgFlag,以創建 _crtDbgFlag 的新狀態。

例如,下列代碼行打開自動泄漏檢測,關閉檢查 _CRT_BLOCK 類型的塊:

// Get current flag
int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );

// Turn on leak-checking bit
tmpFlag |= _CRTDBG_LEAK_CHECK_DF;

// Turn off CRT block checking bit
tmpFlag &= ~_CRTDBG_CHECK_CRT_DF;

// Set flag to the new value
_CrtSetDbgFlag( tmpFlag );

C++ 中的調試堆


C 運行時庫的“Debug”版本包含 C++ 的 new delete 運算符的“Debug”版本。如果 C++ 代碼定義了 ,則 new 的所有實例都映射到“Debug”版本,該版本將記錄源文件和行號信息。

如果希望使用 _CLIENT_BLOCK 分配類型,請不要定義 _CRTDBG_MAP_ALLOC。而必須直接調用 new 運算符的“Debug”版本,或創建替換調試模式中的 new 運算符的宏,如下麵的示例所示:

/* MyDbgNew.h
Defines global operator new to allocate from
client blocks
*/

#ifdef _DEBUG
#define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__)
#else
#define DEBUG_CLIENTBLOCK
#endif // _DEBUG


/* MyApp.cpp
Compile options needed: /Zi /D_DEBUG /MLd
* or use a
* Default Workspace for a Console Application to
* build a Debug version
*/

#include "crtdbg.h"
#include "mydbgnew.h"

#ifdef _DEBUG
#define new DEBUG_CLIENTBLOCK
#endif

int main( ) {
char *p1;
p1 = new char[40];
_CrtMemDumpAllObjectsSince( NULL );
}

delete 運算符的“Debug”版本可用於所有塊類型,並且編譯“Release”版本時程序中不需要任何更改。


堆狀態報告函數


有幾個函數可報告給定時刻調試堆的內容。

_CrtMemState

若要捕獲給定時刻堆狀態的摘要快照,請使用 CRTDBG.H 中定義的 _CrtMemState 結構:

typedef struct _CrtMemState
{
// Pointer to the most recently allocated block:
struct _CrtMemBlockHeader * pBlockHeader;
// A counter for each of the 5 types of block:
size_t lCounts[_MAX_BLOCKS];
// Total bytes allocated in each block type:
size_t lSizes[_MAX_BLOCKS];
// The most bytes allocated at a time up to now:
size_t lHighWaterCount;
// The total bytes allocated at present:
size_t lTotalCount;
} _CrtMemState;

該結構保存指向調試堆的鏈接列表中的第一個(最近分配的)塊的指針。然後,它在兩個數組中記錄列表中每種類型的內存塊(_NORMAL_BLOCK_CLIENT_BLOCK_FREE_BLOCK 等等)的個數,以及每種類型的塊中分配的字節數。最後,它記錄到該點為止堆中總共分配的最大字節數以及當前分配的字節數。

其他 CRT 報告函數

下列函數報告堆的狀態和內容,並使用這些信息幫助檢測內存泄漏及其他問題:

函數 說明
在應用程序提供的 _CrtMemState 結構中保存堆的快照。
比較兩個內存狀態結構,在第三個狀態結構中保存二者之間的差異,如果兩個狀態不同,則返回 TRUE
轉儲給定的 _CrtMemState 結構。該結構可能包含給定時刻調試堆狀態的快照或兩個快照之間的差異。
轉儲自對堆拍了給定快照以來或從執行開始以來所分配的所有對象的信息。如果已經使用 _CrtSetDumpClient 安裝了掛鉤函數,那麼,_CrtMemDumpAllObjectsSince 每次轉儲 _CLIENT_BLOCK 塊時,都會調用應用程序所提供的掛鉤函數。
確定自程序開始執行以來是否發生過內存泄漏,如果發生過,則轉儲所有已分配對象。如果已使用 _CrtSetDumpClient 安裝了掛鉤函數,那麼,_CrtDumpMemoryLeaks 每次轉儲 _CLIENT_BLOCK 塊時,都會調用應用程序所提供的掛鉤函數。


跟蹤堆分配請求


盡管查明在其中執行斷言或報告宏的源文件名和行號對於定位問題原因常常很有用,對於堆分配函數卻可能不是這樣。雖然可在應用程序的邏輯樹中的許多適當點插入宏,但分配經常隱藏在特殊例程中,該例程會在很多不同時刻從很多不同位置進行調用。問題通常並不在於如何確定哪行代碼進行了錯誤分配,而在於如何確定該行代碼進行的上千次分配中的哪一次是錯誤分配以及原因。

唯一分配請求編號和 _crtBreakAlloc

標識發生錯誤的特定堆分配調用的最簡單方法是利用與調試堆中的每個塊關聯的唯一分配請求編號。當其中一個轉儲函數報告某塊的有關信息時,該分配請求編號將括在大括號中(例如“{36}”)。

知道某個錯誤分配塊的分配請求編號後,可以將該編號傳遞給 以創建一個斷點。執行將恰在分配該塊以前中斷,您可以向回追蹤以確定哪個例程執行了錯誤調用。為避免重新編譯,可以在調試器中完成同樣的操作,方法是將 _crtBreakAlloc 設置為所感興趣的分配請求編號。

創建分配例程的“Debug”版本

略微複雜的方法是創建您自己的分配例程的“Debug”版本,等同於堆分配函數_dbg 版本。然後,可以將源文件和行號參數傳遞給基礎堆分配例程,並能立即看到錯誤分配的出處。

例如,假定您的應用程序包含與下麵類似的常用例程:

int addNewRecord(struct RecStruct * prevRecord,
int recType, int recAccess)
{
// ...code omitted through actual allocation...
if ((newRec = malloc(recSize)) == NULL)
// ... rest of routine omitted too ...
}

在頭文件中,可以添加如下代碼:

#ifdef _DEBUG
#define addNewRecord(p, t, a) /
addNewRecord(p, t, a, __FILE__, __LINE__)
#endif

接下來,可以如下更改記錄創建例程中的分配:

int addNewRecord(struct RecStruct *prevRecord,
int recType, int recAccess
#ifdef _DEBUG
, const char *srcFile, int srcLine
#endif
)
{
/* ... code omitted through actual allocation ... */
if ((newRec = _malloc_dbg(recSize, _NORMAL_BLOCK,
srcFile, scrLine)) == NULL)
/* ... rest of routine omitted too ... */
}

在其中調用 addNewRecord 的源文件名和行號將存儲在產生的每個塊中(這些塊是在調試堆中分配的),並將在檢查該塊時進行報告。

最後更新:2017-04-02 00:06:17

  上一篇:go 北京奧組委宣布奧運門票開始麵向全球公眾預售
  下一篇:go Lucene學習筆記(應用)