在Windows上立即检测堆损坏错误。 如何做到?
在Windows上立即检测堆损坏错误。 如何做到?
我睡不着! :)\n我在Windows上有一个相当大的项目,遇到了一些堆破坏的问题。我已经阅读了所有的stackoverflow文章,包括这个不错的主题:如何调试堆破坏错误?,但是没有什么适合直接帮助我的。Debug CRT和BoundsChecker都检测到了堆破坏,但是地址总是不同,检测点总是远离实际的内存覆写位置。我一直没有睡到半夜,做了以下的hack:\n
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); }
\n一些堆破坏错误变得明显起来,我能够修复它们。退出时不再有Debug CRT警告。然而,我对这个hack有一些问题:\n1. 它会产生假阳性吗?\n2. 它会错过一些堆破坏吗?(即使我们替换malloc/realloc/free?)\n3. 在32位上运行时会出现OUT_OF_MEMORY错误,只有在64位上才能运行。我对于在32位上简单地运行出了虚拟地址空间是正确的吗?
Windows上如何立即检测堆错误?
要检测堆缓冲区的下溢和上溢的堆破坏错误,可以使用以下代码。该代码在每次调用new()时交替执行其中一种操作。代码使用了一个具有随机值的全局变量priorityForUnderrun,以确保在每次程序运行时模式都不同,从而对全局单例对象应用不同的检查。
代码中的ProtectMemRegion函数用于保护内存区域。它将区域的前后两个页面设置为PAGE_NOACCESS,以防止对其进行读取或写入。new()函数使用VirtualAlloc函数分配内存,并调用ProtectMemRegion函数保护分配的内存区域。然后,根据priorityForUnderrun的值,决定返回的指针是指向分配对象之后的页面还是指向分配对象之前的页面。
delete()函数根据传入的指针使用VirtualQuery函数获取内存基本信息。然后,使用VirtualFree函数释放内存,并使用VirtualProtect函数保护地址空间,以防止对已释放内存的访问。
需要注意的是,通过以上代码检测堆下溢和上溢仍然具有一定的概率性。特别是在程序的整个运行期间只分配了某些对象的情况下,这一点尤为重要。此外,需要调整delete()函数,使用mbi.AllocationBase替代ptr参数,以确保正确释放内存。
在Windows上立即检测堆破坏错误的原因是:
1. 无法捕捉未初始化内存的使用(一旦分配了指针,就可以随意读取其中的垃圾数据)。
2. 无法捕捉缓冲区溢出(除非溢出PageSize边界)。
解决方法是:
1. 在分配的块前后写入已知的位模式,这样`operator delete`可以检查它们是否被覆盖(表示缓冲区溢出或溢出)。
2. 使用`malloc`等函数时,切换回去会导致堆受损,并在稍后(例如在释放溢出块后)显示为错误。
然而,并不能捕获所有问题:例如,如果底层问题是在某个位置被覆盖为垃圾数据的(有效的)指针,直到被破坏的指针被解引用时才能检测到。
以推迟方式捕获缓冲区溢出不是问题-调试CRT可以做到。你有任何改进此方法以立即捕获缓冲区溢出的想法吗?
实际上,我认为你要么需要访问硬件监视点,要么需要一些巧妙的内存保护技巧(最好还能处理陷阱)。而Valgrind通过解释程序来拦截每次读取和写入来实现这一点。
有时我通过使用自定义分配器来解决这个问题,它在特定位置分配,使得有效负载的最后一个字节恰好是页面的最后一个字节,并且下一个页面是一个保护页面,触发访问冲突异常,这对于溢出问题非常有效。
在Windows上立即检测堆破坏错误。 如何?
堆破坏错误的出现原因:
- 如果尝试删除未使用new
关键字分配的内存,则会出现问题。
- 在使用delete
之前,应先检查内存是否确实已分配。不应该盲目释放内存并将其标记为不可访问。
解决方法:
- 对于堆数据在new
和相应的delete
之间的所有损坏,此方法将无法捕获。
- 可以通过进行调试中断并报告尝试删除不应删除的内容的情况来避免这种情况,因为它从未使用new
关键字。
- 在32位目标上可能会遇到内存不足的错误,因为虚拟地址空间不足。
- 在32位Windows上,您大约有2GB的虚拟地址空间可用。这对于最多524288个在提供的代码中使用new
的情况是足够的。但是,对于大于4KB的对象,您将能够成功分配的实例数量较少。然后,地址空间碎片化将进一步减少该数字。
- 如果在程序的生命周期中创建了许多对象实例,这是完全可以预期的结果。
感谢您的详细解释!很高兴在这里遇见您,先生!我从您的Pascal/ASM 3D引擎开始学习3D图形 🙂