CRT 偵錯堆積詳細資料

CRT 偵錯堆積和相關函式提供許多方法來追蹤和偵錯程式碼中的記憶體管理問題。 您可以使用它來尋找緩衝區滿溢,以及追蹤和報告記憶體配置和記憶體狀態。 它也支援針對您唯一的應用程式需求建立自己的偵錯配置函式。

使用偵錯堆積尋找緩衝區溢位

程式設計人員遇到的兩個最常見且棘手的問題是覆寫配置緩衝區和記憶體流失的結尾(不再需要配置之後無法釋放配置)。 偵錯堆積提供的強大工具,可以解決這類的記憶體配置問題。

堆積函式的偵錯版本是呼叫發行版本裡使用之函式的標準或基底版本。 當您要求記憶體區塊時,偵錯堆積管理員會從基底堆積配置比您要求更大的記憶體區塊,並傳回該區塊部分的指標。 例如,假設您的應用程式包含呼叫:malloc( 10 )。 在發行組建中, malloc 會呼叫要求配置 10 個位元組的基底堆積配置常式。 不過,在偵錯組建中, malloc 會呼叫 _malloc_dbg ,然後呼叫基底堆積配置常式,要求配置 10 個位元組加上大約 36 個位元組的額外記憶體。 偵錯堆積裡所有產生的記憶體區塊會在單向連結串列 (Single-Linked List) 中完成連接 (依配置時間排列順序)。

偵錯堆積常式所配置的額外記憶體會用於記帳資訊。 其指標會將偵錯記憶體區塊連結在一起,以及資料任一端的小緩衝區,以攔截已配置區域的覆寫。

目前,用來儲存偵錯堆積簿記資訊的區塊標頭結構會在標頭中 <crtdbg.h> 宣告,並在 CRT 原始程式檔中 <debug_heap.cpp> 定義。 在概念上,它類似于這個結構:

typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before this one:
    _CrtMemBlockHeader* _block_header_next;
// Pointer to the block allocated just after this one:
    _CrtMemBlockHeader* _block_header_prev;
    char const*         _file_name;
    int                 _line_number;

    int                 _block_use;      // Type of block
    size_t              _data_size;      // Size of user block

    long                _request_number; // Allocation number
// Buffer just before (lower than) the user's memory:
    unsigned char       _gap[no_mans_land_size];

    // Followed by:
    // unsigned char    _data[_data_size];
    // unsigned char    _another_gap[no_mans_land_size];
} _CrtMemBlockHeader;

no_mans_land區塊使用者資料區任一端的緩衝區目前大小為 4 個位元組,而且會填入偵錯堆積常式所使用的已知位元組值,以確認使用者記憶體區塊的限制尚未被覆寫。 偵錯堆積也會以一個已知值來填寫新的記憶體區塊。 如果您選擇將釋放的區塊保留在堆積的連結清單中,這些已釋放的區塊也會填入已知的值。 目前,使用的實際位元組值如下:

no_mans_land (0xFD)
應用程式使用之記憶體的任一端的「no_mans_land」緩衝區目前會填入0xFD。

釋放區塊 (0xDD)
_CRTDBG_DELAY_FREE_MEM_DF 旗標設定時,偵錯堆積中的連結串列保持未使用的釋放區塊目前是填入 0xDD。

新物件 (0xCD)
新物件會在配置時填入0xCD。

偵錯堆積上的區塊類型

偵錯堆積裡的每個記憶體區塊會設定成五種配置類型的其中一種。 這些類型可以針對不同的流失偵測和狀態報告目的來追蹤和報告。 您可以使用直接呼叫其中一個偵錯堆積配置函式來指定區塊的類型,例如 _malloc_dbg 。 偵錯堆積中的五種類型的記憶體區塊(在 結構的成員 _CrtMemBlockHeadernBlockUse 設定)如下所示:

_NORMAL_BLOCK
呼叫 malloccalloc 建立 Normal 區塊。 如果您想要只使用 Normal 區塊,而且不需要用戶端區塊,您可能想要定義 _CRTDBG_MAP_ALLOC_CRTDBG_MAP_ALLOC 會導致所有堆積配置呼叫都對應至偵錯組建中的偵錯對等專案。 它允許儲存對應區塊標頭中每個配置呼叫的檔案名和行號資訊。

_CRT_BLOCK
由許多執行階段程式庫函式內部所配置的記憶體區塊標記為 CRT 區塊,以便分別處理。 因此,洩漏偵測和其他作業可能不受其影響。 配置必須從未配置、重新配置或釋放任何 CRT 類型的區塊。

_CLIENT_BLOCK
應用程式可以使用這種記憶體區塊類型配置、使用偵錯堆積函式的明確呼叫,繼續追蹤指定的配置群組,以達到偵錯的目的。 例如,MFC 會將所有 CObject 物件配置為 Client 區塊;其他應用程式可能會在 Client 區塊中保留不同的記憶體物件。 也可以為達更細微的追蹤而設定用戶端區塊的子類型。 若要指定用戶端區塊的子類型,將數字向左移位 (Left Shift) 16 個位元並且以 OR 將之 _CLIENT_BLOCK 起來。 例如:

#define MYSUBTYPE 4
freedbg(pbData, _CLIENT_BLOCK|(MYSUBTYPE<<16));

用戶端提供的攔截函式,可使用 來傾 _CrtSetDumpClient 印儲存在 Client 區塊中的物件,然後在偵錯函式傾印用戶端區塊時呼叫。 此外, _CrtDoForAllClientObjects 也可以用來呼叫應用程式針對偵錯堆積中每個 Client 區塊所提供的指定函式。

_FREE_BLOCK
一般來說,此清單會移除釋放的區塊。 若要檢查釋放的記憶體未寫入,或模擬記憶體不足的情況,您可以將已釋放的區塊保留在連結清單上,標示為 Free,並填入已知的位元組值(目前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 另一 開啟偵錯配置。 當此位關閉時,配置會保持鏈結在一起,但其區塊類型為 _IGNORE_BLOCK
_CRTDBG_DELAY_FREE_MEM_DF 關閉 防止真的釋放記憶體,這是為了模擬低記憶體情況。 當這個位開啟時,釋放的區塊會保留在偵錯堆積的連結清單中,但會標示為 _FREE_BLOCK 並填入特殊位元組值。
_CRTDBG_CHECK_ALWAYS_DF 關閉 在每次配置和解除配置時呼叫的原因 _CrtCheckMemory 。 執行速度較慢,但會快速攔截錯誤。
_CRTDBG_CHECK_CRT_DF 關閉 導致標示為類型的 _CRT_BLOCK 區塊包含在洩漏偵測和狀態差異作業中。 當這個位元關閉時,會忽略在這類操作期間執行階段程式庫內部所使用的記憶體。
_CRTDBG_LEAK_CHECK_DF 關閉 導致透過呼叫 _CrtDumpMemoryLeaks 在程式結束時執行洩漏檢查。 如果應用程式無法釋放它所配置的所有記憶體,會產生錯誤報告。

設定偵錯堆積

所有堆積函式的呼叫,例如 mallocfreecallocreallocnewdelete 都會解析成操作於偵錯堆積裡的這些函式之偵錯版本。 當您釋放記憶體區塊時,偵錯堆積會自動檢查配置區域每端的緩衝區之完整性,如果發生覆寫發便會發出錯誤報告。

若要使用偵錯堆積

  • 將應用程式的偵錯組建連結至 C 執行時間程式庫的偵錯版本。

若要變更一或多個 _crtDbgFlag 位欄位,並建立旗標的新狀態

  1. 使用設為 _CrtSetDbgFlag (為取得目前的 newFlag 狀態) 的 _CRTDBG_REPORT_FLAG 參數來呼叫 _crtDbgFlag,且將傳回值儲存在暫存變數中。

  2. 在暫存變數上使用位 | 運算子 (「or」) 搭配對應的位元遮罩來開啟任何位(以資訊清單常數在應用程式程式碼中表示)。

  3. 在具有適當位元遮罩的位 ~ 運算子 (「not」 或補數) 的變數上使用位 & 運算子 (「and」) 關閉其他位。

  4. 使用設成儲存於暫存變數值的 _CrtSetDbgFlag 參數呼叫 newFlag,以便建立 _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 );
    

newC++ 偵錯堆積中的 、 delete_CLIENT_BLOCK 配置

C 執行階段程式庫的偵錯版本包含 C++ newdelete 運算子的偵錯版本。 如果您使用 _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> 定義的 結構:

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 報告函式

下列函式報告堆積的狀態和內容,並且使用資訊來幫助偵測記憶體流失和其他問題。

函式 描述
_CrtMemCheckpoint 將堆積的快照集儲存在 _CrtMemState 應用程式所提供的結構中。
_CrtMemDifference 比較兩個記憶體狀態結構,將它們之間的差異儲存在第三個狀態結構,如果兩個狀態不同則傳回 TRUE。
_CrtMemDumpStatistics 傾印指定的 _CrtMemState 結構。 結構可能包含指定時間裡偵錯堆積的狀態快照或者是兩個快照之間的差異。
_CrtMemDumpAllObjectsSince 傾印從堆積的指定快照使用後或從執行開始的所有配置物件之相關資訊。 每次傾印 _CLIENT_BLOCK 區塊時,如果已使用 _CrtSetDumpClient 安裝攔截函式,就會呼叫應用程式所提供的攔截函式。
_CrtDumpMemoryLeaks 判斷自從程式執行開始時,是否有任何的記憶體流失發生,如果有的話,傾印所有配置的物件。 _CrtDumpMemoryLeaks每次傾印 _CLIENT_BLOCK 區塊時,如果已使用 _CrtSetDumpClient 安裝攔截函式,它會呼叫應用程式所提供的攔截函式。

追蹤堆積配置要求

瞭解判斷提示或報告宏的來原始檔案名和行號,通常有助於尋找問題的原因。 堆積配置函式不太可能也是如此。 雖然您可以在應用程式邏輯樹狀結構中的許多適當點插入宏,但配置通常會埋藏在許多不同時間從許多不同位置呼叫的函式中。 問題不是程式程式碼的設定不正確。 相反地,這是該行程式碼所做出的數千個配置之一是錯誤的,以及原因。

唯一配置要求號碼和 _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 ...
}

在標頭檔中,您可以新增程式碼,例如下列範例:

#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 位置的原始程式檔名稱和行號,而區塊檢查時也會報告這些資訊。

另請參閱

偵錯機器碼