14

我睡不着!:)

我在 Windows 上有一个相当大的项目,遇到了一些堆损坏问题。我已经阅读了所有内容,包括这个不错的主题:如何调试堆损坏错误?,但是没有什么可以开箱即用地帮助我。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 警告。但是,我对这个 hack 有一些疑问:

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

2.它可以遗漏一些堆损坏吗?(即使我们替换 malloc/realloc/free?)

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

4

3 回答 3

8

它会产生任何误报吗?

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

如果你尝试delete一些没有被new编辑过的东西,那是一种不同类型的错误。delete您应该首先检查内存是否确实已分配。您不应该盲目地释放内存并将其标记为不可访问。我会尽量避免这种情况并报告(例如,通过调试中断)当有人尝试delete不应该删除的东西时,因为它从未被删除new

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

显然,这不会捕获new和 和 各自的堆数据的所有损坏delete。它只会捕获那些在 之后尝试过的人delete

例如:

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

它无法在 32 位目标上运行并出现 OUT_OF_MEMORY 错误,只能在 64 位上运行。我们只是用完了 32 位的虚拟地址空间,我说得对吗?

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

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

于 2012-10-04T09:54:29.590 回答
6

这不会抓住:

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

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

目前,这将在您的方案中静默允许,并且切换回mallocetc. 将允许它静默损坏堆,并在稍后显示为错误(例如,在溢出后释放块时)。

但是您无法捕获所有内容:请注意,例如,如果潜在问题是(有效)指针某处被垃圾覆盖,则在取消引用损坏的指针之前您无法检测到这一点。

于 2012-10-04T09:36:46.063 回答
3

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

请注意,当仅返回一个靠近欠载保护页面的指针时,溢出保护页面很可能位于远离分配对象的位置,并且在分配的对象未受保护之后紧邻。 为了弥补这一点,需要返回这样一个指针,即对象位于溢出保护页面之前(在这种情况下,再次不太可能检测到欠载)。 下面的代码为每次调用new()交替执行一个或另一个。或者有人可能想要修改它以使用线程安全随机生成器来防止对调用new()的代码产生任何干扰。


考虑到这一切,人们应该意识到,通过以下代码检测不足和溢出在一定程度上仍然是概率性的——这在某些对象在整个程序期间仅分配一次的情况下尤其重要。

注意!因为new()返回一个修改后的地址,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);
}
于 2014-08-21T18:28:11.720 回答