ead*_*ead 2 c c++ debugging visual-c++
当我在调试模式下使用 VisualStudio 编译程序运行我的程序时,有时我会得到
调试断言失败!表达:
_CrtIsValidHeapPointer(block)
或者
调试断言失败!表达:
is_block_type_valid(header->_block_use)
(或两者之后)断言。
这是什么意思?如何找到并修复此类问题的根源?
这些断言表明,要么应该释放的指针无效(或不再有效)(- _CrtIsValidHeapPointerassertion),要么在程序运行期间的某个时刻堆已损坏(早期版本中的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-assertion 什么时候触发?
有一些最常见的场景:
A. 指针不从堆开始指向内存:
char *mem = "not on the heap!";
free(mem);
Run Code Online (Sandbox Code Playgroud)
这里的文字没有存储在堆上,因此可以/不应该被释放。
B. 指针的值不是malloc/返回的原始地址calloc:
unsigned char *mem = (unsigned char*)malloc(100);
mem++;
free(mem); // mem has wrong address!
Run Code Online (Sandbox Code Playgroud)
由于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;
Run Code Online (Sandbox Code Playgroud)
当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);
free(mem);
free(mem); # the pointer is already freed
Run Code Online (Sandbox Code Playgroud)
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之后,-function free(更精确free_dbg_nolock)在块头(调试堆使用的特殊数据结构,稍后会提供更多信息)中查找信息并检查块类型是否有效。然而,由于指针完全是假的,内存中nBlockUse预期的位置是一些随机值。
但是,在某些情况下,在is_block_type_valid(header->_block_use)没有先前的_CrtIsValidHeapPointer断言的情况下触发。
A._CrtIsValidHeapPointer不检测无效指针
下面是一个例子:
unsigned char *mem = (unsigned char*)malloc(10);
free(mem);
free(mem); # the pointer is already freed
Run Code Online (Sandbox Code Playgroud)
因为 debug-heap 用 填充分配的内存0xCD,我们可以肯定访问nBlockUse会产生错误的类型,从而导致上述断言。
B. 堆的损坏
大多数情况下,当is_block_type_valid(header->_block_use)没有_CrtIsValidHeapPointer它触发时,意味着堆由于一些超出范围的写入而损坏。
因此,如果我们“精致”(并且不要覆盖“无人区”-稍后会详细介绍):
unsigned char *mem = (unsigned char*)malloc(100);
mem+=64;
free(mem);
Run Code Online (Sandbox Code Playgroud)
仅导致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 |
------------------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)
“无人区”的结尾和开头的每个字节都设置为一个特殊值 ( 0xFD),因此一旦它被覆盖,我们就可以注册越界写访问(只要它们最多关闭 4 个字节) )。
例如在的情况下new[]- delete-mismatch我们可以将指针之前分析内存条,看看这是否是无人区与否(这里的代码,但通常在调试器完成):
unsigned char *mem = (unsigned char*)malloc(100);
*(mem-17)=64; // thrashes _block_use.
free(mem);
Run Code Online (Sandbox Code Playgroud)
我们得到:
0 0 0 0 0 0 0 0 10 253 253 253 253 0 0 52
Run Code Online (Sandbox Code Playgroud)
即前 8 个字节用于元素数量 (10),然后是我们看到的“无人区”( 0xFD=253) 和其他信息。很容易看出,出了什么问题 - 如果指针正确,则前 4 个值在哪里253。
当调试堆释放内存时,它会用一个特殊的字节值覆盖它:0xDD,即221。还可以通过设置 flag 来限制曾经使用和释放的内存的重用_CRTDBG_DELAY_FREE_MEM_DF,因此内存不仅在free-call之后直接保持标记,而且在程序的整个运行过程中都保持标记。所以当我们第二次尝试释放同一个指针时,调试堆可以看到内存已经被释放一次并触发断言。
因此,通过分析指针周围的值,也很容易看出问题是双重自由的:
------------------------------------------------------------------------
| header of the block + no man's land | "real" memory | no man's land |
----------------------------------------------------------------------
| 32 bytes + 4bytes | ? bytes | 4 bytes |
------------------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)
印刷
221 221 221 221 221 221 221 221 221 221 221 221 221 221 221 221
Run Code Online (Sandbox Code Playgroud)
内存,即内存已经被释放一次。
关于检测堆损坏:
无人区的目的是检测超出范围的写入,但这仅适用于在任一方向上关闭 4 个字节,例如:
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)) << " ";
}
Run Code Online (Sandbox Code Playgroud)
造成
HEAP CORRUPTION DETECTED: before Normal block (#13266) at 0x0000025C6CC21050.
CRT detected that the application wrote to memory before start of heap buffer.
Run Code Online (Sandbox Code Playgroud)
查找堆损坏的一个好方法是使用_CrtSetDbgFlag(_CRTDBG_CHECK_ALWAYS_DF)or ASSERT(_CrtCheckMemory());(请参阅此SO-post)。然而,这在某种程度上是间接的 - 一种更直接的使用方式,gflags如this SO-post中所述(gflags需要大约 30 倍的内存并且慢大约 10 倍,这并不罕见)。
顺便说一句,_CrtMemBlockHeader随着时间的推移, 的定义发生了变化,不再是online-help 中显示的定义,而是:
0 0 0 0 0 0 0 0 10 253 253 253 253 0 0 52
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
852 次 |
| 最近记录: |