65.9K
CodeProject 正在变化。 阅读更多。
Home

谁删除了我的指针?

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (28投票s)

2003年4月23日

4分钟阅读

viewsIcon

71790

downloadIcon

505

使用重载的 new 和 delete 操作符定位悬空指针

引言

前几天我遇到了这个问题。我当时正在修复一个应用程序,它实现为一个极其混乱的指针集合。该应用程序正在处理一个链表,当它在某个节点内指向的错误数据上崩溃时,程序就停止运行了。我们应该指向的对象类型只是系统中一半对象的虚拟基类。我首先问的问题是,那里曾经存在过对象吗?它崩溃的指针可以被4整除且不为NULL,所以它曾经可能是一个有效的指针。使用 Visual Studio 的内存查看器(视图->调试窗口->内存)进行调查显示,该指针指向的数据充满了FE EE FE EE FE EE……这通常表示已分配但现在未使用的内存。某些地方释放了我的数据。我需要一种方法来弄清楚我的数据发生了什么。

背景

我最终通过重载`new`和`delete`运算符找到了丢失的数据。当调用一个函数时,在参数压入堆栈后,返回地址也会被压入堆栈。然后,我们可以在`new`和`delete`运算符中从堆栈中提取它以帮助调试。

Using the Code

在对我的指针去了哪里做出几次错误的猜测后,我采取了重载`new`和`delete`运算符的方法,如下所示。此`new`运算符的实现从堆栈中提取返回地址。返回地址位于函数参数的地址和第一个自动变量的地址之间。编译器设置、调用约定和机器架构可能会影响返回地址的查找位置,因此您可能需要根据您的环境略微调整此设置。一旦获得其返回地址,`new`就会额外分配十六个字节,并将返回地址和缓冲区的预期大小存储在缓冲区的前面,并返回指向缓冲区中第十六个字节的指针。

`delete`运算符,正如您所看到的,不再删除。相反,它以相同的方式提取返回地址,将其粘贴到大小后面的缓冲区前面,将`DE AD BE EF`写入最后四个字节,然后用重复模式填充缓冲区的其余部分。

现在,当应用程序在调试器中因错误指针而崩溃时,我只需打开内存窗口,找到我的指针指向的位置,然后返回16个字节。前四个字节是`new`调用的位置。接下来的四个字节是已分配的大小。第三组四个字节是`delete`调用的位置。最后一组四个字节应该是`DE AD BE EF`。后面跟着用`77 77 77 77`填充的其余已分配缓冲区。

要将`new`和`delete`的这些返回地址映射回源代码中的点,首先反转它们的字节顺序。这是因为 Intel 的小端序。接下来,右键单击源代码并选择“转到反汇编”。最左边的列包含每个机器指令的内存地址。按 Ctrl-G 或选择(编辑->转到…)并输入您的一个提取地址。然后它应该将您滚动到对`new`或`delete`的调用。要返回到源文件,再次右键单击,然后选择“转到源”。然后您应该看到对`new`或`delete`的调用。

现在您可以快速找出丢失的数据去了哪里。至于弄清楚为什么在您仍然需要数据时对您的数据调用了`delete`,嗯,这取决于你自己了。

#include <MALLOC.H>

void * ::operator new(size_t size)
{
    int stackVar;
    unsigned long stackVarAddr = (unsigned long)&stackVar;
    unsigned long argAddr = (unsigned long)&size;

    void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2);

    void * retAddr = * retAddrAddr;

    unsigned char *retBuffer = (unsigned char*)malloc(size + 16);

    memset(retBuffer, 0, 16);

    memcpy(retBuffer, &retAddr, sizeof(retAddr));

    memcpy(retBuffer + 4, &size, sizeof(size));

    return retBuffer + 16;
}

void ::operator delete(void *buf)
{
    int stackVar;
    if(!buf)
        return;

    unsigned long stackVarAddr = (unsigned long)&stackVar;
    unsigned long argAddr = (unsigned long)&buf;

    void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2);

    void * retAddr = * retAddrAddr;

    unsigned char* buf2 = (unsigned char*)buf;

    buf2 -= 8;

    memcpy(buf2, &retAddr, sizeof(retAddr));

    size_t size;

    buf2 -= 4;

    memcpy(&size, buf2, sizeof(buf2));

    buf2 += 8;

    buf2[0] = 0xde;
    buf2[1] = 0xad;
    buf2[2] = 0xbe;
    buf2[3] = 0xef;

    buf2 += 4;

    memset(buf2, 0x7777, size);

//  deallocating destroys saved addresses, so don't
//    buf -= 16;
//    free(buf2);
}

关注点

此代码也可用于检测内存泄漏。只需修复`delete`运算符使其实际释放内存。然后,在应用程序退出之前,使用`_heapwalk`遍历已分配的缓冲区并提取`new`调用的地址。这将为您提供一个未与`delete`调用匹配的`new`调用的列表。

此外,此代码用于调试,如果您将此代码放入生产应用程序中,它将很快耗尽内存。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

谁删除了我的指针?- CodeProject - 代码之家
© . All rights reserved.