![](/img/trans.png)
[英]WHY I solved “Debug Assertion Failed OpenCv 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.