谁删除了我的指针?






4.93/5 (28投票s)
2003年4月23日
4分钟阅读

71790

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`调用的列表。
此外,此代码用于调试,如果您将此代码放入生产应用程序中,它将很快耗尽内存。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。