立即检测Windows上的堆损坏错误。 怎么样?

我无法入睡! 🙂

我在Windows上有一个相当大的项目,遇到了一些堆损坏问题。 我已阅读所有SO,包括这个好主题: 如何调试堆损坏错误? 但是没有什么比开箱即用更能帮助我了。 Debug CRTBoundsChecker检测到堆损坏,但地址始终不同,检测点总是远离实际的内存覆盖。 我没有睡到半夜,并制作了以下黑客:

 DWORD PageSize = 0; inline void SetPageSize() { if ( !PageSize ) { SYSTEM_INFO sysInfo; GetSystemInfo(&sysInfo); PageSize = sysInfo.dwPageSize; } } void* operator new (size_t nSize) { SetPageSize(); size_t Extra = nSize % PageSize; nSize = nSize + ( PageSize - Extra ); return Ptr = VirtualAlloc( 0, nSize, MEM_COMMIT, PAGE_READWRITE); } void operator delete (void* pPtr) { MEMORY_BASIC_INFORMATION mbi; VirtualQuery(pPtr, &mbi, sizeof(mbi)); // leave pages in reserved state, but free the physical memory VirtualFree(pPtr, 0, MEM_DECOMMIT); DWORD OldProtect; // protect the address space, so noone can access those pages VirtualProtect(pPtr, mbi.RegionSize, PAGE_NOACCESS, &OldProtect); } 

一些堆损坏错误变得明显,我能够修复它们。 退出时不再有Debug CRT警告。 但是,我有一些关于这个黑客的问题:

1.它会产生任何误报吗?

它可以错过一些堆损坏吗? (即使我们替换malloc / realloc / free?)

3.它无法使用OUT_OF_MEMORY在32位上OUT_OF_MEMORY ,仅在64位上运行。 我是对的,我们只是用完了32位的虚拟地址空间?

它会产生任何误报吗?

因此,这只会捕获类“free after()之后的错误”。 为此,我认为,这是相当不错的。

如果你试图delete一些非new的东西,那就是一种不同类型的bug。 在delete您应首先检查内存是否确实已分配。 你不应盲目地释放内存并将其标记为无法访问。 我试图避免这种情况并报告(例如,通过执行调试中断)当尝试delete不应删除的内容时,因为它从来都不是new的。

它可以错过一些堆损坏吗? (即使我们替换malloc / realloc / free?)

显然,这不会捕获new数据delete和相应delete之间的堆数据的所有损坏。 它只会捕获delete后尝试的那些。

例如:

 myObj* = new MyObj(1,2,3); // corruption of *myObj happens here and may go unnoticed delete myObj; 

它无法在具有OUT_OF_MEMORY错误的32位目标上运行,仅在64位上运行。 我是对的,我们只是用完了32位的虚拟地址空间?

通常,您可以在32位Windows上获得大约2GB的虚拟地址空间。 这在提供的代码中最多只能是524288 new的。 但是对于大于4KB的对象,您将能够成功分配更少的实例。 然后地址空间碎片将进一步减少该数量。

如果在程序的生命周期中创建许多对象实例,那么这是一个完美的预期结果。

这不会抓住:

  • 使用未初始化的内存(一旦你的指针被分配,你可以随意读取垃圾)
  • 缓冲区溢出(除非你超出PageSize边界)

理想情况下,您应该在分配的块之前和之后编写一个众所周知的位模式,以便operator delete可以检查它们是否被覆盖(指示缓冲区过度运行或运行不足)。

目前,这将在你的方案中默默地允许,并且切换回malloc等将允许它静默地破坏堆,并且稍后显示为错误(例如,在过度运行之后释放块时)。

你不能抓住所有东西:注意例如,如果底层问题是(有效)指针被垃圾覆盖某处,你就无法检测到这一点,直到损坏的指针被解除引用。

是的,您当前的答案可能会错过缓冲区 欠载溢出的堆损坏。
你的delete()函数非常好!
我以类似的方式实现了一个new()函数,它为欠载和溢出添加了防护页面。
GFlags文档中我得出结论,它只能防止超支。

注意,当简单地返回欠载保护页面旁边的指针时,在未分配的对象未被保护之后,超出的保护页面可能远离分配的对象并且紧邻
为了补偿这一点,需要返回这样一个指针,即在超限保护页面之前对象位于该指针之外(在这种情况下,再次发生欠载的可能性较小)。
对于new()的每次调用,下面的代码交替执行一个或另一个。 或者可能想要修改它以使用线程安全随机生成器来防止对调用new()的代码的任何干扰。
考虑到所有这一点,应该意识到通过以下代码检测欠载和超出仍然在某种程度上是概率性的 – 这在某些对象在整个程序期间仅被分配一次的情况下尤其相关。

NB! 因为new()返回一个修改过的aadress,所以delete()函数也必须稍微调整一下,所以它现在使用mbi.AllocationBase而不是ptr用于VirtualFree()VirtualProtect()

PS。 Driver Verifier的 特殊游泳池使用类似的技巧。

 volatile LONG priorityForUnderrun = rand(); //NB! init with rand so that the pattern is different across program runs and different checks are applied to global singleton objects void ProtectMemRegion(void* region_ptr, size_t sizeWithGuardPages) { size_t preRegionGuardPageAddress = (size_t)region_ptr; size_t postRegionGuardPageAddress = (size_t)(region_ptr) + sizeWithGuardPages - PageSize; DWORD flOldProtect1; BOOL preRegionProtectSuccess = VirtualProtect( (void*)(preRegionGuardPageAddress), pageSize, PAGE_NOACCESS, &flOldProtect1 ); DWORD flOldProtect2; BOOL postRegionProtectSuccess = VirtualProtect( (void*)(postRegionGuardPageAddress), PageSize, PAGE_NOACCESS, &flOldProtect2 ); } void* operator new (size_t size) { size_t sizeWithGuardPages = (size + PageSize - 1) / PageSize * PageSize + 2 * PageSize; void* ptr = VirtualAlloc ( NULL, sizeWithGuardPages, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE ); if (ptr == NULL) //NB! check for allocation failures { return NULL; } ProtectMemRegion(ptr, sizeWithGuardPages); void* result; if (InterlockedIncrement(&priorityForUnderrun) % 2) result = (void*)((size_t)(ptr) + pageSize); else result = (void*)(((size_t)(ptr) + sizeWithGuardPages - pageSize - size) / sizeof(size_t) * sizeof(size_t)); return result; } void operator delete (void* ptr) { MEMORY_BASIC_INFORMATION mbi; DWORD OldProtect; VirtualQuery(ptr, &mbi, sizeof(mbi)); // leave pages in reserved state, but free the physical memory VirtualFree(mbi.AllocationBase, 0, MEM_DECOMMIT); // protect the address space, so noone can access those pages VirtualProtect(mbi.AllocationBase, mbi.RegionSize, PAGE_NOACCESS, &OldProtect); }