我在Windows上有一个相当大的项目,遇到了一些堆损坏问题。我已经阅读了所有SO,包括这个很好的主题:How to debug heap corruption errors?,但没有什么比开箱即用更能帮助我了。 Debug CRT
和BoundsChecker
检测到堆损坏,但地址始终不同,检测点总是远离实际的内存覆盖。我没有睡到半夜,并制作了以下黑客:
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。它可以产生任何误报吗?
2。它可以错过一些堆损坏吗? (即使我们替换malloc / realloc / free?)
3. 它无法在OUT_OF_MEMORY
的32位上运行,仅在64位上运行。我是对的,我们只是用完了32位的虚拟地址空间?
答案 0 :(得分:8)
它会产生任何误报吗?
因此,这只会捕获类“free after()之后的错误”。为此,我认为,这是相当不错的。
如果你尝试delete
某些不是new
的东西,那就是另一种类型的错误。在delete
中,您应首先检查内存是否确实已分配。你不应该盲目地释放内存并将其标记为无法访问。我试图避免这种情况并报告(例如,通过执行调试中断),当尝试delete
不应删除的内容时,因为它从未new
编辑。
可以错过一些堆损坏吗? (即使我们替换malloc / realloc / free?)
显然,这不会捕获new
和相应delete
之间的堆数据的所有损坏。它只会捕获delete
之后尝试的那些。
E.g:
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的对象,您将能够成功分配更少的实例。然后地址空间碎片将进一步减少这个数字。
如果您在程序的生命周期中创建了许多对象实例,那么这是一个完美的预期结果。
答案 1 :(得分:6)
这不会抓住:
理想情况下,您应该在分配的块之前和之后编写一个众所周知的位模式,以便operator delete
可以检查它们是否被覆盖(指示缓冲区过度运行或运行不足)。
目前,这将在您的方案中以静默方式允许,并且切换回malloc
等将允许静默损坏堆,并在以后显示为错误(例如。在过度运行之后释放块时)。
你无法捕捉到所有内容:请注意,例如,如果底层问题是(有效)指针被垃圾覆盖某处,则在损坏的指针被取消引用之前,您无法检测到这一点。
答案 2 :(得分:3)
是的,您当前的答案可能会错过缓冲 在和超支下的堆损坏。
你的 delete()功能非常好!
我以类似的方式实现了 new()功能,为欠载和超出添加了防护页面。
从 GFlags 文档中我得出结论,它只能防止超支。
请注意,当简单地返回欠载保护页面旁边的指针时,在分配的对象 NOT之后,超出保护页面可能远离分配的对象和紧邻附近把守即可。
为了补偿这一点,需要返回这样一个指针,即在超限保护页面之前对象位于该对象之下(在这种情况下,再次发生不足的可能性不足)。
对于 new()的每次调用,下面的代码交替执行一个或另一个。或者可能想要修改它以使用线程安全随机生成器来防止对调用 new()的代码的任何干扰。
考虑到所有这一点,应该意识到通过以下代码检测欠载和超出在某种程度上仍然是概率性的 - 这在一些对象在整个程序期间仅被分配一次的情况下尤其相关。
NB!因为 new()会返回修改过的aadress,所以 delete()函数也需要稍微调整一下,所以它现在使用 mbi.AllocationBase 代替 ptr ,用于 VirtualFree()和 VirtualProtect()。
PS。 Driver Verifier的 Special Pool使用类似的技巧。
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);
}