【问题标题】:Why do I get _CrtIsValidHeapPointer(block) and/or is_block_type_valid(header->_block_use) assertions?为什么我会得到 _CrtIsValidHeapPointer(block) 和/或 is_block_type_valid(header->_block_use) 断言?
【发布时间】:2021-02-01 17:09:50
【问题描述】:

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

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

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

(或两个都在一个之后)断言。

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

【问题讨论】:

    标签: c++ c debugging visual-c++


    【解决方案1】:

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

    在从堆中获取内存时,函数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]被调用时,实际上sizeof(size_t)+n*sizeof(A)bytes内存是通过malloc分配的(当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
    

    在 C++ 中一个非常常见的原因是 rule of three/five 没有被遵守,例如:

    struct A {// bad: doesn't adhere to rule of three
        int* ptr;
        A(int i): ptr(new int(i)){}
        ~A() { delete ptr; }
    };
    
    {
      A a(0);
      A b = a; // a and b share pointer: a.ptr == b.ptr
    } // here destructors of b and a called => problem
    //  at first b.ptr gets deleted
    //  deleting (already deleted) a.ptr leads now to UB/error.
    

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

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

    2。 is_block_type_valid(header-&gt;_block_use)-assertion 什么时候触发?

    在上述情况 A. 和 B. 中,除了 is_block_type_valid(header-&gt;_block_use) 也会触发。在_CrtIsValidHeapPointer-assertion 之后,free-函数(更精确的free_dbg_nolock)在块头(debug-heap 使用的特殊数据结构,稍后会详细介绍)中查找信息并检查块类型有效。但是,由于指针完全是伪造的,所以内存中 nBlockUse 的预期位置是某个随机值。

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

    A. _CrtIsValidHeapPointer 没有检测到无效指针

    这是一个例子:

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

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

    B.堆损坏

    大多数时候,当is_block_type_valid(header-&gt;_block_use) 在没有_CrtIsValidHeapPointer 的情况下触发时,这意味着堆由于一些超出范围的写入而损坏。

    所以如果我们“精致”(并且不要覆盖“无人区”——稍后会详细介绍):

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

    只导致is_block_type_valid(header-&gt;_block_use)


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

    可以找到关于调试堆的概述,例如in documentation,或者所有实现的细节都可以在相应的 Windows Kit 中找到(例如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 来限制对曾经使用和释放的内存的重用,因此内存不仅在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];
    };
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-08-03
      • 1970-01-01
      • 2017-01-25
      • 2016-04-03
      • 1970-01-01
      • 1970-01-01
      • 2011-02-28
      • 1970-01-01
      相关资源
      最近更新 更多