CRT 偵錯堆積詳細資料
本主題提供 CRT 偵錯堆積的詳細檢視。
內容
使用偵錯堆積尋找緩衝區滿溢
偵錯堆積上的區塊類型
檢查堆積的完整性和記憶體流失
設定偵錯堆積
C++ 偵錯堆積中的 new、delete 和 _CLIENT_BLOCK
堆積狀態報告函式
追蹤堆積配置要求
使用偵錯堆積尋找緩衝區滿溢
開發人員最常面臨的兩種難解決的問題是,覆寫配置緩衝區的結尾和記憶體流失 (無法在不再需要時釋放配置)。 偵錯堆積提供的強大工具,可以解決這類的記憶體配置問題。
堆積函式的偵錯版本是呼叫發行版本裡使用之函式的標準或基底版本。 當您要求記憶體區塊時,偵錯堆積管理員會從基底堆積配置比要求稍微大一點的記憶體區塊,並且傳回此區塊部分的指標。 例如,假設您的應用程式包含呼叫:malloc( 10 )。 在發行組建裡,malloc 會呼叫要求 10 位元組的基底堆積配置常式。 然而在偵錯組建裡,malloc 會呼叫 _malloc_dbg,它會呼叫要求 10 位元組加上大約 36 位元組的額外記憶體配置的基底堆積配置常式。 偵錯堆積裡所有產生的記憶體區塊會在單向連結串列 (Single-Linked List) 中完成連接 (依配置時間排列順序)。
偵錯堆積常式配置的額外記憶體是用於簿記資訊,這些資訊可能為將偵錯記憶體區塊連結在一起的指標,和用來捕捉配置區域覆寫的資料每端的小型緩衝區。
目前,用於儲存偵錯堆積的簿記資訊的區塊標頭結構會根據 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)
在應用程式使用記憶體的每端的 "NoMansLand" 緩衝區目前是填入 0xFD。釋放區塊 (0xDD)
當 _CRTDBG_DELAY_FREE_MEM_DF 旗標設定時,偵錯堆積中的連結串列保持未使用的釋放區塊目前是填入 0xDD。新物件 (0xCD)
新物件在配置時會填入 0xCD。
內容
偵錯堆積上的區塊類型
偵錯堆積裡的每個記憶體區塊會設定成五種配置類型的其中一種。 這些類型可以針對不同的流失偵測和狀態報告目的來追蹤和報告。 您可以使用直接呼叫其中一個偵錯堆積配置函式,例如 _malloc_dbg,來指定區塊類型。 五種偵錯堆積 (於 _CrtMemBlockHeader 結構的 nBlockUse 成員設定) 裡的記憶體區塊類型如下:
_NORMAL_BLOCK
呼叫 malloc 或 Calloc 建立一般區塊。 如果您只要使用一般區塊,而且不需要用戶端區塊,您可能要定義 _CRTDBG_MAP_ALLOC,它會造成所有堆積配置呼叫都對應至它們在偵錯組建裡的偵錯對等用法。 這可將每個配置呼叫的相關檔名和行號資訊儲存在對應的區塊標頭裡。_CRT_BLOCK
由許多執行階段程式庫函式內部所配置的記憶體區塊標記為 CRT 區塊,以便分別處理。 因此,流失偵測和其他操作不會受到影響。 配置必須從未配置、重新配置或釋放任何 CRT 類型的區塊。_CLIENT_BLOCK
應用程式可以使用這種記憶體區塊類型配置、使用偵錯堆積函式的明確呼叫,繼續追蹤指定的配置群組,以達到偵錯的目的。 例如,MFC 將所有的 CObjects 配置為用戶端區塊;其他應用程式可能會將不同的記憶體物件保持在用戶端區塊中。 也可以為達更細微的追蹤而設定用戶端區塊的子類型。 若要指定用戶端區塊的子類型,將數字向左移位 (Left Shift) 16 個位元並且以 _CLIENT_BLOCK 將之 OR 起來。 例如:#define MYSUBTYPE 4 freedbg(pbData, _CLIENT_BLOCK|(MYSUBTYPE<<16));
可以使用 _CrtSetDumpClient 安裝用戶端提供的攔截函式 (可用來傾印儲存在用戶端區塊的物件),每當偵錯函式傾印用戶端區塊時就會被呼叫。 此外,可使用 _CrtDoForAllClientObjects,來呼叫應用程式為偵錯堆積裡的每個用戶端區塊所提供的指定函式。
_FREE_BLOCK
一般來說,此清單會移除釋放的區塊。 若要檢查釋放記憶體是否仍然不能寫入,或模擬低記憶體條件,您可以選擇保留連結串列上的釋放區塊,將其標記為可用,並填入已知位元組值 (目前是 0xDD)。_IGNORE_BLOCK
可以在某段時間關閉偵錯堆積操作。 在這段期間,記憶體區塊會保留於清單終上,但是標記為忽略區塊。
若要判斷指定區塊的類型和子類型,請使用 _CrtReportBlockType 函式、_BLOCK_TYPE 和 _BLOCK_SUBTYPE 巨集。 巨集會定義 (在 crtdbg.h 裡),參見下例:
#define _BLOCK_TYPE(block) (block & 0xFFFF)
#define _BLOCK_SUBTYPE(block) (block >> 16 & 0xFFFF)
內容
檢查堆積的完整性和記憶體流失
許多偵錯堆積的功能必須從程式碼內存取。 下一節將說明一些功能以及如何使用這些功能。
_CrtCheckMemory
例如,您可以使用 _CrtCheckMemory 的呼叫來檢查任何一點的堆積完整性。 這個函式檢查堆積裡的每個記憶體區塊,確認記憶體區塊標頭資訊是有效的,並且確認緩衝區未經修改。_CrtSetDbgFlag
您可以使用 _crtDbgFlag 內部旗標,來控制偵錯堆積追蹤配置的方式,該旗標可以使用 _CrtSetDbgFlag 函式進行讀取和設定。 您可以變更這個旗標,來指示偵錯堆積在程式結束時檢查記憶體流失,並且報告任何偵測到的遺漏。 同樣地,您可以指定連結串列不要移除釋放的記憶體區塊,以模擬低記憶體情況。 檢查堆積時,這些釋放的區塊會在它們的項目裡檢查以確定它們沒有被干擾。_crtDbgFlag 旗標包含下列位元欄位:
位元欄位
預設
值
說明
_CRTDBG_ALLOC_MEM_DF
On
開啟偵錯配置。 當這個位元關閉時,配置會繼續連結在一起,但是區塊類型是 _IGNORE_BLOCK。
_CRTDBG_DELAY_FREE_MEM_DF
Off
防止真的釋放記憶體,這是為了模擬低記憶體情況。 當這個位元開啟時,釋放區塊會保持在偵錯堆積的連結串列裡,但是會標記為 _FREE_BLOCK 並且會填入一個特殊位元組值。
_CRTDBG_CHECK_ALWAYS_DF
Off
使得每次配置和解除配置時都會呼叫 _CrtCheckMemory。 這會減緩執行,但可以快速捕捉錯誤。
_CRTDBG_CHECK_CRT_DF
Off
造成區塊標記為 _CRT_BLOCK 類型,以便包含於流失偵測和狀態差異操作中。 當這個位元關閉時,會忽略在這類操作期間執行階段程式庫內部所使用的記憶體。
_CRTDBG_LEAK_CHECK_DF
Off
以呼叫 _CrtDumpMemoryLeaks,來執行在程式結束時的流失檢查。 如果應用程式無法釋放它所配置的所有記憶體,會產生錯誤報告。
內容
設定偵錯堆積
所有堆積函式的呼叫,例如 malloc、free、calloc、realloc、new 和 delete 都會解析成操作於偵錯堆積裡的這些函式之偵錯版本。 當您釋放記憶體區塊時,偵錯堆積會自動檢查配置區域每端的緩衝區之完整性,如果發生覆寫發便會發出錯誤報告。
若要使用偵錯堆積
- 以 C 執行階段程式庫的偵錯版本連結您應用程式的偵錯組建。
若要變更一個或多個 _crtDbgFlag 位元欄位並建立旗標的新狀態
使用設為 _CRTDBG_REPORT_FLAG (為取得目前的 _crtDbgFlag 狀態) 的 newFlag 參數來呼叫 _CrtSetDbgFlag,且將傳回值儲存在暫存變數中。
使用 OR (位元 (Bitwise) | 符號) 暫存變數和對應的位元遮罩 (以資訊清單常數 (Manifest Constant) 表示於應用程式程式碼) 來開啟任何位元。
使用 AND (位元 & 符號) 變數和適當位元遮罩的 NOT (位元 ~ 符號) 來關閉其他位元。
使用設成儲存於暫存變數值的 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++ 偵錯堆積中的 new、delete 和 _CLIENT_BLOCK
C 執行階段程式庫的偵錯版本包含 C++ new 及 delete 運算子的偵錯版本。 如果您使用 _CLIENT_BLOCK 配置類型,則必須直接呼叫 new 運算子的偵錯版本,或建立可以取代偵錯模式中 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
Use a default workspace for a Console Application to
* build a Debug version of this code
*/
#include "crtdbg.h"
#include "mydbgnew.h"
#ifdef _DEBUG
#define new DEBUG_CLIENTBLOCK
#endif
int main( ) {
char *p1;
p1 = new char[40];
_CrtMemDumpAllObjectsSince( NULL );
}
delete 運算子的偵錯版本會作用在所有的區塊類型上,當您編譯發行版本時不需要在程式裡做任何的變更。
內容
堆積狀態報告函式
_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 安裝,每次它傾印 _CLIENT_BLOCK 區塊時,就會呼叫應用程式所提供的攔截函式。 |
|
判斷自從程式執行開始時,是否有任何的記憶體流失發生,如果有的話,傾印所有配置的物件。 如果應用程式是使用 _CrtSetDumpClient 安裝,每次 _CrtDumpMemoryLeaks 傾印 _CLIENT_BLOCK 區塊時,它就會呼叫應用程式所提供的攔截函式。 |
內容
追蹤堆積配置要求
雖然指出判斷提示或報告巨集執行的原始程式檔名稱和行號通常在找出問題原因很有用,但是對於堆積配置函式可能就不是這樣。 雖然巨集可以插入到許多在應用程式邏輯樹狀圖裡合適的點,但是配置通常是在許多不同時間裡由許多不同地方的特殊常式呼叫。 問題通常不是哪一行程式碼做了錯誤的配置,而是上千個配置中,哪一個配置是由哪一錯誤程式碼所造成,以及其錯誤原因為何。
唯一配置要求號碼和 _crtBreakAlloc
最簡單的辨識發生錯誤的特定堆積配置呼叫方法,是利用與偵錯堆積裡每個區塊相關的唯一配置要求編號。 當區塊的相關資訊由其中一個傾印函式報告時,這個配置要求編號會包含在大括號裡 (例如 "{36}")。
一旦您知道不適當配置區塊的配置要求編號時,您可以將這個編號傳入至 _CrtSetBreakAlloc 來建立中斷點。 執行會在配置區塊之前中斷,而您即可回溯追蹤以判斷哪一個常式要對這個錯誤呼叫負責。 若要避免重新編譯,您可以在偵錯工具裡將 _crtBreakAlloc 設定為您要的配置要求編號,來完成相同的事。
建立配置常式的偵錯版本
較複雜的方法是建立您自己的配置常式的偵錯版本,其須與堆積配置函式的 _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 ...
}
您可以在標頭檔 (Header File) 裡加入像這樣的程式碼:
#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 位置的原始程式檔名稱和行號,而區塊檢查時也會報告這些資訊。
內容