【问题标题】:Immediate detection of heap corruption errors on Windows. How?立即检测 Windows 上的堆损坏错误。如何?
【发布时间】:2012-09-25 07:11:39
【问题描述】:

我睡不着! :)

我在 Windows 上有一个相当大的项目,但遇到了一些堆损坏问题。我已经阅读了所有 SO,包括这个不错的主题:How to debug heap corruption errors?,但是没有什么适合开箱即用的帮助我。 Debug CRTBoundsChecker 检测到堆损坏,但地址总是不同的,检测点总是远离实际的内存覆盖。我一直到半夜都没睡,并制作了以下 hack:

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. 使用OUT_OF_MEMORY 无法在 32 位上运行,只能在 64 位上运行。我说得对吗,我们只是用完了 32 位的虚拟地址空间?

【问题讨论】:

  • 您是否想重新发明PageHeap GFLAG?见Detecting Heap Corruption Using GFlags and Dumps
  • @Remus Rusanu:链接!我很想在 24 小时前拥有它 :)
  • 下次你就知道了:)
  • 虽然它会捕获重用后释放错误,但请注意分配任何类型的对象(例如,4 字节大小)将从您的地址空间中占用 64kiB,因为这是最小分配Windows 将执行的粒度。这也是它在 OUT_OF_MEMORY 低于 32 位时失败的原因——这是最糟糕的事情。

标签: c++ c heap-memory heap-corruption virtualalloc


【解决方案1】:

它会产生任何误报吗?

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

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

它可以遗漏一些堆损坏吗? (即使我们替换 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 的虚拟地址空间。在提供的代码中,这对于最多 ~524288 new 是有好处的。但是对于大于 4KB 的对象,您将能够成功分配比这更少的实例。然后地址空间碎片将进一步减少这个数字。

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

【讨论】:

  • 感谢您的详细说明!很高兴在这里见到你,先生!我从你的 pascal/asm 3D 引擎开始学习 3D 图形:)
【解决方案2】:

这不会捕获:

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

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

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

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

【讨论】:

  • 以延迟的方式捕获缓冲区溢出是没有问题的 - 调试 CRT 可以做到。您有任何想法如何改进此方法以立即捕获缓冲区溢出?
  • 真的,我认为您要么需要访问硬件观察点,要么需要一些巧妙的内存保护技巧(最好是处理陷阱的能力)。另一方面,Valgrind 本质上是通过解释程序来实现的,因此它可以拦截每一次读写。
  • "buffer overruns" - 偶尔我会通过使用自定义分配器来解决这个问题,该分配器分配特定的有效负载的最后一个字节正是页面上的最后一个字节,下一页是一个守卫页面遇到访问冲突异常,这对于溢出非常有效 - Hardware assisted memory corruption detection
【解决方案3】:

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

请注意,当仅返回一个与欠载保护页面相邻的指针时,溢出保护页面可能位于远离分配对象的位置,并且在分配对象NOT之后紧邻保护
为了弥补这一点,需要返回这样一个指针,即该对象位于溢出保护页面之前(在这种情况下,再次不太可能检测到欠载)。
下面的代码对 new() 的每次调用交替执行一个或另一个。或者有人可能想修改它以使用线程安全随机生成器,以防止对调用 new() 的代码产生任何干扰。
考虑到这一切,应该意识到,通过下面的代码检测不足和溢出在一定程度上仍然是概率性的——这在某些对象在整个程序期间只分配一次的情况下尤其重要。

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

PS。 Driver Verifier's 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);
}

【讨论】:

    猜你喜欢
    • 2012-10-20
    • 2015-08-05
    • 2011-08-20
    • 1970-01-01
    • 2011-07-24
    • 1970-01-01
    • 2018-03-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多