CRT 调试堆详细信息

CRT 调试堆和相关函数提供许多方法来跟踪和调试代码中的内存管理问题。 可以使用它来查找缓冲区溢出,以及跟踪和报告内存分配和内存状态。 它还支持为独特的应用需求创建自己的调试分配函数。

利用调试堆查找缓冲区溢出

程序员遇到的两种最常见而又难处理的问题是,覆盖已分配缓冲区的末尾以及内存泄漏(未能在不再需要某些分配后将其释放)。 调试堆提供功能强大的工具来解决这类内存分配问题。

堆函数的“调试”版本调用“发布”版本中使用的标准版本或基版本。 当请求内存块时,调试堆管理器从基堆分配略大于所请求的块的内存块,并返回指向该块中属于你的部分的指针。 例如,假定应用程序包含调用:malloc( 10 )。 在发行版本中,malloc 将调用基堆分配例程以请求分配 10 个字节。 而在调试版本中,malloc 将调用 _malloc_dbg,该函数接着调用基堆分配例程以请求分配 10 个字节加上大约 36 个字节的额外内存。 调试堆中产生的所有内存块在单个链接列表中连接起来,按照分配时间排序。

调试堆例程分配的额外内存用于簿记信息。 它具有将调试内存块链接在一起的指针,以及位于数据一侧的小缓冲区,用于捕获已分配区域的覆盖内容。

当前,用于存储调试堆的簿记信息的块标头结构在 <crtdbg.h> 标头中声明,并在 <debug_heap.cpp> CRT 源文件中定义。 从概念上讲,它类似于以下结构:

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)的直接调用来分配块,从而指定块的类型。 调试堆中五种类型的内存块(在 _CrtMemBlockHeader 结构的 nBlockUse 成员中设置)如下:

_NORMAL_BLOCK
malloccalloc 的调用将创建“正常”块。 如果只想使用“正常”块,并且不需要“客户端”块,则可能需要定义 _CRTDBG_MAP_ALLOC_CRTDBG_MAP_ALLOC 会导致所有堆分配调用映射到调试版本中的调试等效项。 它允许存储有关相应块标头中每个分配调用的文件名和行号信息。

_CRT_BLOCK
由许多运行库函数内部分配的内存块被标记为 CRT 块,以便可以单独处理这些块。 因此,泄漏检测和其他操作可能不受这些块影响。 分配永不可以分配、重新分配或释放任何 CRT 类型的块。

_CLIENT_BLOCK
出于调试目的,应用程序可以专门跟踪一组给定的分配,方法是使用对调试堆函数的显式调用将它们作为该类型的内存块进行分配。 例如,MFC 将所有 CObject 分配为客户端块;其他应用程序可能会在客户端块中保留不同的内存对象。 还可以指定客户端块的子类型,使跟踪粒度更大。 若要指定“客户端”块子类型,请将该数字向左移 16 位,并将它与 OR 进行 _CLIENT_BLOCK 运算。 例如:

#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 打开调试分配。 当此位关闭时,分配仍链接在一起,但它们的块类型为 _IGNORE_BLOCK
_CRTDBG_DELAY_FREE_MEM_DF 关闭 防止实际释放内存,与模拟内存不足情况相同。 当该位打开时,已释放块保留在调试堆的链接列表中,但标记为 _FREE_BLOCK,并用特殊字节值填充。
_CRTDBG_CHECK_ALWAYS_DF 关闭 导致 _CrtCheckMemory 每次分配和释放时均调用。 执行速度将减慢,但可快速捕捉错误。
_CRTDBG_CHECK_CRT_DF 导致将标记为 _CRT_BLOCK 类型的块包括在泄漏检测和状态差异操作中。 当该位为 off 时,在这些操作期间将忽略由运行库内部使用的内存。
_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 );
    

C++ 调试堆中的 newdelete_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 运算符的“Debug”版本可用于所有块类型,并且编译“Release”版本时程序中不需要任何更改。

堆状态报告函数

若要捕获给定时刻堆状态的摘要快照,请使用 <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 报告函数

下列函数报告堆的状态和内容,并使用这些信息帮助检测内存泄漏及其他问题:

函数 说明
_CrtMemCheckpoint 在应用程序提供的 _CrtMemState 结构中保存堆的快照。
_CrtMemDifference 比较两个内存状态结构,在第三个状态结构中保存二者之间的差异,如果两个状态不同,则返回 TRUE。
_CrtMemDumpStatistics 转储给定 _CrtMemState 结构。 该结构可能包含给定时刻调试堆状态的快照或两个快照之间的差异。
_CrtMemDumpAllObjectsSince 转储有关在堆的给定快照之后,或是从执行开始时起,分配的所有对象的信息。 如果已使用 _CrtSetDumpClient 安装挂钩函数,则每次它转储 _CLIENT_BLOCK 块时,都会调用应用程序所提供的挂钩函数。
_CrtDumpMemoryLeaks 确定自程序开始执行以来是否发生过内存泄漏,如果发生过,则转储所有已分配对象。 如果已使用 _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 ...
}

在头文件中,可以添加类似于以下示例的代码:

#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 的源文件名和行号将存储在产生的每个块中(这些块是在调试堆中分配的),并将在检查该块时进行报告。

另请参阅

调试本机代码