繁体   English   中英

为什么我会得到 _CrtIsValidHeapPointer(block) 和/或 is_block_type_valid(header->_block_use) 断言?

[英]Why do I get _CrtIsValidHeapPointer(block) and/or is_block_type_valid(header->_block_use) assertions?

当我在调试模式下使用 VisualStudio 编译程序运行我的程序时,有时我会得到

调试断言失败! 表达式: _CrtIsValidHeapPointer(block)

或者

调试断言失败! 表达式: is_block_type_valid(header->_block_use)

(或两者之后)断言。

这是什么意思? 如何找到并修复此类问题的根源?

这些断言表明,应该释放的指针无效(或不再有效)( _CrtIsValidHeapPointer -assertion),或者堆在程序运行期间的某个时刻被破坏( is_block_type_valid(header->_block_use) -assertion又名_Block_Type_Is_Valid (pHead->nBlockUse) - 早期版本中的断言)。

从堆中获取内存时,函数malloc / free不直接与操作系统通信,而是与内存管理器通信,通常由相应的 C 运行时提供。 VisualStudio/Windows SDK 为调试构建提供了一个特殊的堆内存管理器,它在运行时执行额外的健全性检查。

_CrtIsValidHeapPointer只是一个启发式,但是无效指针的情况已经足够多,为此该函数可以报告问题。

1. _CrtIsValidHeapPointer -assertion 何时触发?

有一些最常见的场景:

A. 指针不指向堆中的内存开始:

char *mem = "not on the heap!";
free(mem); 

这里的文字没有存储在堆上,因此可以/不应该被释放。

B. 指针的值不是malloc / calloc返回的原始地址:

unsigned char *mem = (unsigned char*)malloc(100);
mem++;
free(mem); // mem has wrong address!

由于mem值在增量后不再是 64 字节对齐的,因此健全性检查可以很容易地看出它不能是堆指针!

一个稍微复杂但并不罕见的 C++ 示例(不匹配new[]delete ):

struct A {
    int a = 0;
    ~A() {// destructor is not trivial!
         std::cout << a << "\n";
    }
};
A *mem = new A[10];
delete mem;

调用new A[n] ,实际上是通过malloc分配了sizeof(size_t)+n*sizeof(A)字节的内存(当A类的析构函数不是平凡的时候),数组中的元素个数保存在分配内存的开头和返回的指针mem不是指向malloc返回的原始地址,而是指向地址+偏移量( sizeof(size_t) )。 但是, delete对此偏移量一无所知,并尝试删除地址错误的指针( delete []会做正确的事情)。

C.双免:

unsigned char *mem = (unsigned char*)malloc(10);
free(mem);
free(mem);  # the pointer is already freed

D. 来自另一个运行时/内存管理器的指针

Windows 程序能够同时使用多个运行时:每个使用过的 dll 都可能有自己的运行时/内存管理器/堆,因为它是静态链接的,或者因为它们有不同的版本。 因此,在一个 dll 中分配的内存在另一个 dll 中释放时可能会失败,该 dll 使用不同的堆(参见例如这个SO-question或 this SO-question )。

2. is_block_type_valid(header->_block_use) -assertion 什么时候触发?

在上述情况 A. 和 B. 中,此外is_block_type_valid(header->_block_use)也会触发。 _CrtIsValidHeapPointer -assertion 之后, free函数(更精确的free_dbg_nolock )在块头(调试堆使用的特殊数据结构,稍后会详细了解它)中查找信息并检查块类型是否有效。 然而,因为指针完全是假的,内存中nBlockUse预期所在的nBlockUse是一些随机值。

但是,在某些情况下,当is_block_type_valid(header->_block_use)没有先前_CrtIsValidHeapPointer -assertion 的情况下触发时。

A. _CrtIsValidHeapPointer不检测无效指针

下面是一个例子:

unsigned char *mem = (unsigned char*)malloc(100);
mem+=64;
free(mem);

因为 debug-heap 用0xCD填充分配的内存,我们可以肯定访问nBlockUse会产生错误的类型,从而导致上述断言。

B. 堆的损坏

大多数情况下,当is_block_type_valid(header->_block_use)没有_CrtIsValidHeapPointer情况下_CrtIsValidHeapPointer这意味着堆由于某些超出范围的写入而损坏。

