为什么我会得到 _CrtIsValidHeapPointer(block) 和/或 is_block_type_valid(header->_block_use) 断言?
当我在调试模式下使用 VisualStudio 编译程序运行我的程序时,有时我会得到
<块引用>调试断言失败!表达式:_CrtIsValidHeapPointer(block)
或
<块引用>调试断言失败!表达式:is_block_type_valid(header->_block_use)
(或两个相继)断言.
什么意思?如何找到并修复此类问题的根源?
解决方案这些断言表明,应该释放的指针无效(或不再有效)(_CrtIsValidHeapPointer
-assertion)或堆在程序运行期间的某个时间点被破坏 (is_block_type_valid(header->_block_use)
-assertion aka _Block_Type_Is_Valid (pHead->nBlockUse)
-assertion在早期版本中).
从堆中获取内存时,malloc
/free
函数不直接与操作系统通信,而是与内存管理器通信,通常由相应的内存管理器提供C-运行时.VisualStudio/Windows SDK 为调试构建提供了一个特殊的堆内存管理器,它在运行时执行额外的健全性检查.
_CrtIsValidHeapPointer
只是一个启发式,但是无效指针的情况已经足够多,这个函数可以报告问题.
1._CrtIsValidHeapPointer
-断言何时触发?
有一些最常见的场景:
A.指针不指向从堆开始的内存:
char *mem = 不在堆上!";免费(内存);
这里的文字没有存储在堆上,因此可以/不应该被释放.
B.指针的值不是malloc
/calloc
返回的原始地址:
unsigned char *mem = (unsigned char*)malloc(100);内存++;免费(内存);//mem地址错误!
由于 mem
的值在增量后不再是 64byte 对齐的,所以通过完整性检查可以很容易地看出它不能是堆指针!
一个稍微复杂但并不罕见的 C++ 示例(不匹配 new[]
和 delete
):
struct A {int a = 0;~A() {//析构函数不是微不足道的!std::cout <<<<
";}};A *mem = 新 A[10];删除内存;
当new A[n]
被调用时,实际上sizeof(size_t)+n*sizeof(A)
字节内存是通过malloc
(当A
类的析构函数不是平凡时),数组中元素的个数保存在分配内存的开头,返回的指针mem
指向的不是到 malloc
返回的原始地址,但是到地址+偏移量 (sizeof(size_t)
).然而,delete
对这个偏移量一无所知,并试图删除地址错误的指针(delete []
会做正确的事情).
C.双重免费:
unsigned char *mem = (unsigned char*)malloc(10);免费(内存);免费(内存);# 指针已经被释放
C++ 中一个很常见的原因是 3 的规则/5 没有得到遵守,例如:
struct A {//bad: 不遵守三规则int* ptr;A(int i): ptr(new int(i)){}~A() { 删除指针;}};{A(0);a b = a;//a 和 b 共享指针:a.ptr == b.ptr}//这里 b 和 a 的析构函数被调用 =>问题//首先 b.ptr 被删除//删除(已经删除)a.ptr 现在会导致 UB/error.
D.来自另一个运行时/内存管理器的指针
Windows 程序能够同时使用多个运行时:每个使用的 dll 都可能有自己的运行时/内存管理器/堆,因为它是静态链接的,或者因为它们有不同的版本.因此,在一个 dll 中分配的内存在另一个 dll 中释放时可能会失败,该 dll 使用不同的堆(参见例如这个 SO-question 或这个 SO 问题).
2.is_block_type_valid(header->_block_use)
-assertion 何时触发?
在上述情况 A. 和 B. 中,另外 is_block_type_valid(header->_block_use)
也会触发.在 _CrtIsValidHeapPointer
断言之后,free
函数(更精确的 free_dbg_nolock
)在块头(由调试堆,稍后会提供更多信息)并检查块类型是否有效.但是,由于指针完全是伪造的,因此 nBlockUse
预期在内存中的位置是一些随机值.
但是,在某些情况下,当 is_block_type_valid(header->_block_use)
在没有先前的 _CrtIsValidHeapPointer
-assertion 的情况下触发时.
A._CrtIsValidHeapPointer
不检测无效指针
这是一个例子:
unsigned char *mem = (unsigned char*)malloc(100);内存+=64;免费(内存);
因为 debug-heap 用 0xCD
填充分配的内存,我们可以肯定访问 nBlockUse
会产生错误的类型,从而导致上述断言.>
B.堆损坏
大多数时候,当 is_block_type_valid(header->_block_use)
在没有 _CrtIsValidHeapPointer
的情况下触发时,这意味着堆由于某些超出范围而损坏写.
所以如果我们精致"(并且不要覆盖无人区"-稍后会详细介绍):
unsigned char *mem = (unsigned char*)malloc(100);*(mem-17)=64;//颠簸 _block_use.免费(内存);
仅导致 is_block_type_valid(header->_block_use)
.
在上述所有情况下,可以通过跟踪内存分配来找到潜在的问题,但了解更多关于调试堆的结构会有很大帮助.
可以找到有关调试堆的概述,例如在文档中,或者所有的实现细节都可以在相应的Windows Kit中找到,(例如C:Program Files (x86)Windows Kits10Source10.0.16299.0ucrtheapdebug_heap.cpp代码>).
简而言之:当在调试堆上分配内存时,分配的内存比需要的多,因此无人区"之类的附加结构将被分配.和附加信息,例如 _block_use
,可以存储在真实"旁边.记忆.实际的内存布局是:
-------------------------------------------------------------------------|区块头+无人区|真实"记忆|无人区| 高分辨率照片| CLIPARTO----------------------------------------------------------------------|32 字节 + 4 字节 |?字节 |4 字节 |-----------------------------------------------------------------
无人区"中的每一个字节在末尾和开头设置为一个特殊值 (0xFD
),因此一旦它被覆盖,我们就可以注册越界写访问(只要它们最多关闭 4 个字节)).
比如在new[]
-delete
-mismatch的情况下,我们可以分析一下指针之前的内存,看看这是否是无人区(这里作为代码,但通常在调试器中完成):
A *mem = 新 A[10];...//代替//删除内存;//调查内存:unsigned char* ch = reinterpret_cast(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);免费(内存);for (int i = 0; i <16; i++) {printf("%d", (int)(*(mem - i)));}免费(内存);//第二个空闲
印刷品
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;//击败无人区免费(内存);
导致
检测到堆损坏:在正常块 (#13266) 之前 0x0000025C6CC21050.CRT 检测到应用程序在开始堆缓冲区之前写入内存.
查找堆损坏的一个好方法是使用 _CrtSetDbgFlag(_CRTDBG_CHECK_ALWAYS_DF)
或 ASSERT(_CrtCheckMemory());
(请参阅此 SO-post).但是,这有点间接 - 使用 gflags
的更直接方式,如本 SO-post<中所述/a>(gflags
需要大约 30 倍的内存并且慢大约 10 倍,这并不罕见).
顺便说一句,_CrtMemBlockHeader
的定义随着时间的推移而改变,不再是 在线帮助,但是:
struct _CrtMemBlockHeader{_CrtMemBlockHeader* _block_header_next;_CrtMemBlockHeader* _block_header_prev;字符常量* _file_name;int_line_number;int_block_use;size_t _data_size;长_request_number;无符号字符_gap [no_mans_land_size];//其次是://无符号字符 _data[_data_size];//unsigned char _another_gap[no_mans_land_size];};
When I run my with VisualStudio compiled programs in debug-mode, sometimes I get
Debug assertion failed! Expression:
_CrtIsValidHeapPointer(block)
or
Debug assertion failed! Expression:
is_block_type_valid(header->_block_use)
(or both after each other) assertions.
What does it mean? How can I find and fix the origin of such problems?
解决方案These assertions show that either the pointer, which should be freed is not (or no longer) valid (_CrtIsValidHeapPointer
-assertion) or that the heap was corrupted at some point during the run of the program (is_block_type_valid(header->_block_use)
-assertion aka _Block_Type_Is_Valid (pHead->nBlockUse)
-assertion in earlier versions).
When acquiring memory from the heap, functions malloc
/free
don't communicate directly with the OS, but with a memory manager, which is usually provided by the corresponding C-runtime. VisualStudio/Windows SDK provide a special heap-memory manager for debug-builds, which performs additional sanity checks during the run time.
_CrtIsValidHeapPointer
is just a heuristic, but there are enough cases of invalid pointers, for which this function can report a problem.
1. When does _CrtIsValidHeapPointer
-assertion fire?
There are some of the most usual scenarios:
A. Pointer doesn't point to a memory from the heap to begin with:
char *mem = "not on the heap!";
free(mem);
here the literal isn't stored on the heap and thus can/should not be freed.
B. The value of the pointer isn't the original address returned by malloc
/calloc
:
unsigned char *mem = (unsigned char*)malloc(100);
mem++;
free(mem); // mem has wrong address!
As value of mem
is no longer 64byte aligned after the increment, the sanity check can easily see that it cannot be a heap-pointer!
A slightly more complex, but not unusual C++-example (mismatch new[]
and delete
):
struct A {
int a = 0;
~A() {// destructor is not trivial!
std::cout << a << "
";
}
};
A *mem = new A[10];
delete mem;
When new A[n]
is called, actually sizeof(size_t)+n*sizeof(A)
bytes memory are allocated via malloc
(when the destructor of the class A
is not trivial), the number of elements in array is saved at the beginning of the allocated memory and the returned pointer mem
points not to the original address returned by malloc
, but to address+offset (sizeof(size_t)
). However, delete
knows nothing about this offset and tries to delete the pointer with wrong address (delete []
would do the right thing).
C. double-free:
unsigned char *mem = (unsigned char*)malloc(10);
free(mem);
free(mem); # the pointer is already freed
A very common reason in C++ that rule of three/five isn't adhered to, e.g:
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. pointer from another runtime/memory manager
Windows programs have the ability to use multiple runtimes at once: every used dll could potentially have its own runtime/memory manager/heap, because it was linked statically or because they have different versions. Thus, a memory allocated in one dll, could fail when freed in another dll, which uses a different heap (see for example this SO-question or this SO-question).
2. When does is_block_type_valid(header->_block_use)
-assertion fire?
In the above cases A. and B., in addition also is_block_type_valid(header->_block_use)
will fire. After _CrtIsValidHeapPointer
-assertion, the free
-function (more precise free_dbg_nolock
) looks for info in the block-header (a special data structure used by debug-heap, more information about it later on) and checks that the block type is valid. However, because the pointer is completely bogus, the place in the memory, where nBlockUse
is expected to be, is some random value.
However, there are some scenarios, when is_block_type_valid(header->_block_use)
fires without previous _CrtIsValidHeapPointer
-assertion.
A. _CrtIsValidHeapPointer
doesn't detect invalid pointer
Here is an example:
unsigned char *mem = (unsigned char*)malloc(100);
mem+=64;
free(mem);
Because debug-heap fills the allocated memory with 0xCD
, we can be sure that accessing nBlockUse
will yield a wrong type, thus leading to the above assertion.
B. Corruption of the heap
Most of the time, when is_block_type_valid(header->_block_use)
fires without _CrtIsValidHeapPointer
it means, that the heap was corrupted due to some out-of-range writes.
So if we "delicate" (and don't overwrite "no man's land"-more on that later):
unsigned char *mem = (unsigned char*)malloc(100);
*(mem-17)=64; // thrashes _block_use.
free(mem);
leads only to is_block_type_valid(header->_block_use)
.
In all above cases, it is possible to find the underlying issue by following memory allocations, but knowing more about the structure of debug-heap helps a lot.
An overview about debug-heap can be found e.g. in documentation, alternatively all details of the implementation can be found in the corresponding Windows Kit,(e.g. C:Program Files (x86)Windows Kits10Source10.0.16299.0ucrtheapdebug_heap.cpp
).
In a nutshell: When a memory is allocated on a debug heap, more memory than needed is allocated, so additional structures such as "no man's land" and additional info, such as _block_use
, can be stored next to the "real" memory. The actual memory layout is:
------------------------------------------------------------------------
| header of the block + no man's land | "real" memory | no man's land |
----------------------------------------------------------------------
| 32 bytes + 4bytes | ? bytes | 4 bytes |
------------------------------------------------------------------------
Every byte in "no man's land" at the end and at the beginning are set to a special value (0xFD
), so once it is overwritten we can register out-of-bounds write access (as long as they are at most 4 bytes off).
For example in the case of new[]
-delete
-mismatch we can analyze memory before the pointer, to see whether this is no man's land or not (here as code, but normally done in debugger):
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)) << " ";
}
we get:
0 0 0 0 0 0 0 0 10 253 253 253 253 0 0 52
i.e. the first 8 bytes are used for the number of elements (10), than we see "no man's land" (0xFD=253
) and then other information. It is easy to see, what is going wrong - if the pointer where correct, the first 4 values where 253
.
When Debug-heap frees memory it overwrites it with a special byte value: 0xDD
, i.e. 221
. One also can restrict the reuse of once used and freed memory by setting flag _CRTDBG_DELAY_FREE_MEM_DF
, thus the memory stays marked not only directly after the free
-call, but during the whole run of the program. So when we try to free the same pointer a second time, debug-heap can see, taht the memory was already freed once and fire the assertion.
Thus, it is also easy to see, that the problem is a double-free, by analyzing the values around pointer:
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
prints
221 221 221 221 221 221 221 221 221 221 221 221 221 221 221 221
the memory, i.e. the memory was already freed once.
On the detection of heap-corruption:
The purpose of no-man's land is to detect out-of-range writes, this however works only for being off for 4 bytes in either direction, e.g.:
unsigned char *mem = (unsigned char*)malloc(100);
*(mem-1)=64; // thrashes no-man's land
free(mem);
leads to
HEAP CORRUPTION DETECTED: before Normal block (#13266) at 0x0000025C6CC21050.
CRT detected that the application wrote to memory before start of heap buffer.
A good way to find heap corruption is to use _CrtSetDbgFlag(_CRTDBG_CHECK_ALWAYS_DF)
or ASSERT(_CrtCheckMemory());
(see this SO-post). However, this is somewhat indirect - a more direct way it to use gflags
as explained in this SO-post (it is not unusual that gflags
needs about 30 times more memory and is about 10 times slower).
Btw, the definition of _CrtMemBlockHeader
changed over the time and no longer the one shown in online-help, but:
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];
};
相关文章