【问题标题】:Detecting memory leaks during d-tor of objects在对象的 d-tor 期间检测内存泄漏
【发布时间】:2012-01-01 08:37:40
【问题描述】:

我的应用程序是基于 dll 的实现,并且可能在卸载期间泄漏内存。我在卸载/重新加载周期(未终止托管进程时)注意到它。宿主进程的虚拟内存正在增加。

我已经通过代码检查试图找到泄漏的代码,但没有找到。

我正在寻找其他技术来检测卸载期间的内存泄漏(对象正在被破坏)。

编辑:我使用的是win32(XP)平台。

您有使用此类工具/程序的经验吗? 谢谢

【问题讨论】:

  • 是否卸载 DLL 实际上会调用您的析构函数?在我看来,您的对象更有可能在卸载时被遗弃(而不是被破坏),这就是您的问题。

标签: c++ memory-leaks memory-management malloc


【解决方案1】:

我很久以前的做法是这样的:

我编写了自己的 mymallocmyreallocmyfree(并重载了 newdelete,以便它们调用我的函数。)然后我编写了调用的 mallocreallocmymallocmyrealloc,传递它们 __FILE____LINE__mymalloc 所做的是:它调用标准库的malloc 函数,分配一个稍大的块,并在该块中插入__FILE____LINE__。它还将所有分配的块保存在一个链表中,以便以后能够遍历它们。

在程序退出时,我将遍历尚未释放的块列表,并打印出导致内存泄漏的文件和行。

现在我认为会有现成的工具可以让你为你做这些事情。

【讨论】:

  • +1:是的,我使用了类似的方法,在下面粘贴了一个答案,用于在 Visual Studio 中覆盖 new/delete 并打印输出。
  • +1:这是一个好方法。太糟糕了,您为这些函数选择了错误的名称(前导下划线后跟字母名称保留在全局命名空间中)。发生这种情况是因为你不知道,还是因为对禁忌的某种幼稚的吸引力? :-)
  • @6502 我当时没有使用这些名称,我使用了一些相当长的 SentenceCase 名称。为了方便起见,我在这里只使用了这些名称。但你是对的,我什至不应该在这里,所以我会解决这个问题。修好了。
【解决方案2】:

您没有指定要查找的平台。我是 Windows 开发人员,所以我只能推荐 Windows 解决方案。如果这就是您正在使用的东西,那么可以使用许多商业和免费工具。我个人使用的很少:Purify、BoundsChecker、UMDH、LeakDiag、DebugDiag。

其中,我通常更喜欢 UMDH。它是免费的,作为Debugging Tools for Windows (DTW) 安装的一部分提供。我发现它实际上比大多数其他工具(包括专业工具)更可靠且资源消耗更少。它使用起来非常简单,文档可以在 DTW 安装附带的 .chm 文件中找到。最后,我个人发现 UMDH 与许多其他工具相比具有非常高的信噪比。

DebugDiag 是另一个不错的选择。据我所知,它使用与 UMDH 几乎相同的 API,但使用起来稍微麻烦一些,因为它是基于 UI 而不是命令提示符,因此完成任务通常需要更多点击,但对于新手来说,我会推荐它UMDH。

更新:

有趣的是,大多数人的偏好是将自定义钩子插入到 malloc/free 中,然后将更多用于自定义钩子的代码添加到操作符 new/delete 中。

我强烈建议您看看 UMDH 并了解它是如何工作的,即使您认为在这种特定情况下没有必要。所有内存分配的核心是 Windows 函数 HeapAlloc/HeapFree。微软预计需要泄漏检测方法,已经提供了我们可以在该根级别使用的钩子。

这些是使用 UMDH 优于自定义分配器挂钩的其他优势:

  • 您可以获得每个分配的完整堆栈跟踪,而不仅仅是 __FILE__ 和 __LINE__ 提供的内容
  • 它已经具有完整的报告和统计汇总,这是您必须在拦截 malloc/free 的基础上编写的内容。您可以获得每个跟踪的分配数、每个跟踪分配的字节数以及分配的内存缓冲区列表,以便您实际分析泄漏了哪种数据。
  • 检测其他人代码中发生的 malloc/free 泄漏,而不是您控制下的 DLL
  • 检测来自其他内存分配函数(例如 CoTaskMemAlloc 或 SysStringAlloc)的泄漏
  • 检测未正确释放的 COM 对象的泄漏
  • 当您调用返回您忘记释放的缓冲区的第三方 API 时检测代码中的逻辑错误。
  • 您可以立即将 UMDH 与任何代码库一起使用,而无需一遍又一遍地添加自定义代码。
  • 您接受的方法仅适用于调试环境。 UMDH 可以同样有效地使用,而无需对生产系统进行任何代码更改。