因此,如果我们“精致”(并且不要覆盖“无人区”-稍后会详细介绍):

unsigned char *mem = (unsigned char*)malloc(100);
*(mem-17)=64; // thrashes _block_use.
free(mem);

仅导致is_block_type_valid(header->_block_use)


在上述所有情况下,可以通过跟踪内存分配来找到潜在的问题,但是了解更多关于调试堆的结构会有很大帮助。

关于调试堆的概述可以在例如文档中找到,或者可以在相应的 Windows 工具包中找到实现的所有细节,(例如C:\\Program Files (x86)\\Windows Kits\\10\\Source\\10.0.16299.0\\ucrt\\heap\\debug_heap.cpp )。

简而言之:当在调试堆上分配内存时,会分配比需要更多的内存,因此可以在“真实”内存旁边存储诸如“无人区”之类的附加结构和诸如_block_use类的附加信息。 实际的内存布局是:

------------------------------------------------------------------------
| header of the block + no man's land |  "real" memory | no man's land |
----------------------------------------------------------------------
|    32 bytes         +      4bytes   |     ? bytes    |     4 bytes   |
------------------------------------------------------------------------

“无人区”的结尾和开头的每个字节都被设置为一个特殊值( 0xFD ),因此一旦它被覆盖,我们就可以注册越界写访问(只要它们最多为 4 个字节)离开)。

例如在new[] - delete -mismatch 的情况下,我们可以在指针之前分析内存,看看这是否是无人区(这里是代码,但通常在调试器中完成):


A *mem = new A[10];
...
// instead of
//delete mem;
// investigate memory:
unsigned char* ch = reinterpret_cast<unsigned char*>(mem);
for (int i = 0; i < 16; i++) {
    std::cout << (int)(*(ch - i)) << " ";
}

我们得到:

0 0 0 0 0 0 0 0 10 253 253 253 253 0 0 52

即前 8 个字节用于元素数 (10),然后是我们看到的“无人区”( 0xFD=253 )以及其他信息。 很容易看出,出了什么问题 - 如果指针正确,则前 4 个值在哪里253

当调试堆释放内存时,它会用一个特殊的字节值覆盖它: 0xDD ,即221 还可以通过设置标志_CRTDBG_DELAY_FREE_MEM_DF来限制曾经使用和释放的内存的_CRTDBG_DELAY_FREE_MEM_DF ,因此内存不仅在free调用之后直接保持标记,而且在程序的整个运行过程中都保持标记。 所以当我们第二次尝试释放同一个指针时,调试堆可以看到内存已经被释放一次并触发断言。

因此,通过分析指针周围的值,也很容易看出问题是双重自由的:

unsigned char *mem = (unsigned char*)malloc(10);
free(mem);
for (int i = 0; i < 16; i++) {
    printf("%d ", (int)(*(mem - i)));
}
free(mem); //second free

印刷

221 221 221 221 221 221 221 221 221 221 221 221 221 221 221 221

内存,即内存已经被释放一次。

关于检测堆损坏:

无人区的目的是检测超出范围的写入,但这仅适用于在任一方向上关闭 4 个字节,例如:

unsigned char *mem = (unsigned char*)malloc(100);
*(mem-1)=64; // thrashes no-man's land
free(mem);

造成

HEAP CORRUPTION DETECTED: before Normal block (#13266) at 0x0000025C6CC21050.
CRT detected that the application wrote to memory before start of heap buffer.

查找堆损坏的一个好方法是使用_CrtSetDbgFlag(_CRTDBG_CHECK_ALWAYS_DF)ASSERT(_CrtCheckMemory()); (请参阅此SO-post )。 然而,这在某种程度上是间接的——这是一种更直接的使用gflags方式,如这篇SO-post 所述gflags需要大约 30 倍的内存并且慢大约 10 倍,这并不罕见)。


顺便说一句, _CrtMemBlockHeader的定义随着时间的推移而改变,不再是online-help 中显示的那个,但是:

struct _CrtMemBlockHeader
{
    _CrtMemBlockHeader* _block_header_next;
    _CrtMemBlockHeader* _block_header_prev;
    char const*         _file_name;
    int                 _line_number;
    
    int                 _block_use;
    size_t              _data_size;
    
    long                _request_number;
    unsigned char       _gap[no_mans_land_size];

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM