本節內容
-
記憶體管理和調試堆
-
描述堆函數的“Debug”版本。這些函數解決兩個最難處理的記憶體配置問題:改寫已指派緩衝區的結尾和記憶體流失(當不再需要分配後未能釋放它們)。
-
調試堆中的塊類型
-
描述在調試堆中記憶體塊所分配到的五種配置類型。出於泄漏檢測和狀態報表的目的,以不同方式對這些配置類型進行跟蹤和報告。
-
調試堆
-
提供有關使用調試堆的資訊。資訊包括:哪些調用用於“Debug”版本,釋放記憶體塊時將發生什麼,哪些調試功能必須從代碼內部進行訪問,更改
_crtDbgFlag 位域以建立標誌的新狀態的步驟,以及一個闡釋如何開啟自動泄漏檢測和如何關閉
_CRT_BLOCK 類型塊的檢查的程式碼範例。
-
C++ 中的調試堆
-
討論 C++
new 和
delete 運算子的“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)
調試堆
對堆函數(如 malloc、free、calloc、realloc、new 和 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 位域並建立標誌的新狀態
- 在 newFlag 參數設定為 _CRTDBG_REPORT_FLAG 的情況下調用 _CrtSetDbgFlag(以獲得當前的 _crtDbgFlag 狀態),並在一個臨時變數中儲存傳回值。
- 開啟任何位,對臨時變數與相應位屏蔽(在應用程式代碼中由清單常數表示)進行 OR 運算(按位 | 符號)。
- 關閉其他位,對該變數與相應位屏蔽的 NOT(按位 ~ 符號)進行 AND 運算(按位 & 符號)。
- 在 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 的源檔案名稱和行號將儲存在產生的每個塊中(這些塊是在調試堆中分配的),並將在檢查該塊時進行報告。