几乎,只要内存使用量呈上升趋势,该工具就会告诉您它的来源。大多数情况下,如果它在开发机器上可重现,我可以在 10 分钟内找到泄漏(调试生产代码时需要更长的时间,因为必须匹配符号文件,有时是手动匹配)。

如果您通过 DTW 安装完全免费获得所有这些(顺便说一句,它还有其他很棒的调试功能),为什么人们更喜欢推出自己的泄漏检测代码?

【讨论】:

  • 我再次听取您的建议,从 win32 位的链接下载了 SDK,但我找不到 UMDH 工具...您能帮忙吗?
  • 如果你按照我提供的链接,你会发现winsdk_web.exe。然后您可以从“Common Utilities”或“Redistributable Packages”中选择 DTW。 Redist 将安装文件放在 C:\Program Files\Microsoft SDKs\Windows\v7.1\Redist\Debugging Tools for Windows 下。另一个安装 DTW 并将其放入 C:\Program Files\Debugging Tools for Windows (x64)(或类似的,取决于 64 位与 32 位操作系统)。在该目录中,您应该找到 umdh.exe 您还将找到 debugger.chm,其中记录了如何使用该工具
  • 我已经安装并配置了我的应用程序,比较文件大小为 17MB。你有如何过滤它们的经验吗?和/或如何使用这种方法来检测 d-tor 泄漏(卸载序列)?
  • 您认为什么是泄漏? (也许我应该从那个开始)您的应用程序在运行时是否会泄漏内存,因为您有时会加载/卸载相同的 DLL?或者您只是担心应用程序退出时没有释放一些内存?退出时剩余的内存不是泄漏,因为一旦进程消失,它就会全部被回收,并且许多库/框架一直保留分配的东西。但是,如果您在应用程序运行时看到内存使用量持续增长,那么您就有了泄漏。在这种情况下,找出导致增长的动作并让该动作发生......
  • ... 10k 次(如果需要,可以使用 for 循环,或者使用 UI 自动化工具之一)。 UMDH 差异文件已经排序,因此最大的泄漏首先列出,因此通过重复执行导致泄漏的操作,您将有效地使泄漏直接出现在列表顶部。
【解决方案3】:

如果你可以在supported platform 上编译你的代码,你当然应该试试ValgrindMemcheck 工具。阅读The Valgrind Quick Start Guide 了解如何操作。

这是一个简单的例子...

来源:

$ cat -n leaky.cpp 
     1  struct leaky
     2  {
     3      leaky()
     4          :bytes(new char[256])
     5      {
     6      }
     7  
     8      char* bytes;
     9  };
    10  
    11  int main()
    12  {
    13      leaky sieve;
    14      return sizeof sieve;
    15  }
    16  

构建:

$ make leaky
g++ -Wall -Wextra -Wshadow -pedantic -Wno-long-long -Wfloat-equal -Wcast-qual -g -I/opt/local/include -Weffc++ -Wall -I /opt/local/include -L/opt/local/lib  leaky.cpp   -o leaky

检查:

$ valgrind --leak-check=full ./leaky
==85800== Memcheck, a memory error detector
==85800== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==85800== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==85800== Command: ./leaky
==85800== 
==85800== 
==85800== HEAP SUMMARY:
==85800==     in use at exit: 2,367 bytes in 33 blocks
==85800==   total heap usage: 33 allocs, 0 frees, 2,367 bytes allocated
==85800== 
==85800== 256 bytes in 1 blocks are definitely lost in loss record 6 of 9
==85800==    at 0xB823: malloc (vg_replace_malloc.c:266)
==85800==    by 0x5768D: operator new(unsigned long) (in /usr/lib/libstdc++.6.0.9.dylib)
==85800==    by 0x576DA: operator new[](unsigned long) (in /usr/lib/libstdc++.6.0.9.dylib)
==85800==    by 0x100000EE7: leaky::leaky() (leaky.cpp:4)
==85800==    by 0x100000EB3: main (leaky.cpp:13)
==85800== 
==85800== LEAK SUMMARY:
==85800==    definitely lost: 256 bytes in 1 blocks
==85800==    indirectly lost: 0 bytes in 0 blocks
==85800==      possibly lost: 0 bytes in 0 blocks
==85800==    still reachable: 2,111 bytes in 32 blocks
==85800==         suppressed: 0 bytes in 0 blocks
==85800== Reachable blocks (those to which a pointer was found) are not shown.
==85800== To see them, rerun with: --leak-check=full --show-reachable=yes
==85800== 
==85800== For counts of detected and suppressed errors, rerun with: -v
==85800== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 1 from 1)

