CRT Debug Heap Details
Find buffer overruns(溢出) with debug heap
程序员所遇到的两个最常见且最棘手的问题就是:overwirtes分配buffer的尾部和内存泄漏(当内存不再需要时,解分配失败)。debug heap 提供强大的工具来解决类似这种的内存分配问题。
heap functions 的 debug 版本调用用于Release构建的标准或基本版本。当你请求一个内存块,debug heap manager 从 base heap 中分配一个比请求内存稍微大一点的内存块,并且返回一个指向你的内存块部分的指针。例如,假设你的应用包含这样一个调用: malloc(10)。在 Release 构建中,molloc 将调用 base heap 分配的 routine来请求一个10字节的内存分配。然而,在 Debug 构建中,molloc 将调用 _malloc_dbg
,它将调用 base heap 分配 routine 请求一个10字节的内存分配,再加上大约36字节的额外内存。debug heap 中分配的所有的内存块在一条单链表中相互链接,并根据被分配的时间排序。
debug heap routine 分配的额外内存用于记录信息–将 debug 内存块链接在一起的指针,你的数据两边的小缓存,缓存用于捕获分配区域的overwirtes。
目前,block header 结构体用于存储 heap 的记录信息,它被声明在 DBGINT.H 头文件中,其声明如下:
typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before this one:
// 在本block之前分配的block指针
struct _CrtMemBlockHeader *pBlockHeaderNext;
// Pointer to the block allocated just after this one:
// 在本block之后分配的block指针
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:
// 用户内存前的buffer
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];
*/
在block的用户数据两边的NoMansLand
缓存目前为4字节大小,并由已知的字节值填充,由 debug heap routines 使用来证实用户内存块的限制没有被overwritten。debug heap 也为新的内存块填充已知值。如果你选择如下所述,将在和 heap 的链接链表中释放的内存块保留下来,这些释放的内存块仍然被一个已知值填充。目前,使用的实际字节值如下:
NoMandsLand(0xFD) 应用使用的内存两边的NoMansLand
buffer 使用 0xFD 填充。
释放的 block(0xDD) 当_CRTDBG_DELAY_FREE_MEM_DF
标志被设置时,在 debug heap 链接表中的已释放 blocks 保持未使用状态,目前由 0xDD 填充。
新的对象 (0xCD),当新的对象被分配时,使用 0xCD 填充。
Type of blocks on the debug heap
每个 debug heap 中的内存块都被赋值未5种分配类型种的一种。这些类型采用不同的追踪和报告方式,目的是检测内存泄漏和状态报告。在分配内存块时,直接调用 debug heap 分配函数之一,例如 _malloc_dbg
,你可以指定分配的块的类型。debug heap 中的内存块的5种类型(在_CrtMemBlockHeader
中的nBlockUse
成员中设置)如下:
_NORMAL_BLOCK
一个malloc
或calloc
创建一个 Normal block。如果你打算仅使用 Normal blocks,且没有对 Client blocks 的需求,你可能想要定义 _CRTDBG_MAP_ALLOC
,它会导致所有的堆分配调用映射到它们的在 Debug 构建中的等价版本。这将允许关于每个分配调用的文件名和行号信息被存储在对应的 block header 中。
_CRT_BLOCK
在运行时库函数内部分配的内存块被标记为 CRT blocks,所以它们能够被分开处理。结果,泄漏侦测和其他操作不必受它们的影响。一个分配必须从不分配,重分配或者释放任何CRT类型的block。
_CLIENT_BLOCK
为了调试目的,显式调用 debug heap 函数以使用此内存块类型分配一组内存块,应用能对这组内存块进行特殊的追踪。例如,MFC以Client blocks类型分配所有的CObjects。其他应用可能在 Client Blocks 中保存不同的内存对象。为了更小的跟踪粒度,还可以指定Client Blocks 的子类型。为了指定 Client blocks 的子类型,将述资左移16位并与_CLIENT_BLOCK
进行或操作。例如:
#define MYSUBTYPE 4
freedbg(pbData, _CLIENT_BLOCK|(MYSUBTYPE<<16));
用于dumping存储在Client blocks中对象,可以使用_CrtSetDumpClient
安装一个客户端提供的 hook 函数,无论何时Client block通过一个debug函数dumped时,它将会被调用。同样,_CrtDoForAllClientObjects
能被用于对所有在 debug head 中的Client blocks调用一个由应用提供的函数。
_FREE_BLOCK
通常,被释放的block将从链表中被移除。为了检差被释放的内存没有在继续被写或者模拟低内存的情况,你可以选择在链表中保存释放的blocks,被标记为Free,但是以已知的字节值填充(目前时 0xDD)。
_IGNORE_BLOCK
这可能会关闭 debug heap 操作一段时间。在此期间,内存块一直在链表中,但是被标记为 ignore block。
为了检测所给block的类型和子类型,使用函数 _CrtReportBlockType
和宏 _BLOCK_TYPE
和 _BLOCK+SUBTYPE
。宏定义(在 crtdbg.h)入下:
#define _BLOCK_TYPE(block) (block & 0xFFFF)
#define _BLOCK_SUBTYPE(block) (block >> 16 & 0xFFFF)
Check for heap integrity(完整性) and memory leaks
很多debug heap的特点必须在你的代码中访问。下列部分描述了一些特征和如何使用它们。
_CrtCheckMemory
你可以使用一个_CrtCheckMemory
的调用在任何时间点检查堆的完整性。这个函数检查堆中每个内存块的完整性,证实内存块 header 信息是合法的,确认buffer没有被修改。
_CrtSetDebugFlag
你可以使用一个内部的flag _crtDbgFlag
来控制debug heap 如何追踪分配。使用 _CrtSetDbgFlag
函数能够对这个标志进行读取和设置。通过改变这个参数,你可以命令debug heap 在程序退出时检查内存泄漏,并报告侦测到的任何泄漏。类似的,你可以指定释放的内存块不要从链表中移除,来模拟低内存情况。当堆被检查,这些释放的blocks将被检查,以保证他们没有被干扰。
_crtDbgFlag
包含以下位字段:
Bit field | Default | Desperation |
---|---|---|
_CRTDBG_ALLOC_MEM_DF | On | 打开 debug 分配。当这一位关闭,分配依然链接在一起,但是它们的块类型是 _IGNORE_BLOCK |
_CRTDBG_DELAY_FREE_MEM_DF | Off | 避免内存被实际释放,关于模拟低内存情况。当这一位打开,被释放的blocks依然被保留在 debug 堆链表中,但是被标记为_FREE_BLOCK,并以他是的字节值填充 |
_CRTDBG_CHECK_ALWAYS_DF | Off | 造成_CrtCheckMemory在每一次分配和解分配时都会被调用,所以这会减缓执行,但是能快速获取错误 |
_CRTDBG_CHECK_CRT_DF | Off | 造成blocks被标记为_CRT_BLOCK,则其被泄漏检测和状态改变操作包含。当这一位为off,运行时库内部使用的内存在执行这些操作时被忽略 |
_CRTDBG_LEAK_CHECK_DF | Off | 造成在程序退出时执行泄漏检查,通过调用_CrtDumpMemoryLeaks。如果应用在释放它所分配的内存上失败,将会生成一个错误报告 |
Configure the debug heap
所有的堆函数调用例如 malloc
,free
,calloc
,realloc
,new
和 delete
都被解析为在 debug heap 上操作的的 debug 版本。当你释放一个内存块,debug heap 自动的检查你分配区域的两侧buffer的完整性,如果发生了 overwriting 将会提出一个问题报告。
To use the debug heap
- 使用C run-time library 的 debug 版本来链接你程序的debug构建。
To change one or more _crtDbgFlag bit fields and create a new state for the flag
- 使用
newFlag
参数(设为_CRTDBG_REPORT_FLAG
,为了获取当前的_crtDbgFlag
状态) 来调用_CrtSetDbgFlag
,并在一个临时变量中存储返回值。 - 通过使用相应的位掩码(在应用程序代码中由常量清单表示)来
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 );
new, delete, and _CLIENT_BLOCKs in the C++ debug heap
C run-time library 的 debug 版本包含 C++ new
, delete
操作符的 debug 版本。如果你使用_CLIENT_BLOCK
分配类型,你必须直接调用new
操作符的 debug 版本或者在 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
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 版本对所有的block类型都有效,当你编译Release版本时,在你的程序中不需要改变 。
Heap State Reporting Functions
为了在一个给定时间点抓取堆状态的一个总结性的快照,使用定义在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;
这个结构体存储在 debug heap 链接表中的第一个(最近分配的)block。然后,在两个数组中,它记录链表中的每种类型( _NORMAL_BLOCK
, _CLIENT_BLOCK
, _FREE_BLOCK
, 等 )内存块数量,每种类型的blcok的分配字节数。最后,它记录截止到目前堆上分配的最高字节数,和当前分配的字节数。
Other CRT Reporting Function
下列函数报告堆的状态和内容,使用这些信息来侦测内存泄漏和其他问题:
Function | Description |
---|---|
_CrtMemCheckpoint | 在应用提供的_CrtMemState结构中保存堆的快照 |
_CreMemDifference | 比较两个内存状态结构体,在第三个状态结构体中保存它们之间的不同,如果两个之间有不同的话,返回true |
_CrtMemDumpStatistics | dump 一个所给的 _CrtMemState 结构体,机构体可能包含 debug heap 的在一个给定时刻的快照,或者两个快照之间的异常 |
_CrtMemDumpAllObjectsSince | dump 关于所有对象分配的信息,从一个给定的堆的快照被获取到,或者从执行的起始。每一次它 dump 一个_CLIENT_BLOCK block,它调用应用提供的 hook 函数,如果 hook 函数已经使用 _CrtSetDumpClient 安装了 |
_CrtDumpMemoryLeaks | 确定自程序开始执行起是否有内存泄漏发生,如果是,dump 所有的分配对象。每一次 _CrtDumpMemoryLeaks dumps 一个 _CLIENT_BLOCK block,它调用应用提供的 hook 函数,如果 hook 函数已经使用 _CrtSetDumpClient 安装了 |
Track Heap Allocation Requests
虽然准确指出断言或者报告宏执行所在的源文件名和行数在定位问题发生的原因十分有用,对于堆分配函数来说,情况就不尽相同了。尽管宏可以关注一个应用的逻辑树中很多合适的点,分配通常在隐藏在一个特殊的例程中,它可能在不同的地方被调用多次。问题通常不是在哪一行代码执行了一个坏的分配,而是在成千上万的由那行代码创建的分配中,哪一个是坏的,为什么。
Unique Allocation Request Numbers and _crtBreakAlloc
识别特定的坏的堆分配调用的最简单的方法是利用与debug heap中的每个block相关的唯一的分配请求号。当关于某个block的信息被某一个 dump 函数报告,这个分配请求号用大括号括起来了(例如,"{36}")。
一旦你知道一个分配不合适的block的分配请求号,你可以向_CrtSetBreakAlloc
函数传递这个数来创建一个断点。在分配这个block前,执行将break,你可以回溯来寻找本次调用的源头。为了避免重新编译,你可以在调试器中完成相同的事,通过对你感兴趣的分配请求号设置_crtBreakAlloc
。
Creating Debug Versions of Your Allocation Routines
一种稍微复杂一点的方法是创建你自己的分配例程的 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
的源文件名和行号将会被存储在每个结果块中,块在 debug heap 中分配,并在块被检查是报告。