【讨论】:

  • 他说的是dll,所以不支持
  • 不支持并不意味着它不起作用。看看葡萄酒。
  • 我使用的是win32平台(winXP和VS2005)——这个工具支持吗?
  • @NirMH:来自我的回答中的链接:“这里没有考虑 Windows,因为移植到它需要很多更改,它几乎是一个单独的项目。(但是,Valgrind + Wine 可以努力工作。)”。也就是说,如果您可以在 Linux 上编译项目的相关部分,那么您可能能够追踪到您的问题。
【解决方案4】:

除了 Mike 的回答(覆盖 Malloc)之外,您还可以覆盖 Visual Studio 中的 new 和 delete 运算符。

免责声明:我早在 2004 年就在网上找到了这段代码,并将其包含在一个 C++ 项目中。我不知道原始出处

下面是一个代码示例(我将其作为头文件 memleak.h 包含在内)。这是相当旧的代码,因此编译时可能会出错!但是,它确实说明了如何覆盖 new 和 delete。它还将未释放的内存转储到文件中。如果定义_DEBUG 包含在您的代码中,此代码将生效。

最好的问候,

#include <iostream>
#include <list>
using namespace std;

//void DumpUnfreed();
//void AddTrack(DWORD addr,  DWORD asize,  const char *fname, DWORD lnum);
//void RemoveTrack(DWORD addr);

typedef struct 
{
    DWORD   address;
    DWORD   size;
    char    file[64];
    DWORD   line;
} ALLOC_INFO;      

typedef list<ALLOC_INFO*> AllocList;   

AllocList *allocList; 

void AddTrack(DWORD addr,  DWORD asize,  const char *fname, DWORD lnum)
{
    ALLOC_INFO *info;         
    if(!allocList) 
    {
        allocList = new(AllocList);
    }         
    info = new(ALLOC_INFO);
    info->address = addr;
    strncpy(info->file, fname, 63);
    info->line = lnum;
    info->size = asize;
    allocList->insert(allocList->begin(), info);
};      

void RemoveTrack(DWORD addr)
{
    AllocList::iterator i;        
    if(!allocList)
        return;
    for(i = allocList->begin(); i != allocList->end(); i++)
    {
        if((*i)->address == addr)
        {
            allocList->remove((*i));
            break;
        }
    }
};

void DumpUnfreed()
{
    AllocList::iterator i;
    DWORD totalSize = 0;
    char buf[1024];   
    sprintf(buf, "-----------------------------------------------------------\n");
    OutputDebugString(buf);
    OutputDebugString("DSP.DLL: Detecting unfreed memory...\n");
    if(!allocList)
    {
        OutputDebugString("No memory allocations were tracked!\n");
        return;       
    }
    for(i = allocList->begin(); i != allocList->end(); i++) 
    {
        sprintf(buf, "%-50s:\t\tLINE %d,\t\tADDRESS %d\t%d unfreed\n",
            (*i)->file, (*i)->line, (*i)->address, (*i)->size);
        OutputDebugString(buf);
        totalSize += (*i)->size;
    }
    sprintf(buf, "-----------------------------------------------------------\n");
    OutputDebugString(buf);
    sprintf(buf, "DSP.DLL Total Unfreed: %d bytes\n", totalSize);
    OutputDebugString(buf);
    sprintf(buf, "-----------------------------------------------------------\n");
    OutputDebugString(buf);
};

#ifdef _DEBUG
inline void * __cdecl operator new(unsigned int size,
                                         const char *file, int line)
{
    void *ptr = (void *)malloc(size);
    AddTrack((DWORD)ptr, size, file, line);
    return(ptr);
};
inline void __cdecl operator delete(void *p)
{
    RemoveTrack((DWORD)p);
    free(p);
};
inline void * __cdecl operator new[ ] (unsigned int size, const char *file, int line)
{  
    void *ptr = (void *)malloc(size);
    AddTrack((DWORD)ptr, size, file, line);
    return(ptr);                            
};
inline void __cdecl operator delete[ ] (void *p)
{
    RemoveTrack((DWORD)p);
    free(p);
};
#endif

#ifdef _DEBUG
#define DEBUG_NEW new(__FILE__, __LINE__)
#else
#define DEBUG_NEW new
#endif
#define new DEBUG_NEW

【讨论】:

  • 他为什么要将整个文件名复制到内存块头中?这是完全没有必要的!文件名是一个常量字符串,所以他只需要存储一个指向它的指针!
  • 谢谢 - 我会试一试,看起来很有希望
猜你喜欢
  • 2012-07-16
  • 2015-12-26
  • 1970-01-01
  • 1970-01-01
  • 2011-04-25
  • 1970-01-01
  • 2012-10-01
相关资源
最近更新 更多