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

内存(泄漏) 和异常跟踪(CRT 和 COM 泄漏)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (151投票s)

2002年11月4日

LGPL3

15分钟阅读

viewsIcon

2139243

downloadIcon

21553

通过此实用工具,您可以轻松地在程序中查找内存泄漏(CRT 和 COM),而且几乎不会增加运行时成本。每个泄漏都会与分配时的调用堆栈一起写入文件。

引言

通过此实用工具,您可以轻松地在程序中查找内存泄漏(CRT 和 COM 泄漏!)。每个泄漏都会显示分配时的调用堆栈(包括源代码行)。这样,您就可以轻松地找到泄漏,同时使用 STL。当应用程序崩溃时,它还会写入一个包含调用堆栈的文件(它还可以处理堆栈溢出!)。它几乎没有运行时开销(运行时成本)。而且最棒的是:它是免费的(GNU Lesser General Public License)。

查找内存泄漏

可以轻松地将其集成到您现有的 VC 代码中

  1. Stackwalker.cppStackwalker.h 添加到您的项目中。
  2. 在您的 main 源文件中包含 Stackwalker.h
  3. main 开始后立即调用 InitAllocCheck()
  4. main 结束前不久调用 DeInitAllocCheck()(此时将报告所有泄漏)。

所有泄漏都将列在应用程序目录下的 YouAppName.exe.mem.log 文件中(仅限调试版本;在发布版本中已禁用)。这还将默认激活异常处理(发布和调试版本)。

仅使用异常处理

如果您只想使用异常处理,则需要执行以下操作

  1. Stackwalker.cppStackwalker.h 添加到您的项目中。
  2. 在您的 main 源文件中包含 Stackwalker.h
  3. main 开始后立即调用 OnlyInstallUnhandeldExceptionFilter()

如果发生异常,它将在应用程序目录中写入一个包含调用堆栈的文件,文件名为 YouAppName.exe.exp.log

示例

下面提供了一个简单的示例

#include <windows.h>

#include "Stackwalker.h"


void main()
{
  // Uncomment the following if you only

  // need the UnhandledException-Filter

  // (to log unhandled exceptions)

  // then you can remove the "(De)InitAllocCheck" lines

  //OnlyInstallUnhandeldExceptionFilter();


  InitAllocCheck();

  // This shows how the mem-leak function works

  char *pTest1 = new char[100];

  // This shows a COM-Leak

  CoTaskMemAlloc(120);

  // This shows the exception handling

  // and log-file writing for an exception:

  // If you want to try it, please comment it out...

  //char *p = NULL;

  //*p = 'A'; // BANG!


  DeInitAllocCheck();
}

如果您执行此示例,您将获得一个名为 Appication-Name.exe.mem.log 的文件,内容如下

##### Memory Report ########################################
11/07/02 09:43:56

##### Leaks: ###############################################
RequestID:           42, Removed: 0, Size:          100
1: 11/07/02 09:43:56
1: f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c(359)
                              +30 bytes (_heap_alloc_dbg)
1: f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c(260)
                              +21 bytes (_nh_malloc_dbg)
1: f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c(139) +21 bytes (malloc)
1: f:\vs70builds\9466\vc\crtbld\crt\src\newop.cpp(12) +9 bytes (operator new)
1: d:\privat\memory_and_exception_trace\
            memory_and_exception_trace\main.cpp(9) +7 bytes (main)
1: f:\vs70builds\9466\vc\crtbld\crt\src\crt0.c(259)
                               +25 bytes (mainCRTStartup)

**** Number of leaks: 1

##### COM-Leaks: ###############################################
(shortened)
**** Number of leaks: 1

解释

现在,我将解释内存报告文件

RequestID:           42, Removed: 0, Size:          100

这一行是一个泄漏的开始。如果您有多个泄漏,则每个泄漏都将以 RequestID 开头。

  • RequestID

    对于 CRT:这是传递给 AllocHookRequestID。此 ID 明确标识了一个分配。CRT 仅为每次分配递增此数字。您也可以使用此数字与 _CrtSetBreakAlloc 函数一起使用。

    对于 COM:这是已分配内存的地址。

  • Removed

    在内存泄漏转储中,这必须始终为 0 (false)。

  • 大小

    这是已分配内存块的大小。

1: f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c(359)
                                   +30 bytes (_heap_alloc_dbg)

这是一个实际的堆栈条目。堆栈从顶部的最后一个函数开始,依次通过每个被调用者,直到达到堆栈的末尾。

  • 1:

    此数字会为每个完整的调用堆栈递增。您可以忽略此项。

  • f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c

    实际文件名。

  • (359)

    文件中的行号。

  • +30 字节

    这是相对于此行的字节偏移量(如果一行产生多个汇编指令)。

  • (_heap_alloc_dbg)

    函数名称。

    通过调用 InitAllocCheck 提供更多选项

    InitAllocCheck 有三个参数

    参数名称 描述
    • eAllocCheckOutput
    • eOutput

    这是一个用于输出格式的 enum。以下是可能的选项

    • ACOutput_Simple (默认)

      这会按上述方式输出调用堆栈。

    • ACOutput_Advanced

      这会提供更详细的调用堆栈输出。有关更多信息,请参阅此处

    • ACOutput_XML

      这会将泄漏输出到 XML 文件,以便您从其他应用程序轻松读取,或使用 XSLT 将其转换为您想要的更易读的格式。有关更多信息,请参阅此处

    • BOOL
    • bSetUnhandledExeptionFilter (默认:TRUE)

    如果设置了此项,将安装一个 UnhandledExceptionFilter。如果发生(未处理的)异常,它将把调用堆栈写入日志文件并终止。

    • ULONG
    • ulShowStackAtAlloc (默认:0)

    注意:这仅适用于 CRT allocs。

    在此处,您可以指定 malloc/free 日志记录的级别。默认情况下,在运行时不会在日志文件中记录任何内容。如果您需要了解程序执行过程中发生的情况,可以指定一个值。然后,malloc/free 操作将被记录到文件中(带或不带调用堆栈)。

    有效值包括

    • 0 = 运行时 alloc 调用期间不写入任何输出(默认)。
    • 1 = 仅写入 alloc 操作(mallocreallocfree)。
    • 2 = 写入 alloc 操作和调用堆栈,仅用于 malloc/realloc
    • 3 = 写入 alloc 操作和调用堆栈,用于所有操作。

    包含更多信息的日志输出

    您还可以获得包含每条堆栈条目更多信息的输出。为此,您必须使用设置为 ACOutput_Advanced 的第一个参数调用 InitAllocCheck。如果您执行以下示例,您将获得一个名为 Appication-Name.exe.mem.log 的文件,其中包含更多信息

    #include <windows.h>
    
    #include "Stackwalker.h"
    
    
    void main()
    {
      InitAllocCheck(ACOutput_Advanced);
      // This shows how the mem-leak function works
    
      char *pTest1 = new char[100];
      DeInitAllocCheck();
    }

    这是(缩短的)输出

    ##### Memory Report ########################################
    11/04/02 09:04:04
    
    ##### Leaks: ###############################################
    RequestID:           45, Removed: 0, Size:          100
    1: 11/04/02 09:04:04
    // ...
    1:   5     main +49 bytes
    1:     Decl: main
    1:     Line: d:\privat\memory_and_exception_trace\main.cpp(27) +7 bytes
    1:     Mod:  Memory_and_Exception_Trace, base: 00400000h
    
    1:   6     mainCRTStartup +363 bytes
    1:     Decl: mainCRTStartup
    1:     Line: f:\vs70builds\9466\vc\crtbld\crt\src\crt0.c(259) +25 bytes
    1:     Mod:  Memory_and_Exception_Trace, base: 00400000h
    
    1:   7     _BaseProcessStart@4 +35 bytes
    1:     Decl: _BaseProcessStart@4
    1:     Mod:  kernel32, base: 77e40000h
    
    **** Number of leaks: 1
    // ...

    解释

    在这里,我将解释内存报告文件

    RequestID:           45, Removed: 0, Size:          100

    这一行与上面相同

    1:   5     main +49 bytes
    • 1:

      此数字会为每个完整的调用堆栈递增。您可以忽略此项。

    • 5

      这是调用堆栈的深度。此数字为每个堆栈条目递增。堆栈从顶部的最后一个函数(数字 0)开始,依次通过每个被调用者,直到达到堆栈的末尾。

    • main +49 字节

      存储此调用堆栈的指令所在的函数起始位置的字节数。

    1:     Decl: main
    1:     Line: d:\privat\memory_and_exception_trace\main.cpp(27) +7 bytes
    1:     Mod:  Memory_and_Exception_Trace, base: 00400000h
    • 声明:main

      这是函数的声明。

    • 行:....xyz.cpp(27) +7 字节

      这显示了调用堆栈的实际行(在括号中)(此处为:第 27 行)。此外,它还提供了此行的字节偏移量(如果一行产生多个汇编指令)。

    • 模块:Memory_and_Exception_Trace

      模块名称(EXE、DLL、OCX 等)。

    • 基址:00400000h

      此模块的基址。

    XML 输出

    如果将第一个参数设置为 ACOutput_XML,将生成一个 XML 文件。其内容如下

    <MEMREPORT date="11/08/02" time="10:43:47">
      <LEAK requestID="47" size="100">
        <!-- shortened -->
        <STACKENTRY decl="main" decl_offset="+100"
                        srcfile="d:\...\main.cpp" line="16"
          line_offset="+7" module="Memory_and_Exception_Trace" base="00400000"/>
        <STACKENTRY decl="mainCRTStartup" decl_offset="+363"
                        srcfile="f:\...\crt0.c" line="259"
          line_offset="+25" module="Memory_and_Exception_Trace" base="00400000"/>
      </LEAK>
    </MEMREPORT>

    如果您查看 “高级日志输出”,它会很直观。

    内存泄漏分析工具

    如果您使用 XML 输出格式,则可以使用我的 MemLeakTool 以排序顺序(按调用堆栈排序)显示泄漏。只需选择“xml-leak”文件并按“读取”。调用堆栈将在 TreeView 中显示。如果选择一个节点,源代码将显示在右侧部分(如果能找到)。

    信息:此程序需要 .NET Framework 1.0!

    关于泄漏

    您应该意识到,某些泄漏可能是其他泄漏的结果。例如,以下代码会产生两次泄漏,但如果您删除泄漏的“起源”,其他泄漏也会消失。例如

    #include <windows.h>
    
    #include <stdlib.h>
    
    #include "stackwalker.h"
    
    class MyTest
    {
      public:
        MyTest(const char *szName)
        {
          // The following is the second resulting leak
    
          m_pszName = strdup(szName);
        }
        ~MyTest()
        {
          if (m_pszName != NULL)
            free(m_pszName);
          m_pszName = NULL;
        }
      protected:
        char *m_pszName;
    };
    
    void main()
    {
      InitAllocCheck();
    
      // This is the "main" leak
    
      MyTest *pTest = new MyTest("This is an example");
    
      DeInitAllocCheck();
    }

    工作原理(CRT)

    内存泄漏日志记录器的基础是一个哈希表,其中包含有关所有已分配内存(包括调用堆栈)的信息。基本上,调用 _CrtSetAllocHook 来挂钩所有内存分配/释放。因此,仅记录 C/C++ 分配。每次分配时,都会捕获一部分调用堆栈和指令指针,并与其他分配信息一起存储在哈希表中。

    当应用程序调用 DeinitAllocCheck 时,将遍历哈希表,并将所有条目的(已保存的)调用堆栈列在文件中。为此,我们将指向我们 ProcessMemoryRoutine 函数的指针提供给 StackWalk 函数。

    详细信息

    哈希表

    哈希表默认包含 1024 个条目。如果您进行了大量分配并希望减少冲突,可以更改此值。只需更改 ALLOC_HASH_ENTRIES 定义。

    作为哈希键,使用每个分配的 lRequestID。此 ID 至少对于 allocs 会传递给 AllocHook 函数。如果未传递(例如,用于释放),则会传递一个(有效)地址。通过拥有此地址,还可以通过查看已分配块的 _CrtMemBlockHeader 来获取 lRequestID

    对于哈希,使用了一个非常简单且快速的哈希函数

    static inline ULONG AllocHashFunction(long lRequestID) {
      return lRequestID % ALLOC_HASH_ENTRIES;
    }  // AllocHashFunction

    将分配插入哈希表

    当需要将新分配插入哈希表时,首先通过调用 GetThreadContext 来创建实际线程的线程上下文。此函数需要一个“真实”线程句柄,而不是 GetCurrentThred 返回的伪句柄。因此,为此我必须通过调用 DuplicateHandle 来创建一个“真实”句柄。

    实际上,我只需要当前的 EbpEip 寄存器。这也可以通过仅使用内联汇编器读取寄存器来完成。现在有了寄存器,我读取指定地址处的内存。对于 Eip,我只需要读取 4 个字节。我不知道 StackWalk 为什么需要读取 Eip 值,但如果 StackWalk 无法读取这些值,它将无法构建调用堆栈。真正重要的部分是存储在从 Ebp(或 Esp)指向的内存中的调用堆栈。

    目前,我只尝试通过调用 ReadProcessMemory 函数读取 0x500 字节。我不读取完整的堆栈,因为对于许多分配来说,它可能会使用过多的内存。因此,我将最大大小减少到 0x500。如果您需要更深的调用堆栈,可以更改 MAX_ESP_LEN_BUF 定义。

    如果调用堆栈未达到 0x500 字节的深度,则 ReadProcessMemory 将因 ERROR_PARTIAL_COPY 而失败。如果发生这种情况,我需要询问可以无错误读取多少字节。为此,我需要通过调用 VirtualQuery 来查询此值。然后,我尝试读取尽可能多的字节。

    有了调用堆栈,我就可以轻松地将条目插入哈希表。如果给定的哈希条目已被占用,我将创建一个链表并将此条目追加到末尾。

    构建泄漏列表

    当您调用 DeInitAllocCheck 时,我将遍历哈希表并输出所有未释放的条目。为此,我将调用 StackWalk,并将一个指向我自己的内存读取函数(ReadProcMemoryFromHash)的指针传递给它。此函数由 StackWalk 的内部调用。当被调用时,它将在哈希表中查找给定的 lRequestID,并返回存储在哈希表中的内存。lRequestIDStackWalk 函数的 hProcess 参数中传递(如 StackWalk 的文档中所述)。

    忽略分配

    会忽略 _CRT_BLOCK 的分配/释放(有关更多信息,请参阅此处)。这是因为 CRT 会动态分配一些内存用于“特殊目的”。该工具还会检查 _crtDbgFlag 变量的 _CRTDBG_ALLOC_MEM_DF 标志。如果该标志关闭,则所有分配都将被忽略。有关更多详细信息,请参阅_CrtSetDbgFlag

    工作原理(COM)

    要跟踪 COM 内存泄漏,您必须提供一个 IMallocSpy 接口。此接口必须与 CoRegisterMallocSpy 一起注册。之后,将为每次内存(重新)分配/释放调用(自己的)IMallocSpy 实例。这样,您就可以跟踪所有内存操作。

    调用堆栈的存储方式与 CRT allocs 相同(在哈希表中)。因此,有关更多信息,请阅读 CRT 部分

    关于 COM 泄漏

    实际上,没有什么好说的,但是...

    如果您使用 MSXML 3 或 4 实现,您必须意识到该解析器使用一个“智能”的伪垃圾回收器。这意味着它们会分配内存,并且在使用后不会释放它!因此,您可能会看到一些仅是“缓存内存”的泄漏。如果您先调用 CoUninitialize,则所有缓存的内存将被释放,并报告“真实”的 COM 泄漏。

    有关更多信息,请参阅:理解 MSXML 垃圾回收机制

    MFC 用法

    MFC 的问题在于派生的 CWinApp 类由 C 运行时实例化,因为它是一个全局变量。实现泄漏查找器的最简单方法是在您的 MainApp.cpp 中声明以下 static struct

    您还必须将 stackwalk.cppstackwalk.h 添加到您的项目中。如果使用预编译头文件,您还需要在 stackwalk.cpp 文件的顶部添加 #include <stdafx.h>。本文档顶部也提供了 MFC 应用程序的示例

    static struct _test
    {
      _test()
      {
        InitAllocCheck();
      }
    
      ~_test()
      {
        DeInitAllocCheck();
      }
    } _myLeakFinder;

    暂时禁用日志记录(仅 CRT)

    当您不想记录应用程序的特定分配时(无论出于何种原因;MFC 经常这样做),您可以通过使用 _CrtSetDbgFlag 函数禁用 CRT 标志 _CRTDBG_ALLOC_MEM_DF 来简单地禁用它。下面是一个如何执行此操作的示例

    #include "Stackwalker.h"
    
    
    #include <crtdbg.h>
    
    
    bool EnableMemoryTracking(bool bTrack)
    {
      int nOldState = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
      if (bTrack)
        _CrtSetDbgFlag(nOldState | _CRTDBG_ALLOC_MEM_DF);
      else
        _CrtSetDbgFlag(nOldState & ~_CRTDBG_ALLOC_MEM_DF);
      return nOldState & _CRTDBG_ALLOC_MEM_DF;
    }
    
    void main()
    {
      InitAllocCheck();
    
      // The following will be logged
    
      char *pTest1 = new char[100];
    
      EnableMemoryTracking(false);  // disable logging
    
      // The following will NOT be logged
    
      char *pTest2 = new char[200];
      EnableMemoryTracking(true);  // enable logging
    
    
      // The following will be logged
    
      char *pTest3 = new char[300];
    
      DeInitAllocCheck();
    }

    未处理的异常

    有三种方法可以使用此工具处理未处理的异常。

    简单使用

    如果您仅使用无参数调用 InitAllocCheck 或将第二个参数设置为 TRUE,则将安装一个未处理异常过滤器。如果发生未处理异常,将写入一个包含调用堆栈的日志文件,将显示一个包含异常消息的对话框,并且应用程序将以 FatalAppExit 终止。

    第二次简单使用

    如果您不想要 AllocCheck 的开销((小的)开销仅在调试版本中存在),您可以直接调用 OnlyInstallUnhandeldExceptionFilter。这将安装 UnhandledExceptionFilter,该过滤器会在发生(未处理的)异常时写入日志文件。日志文件将存储在应用程序目录中,文件名为 YouAppName.exe.exp.log

    int main()
    {
      OnlyInstallUnhandeldExceptionFilter();
    
      // do your main code here...
    
    }

    高级使用

    您可以编写自己的异常过滤器,然后只需调用 StackwalkFilter 来生成调用堆栈。然后您可以随心所欲。这是一个小示例

    static LONG __stdcall MyUnhandlerExceptionFilter(EXCEPTION_POINTERS* pExPtrs)
    {
       LONG lRet;
       lRet = StackwalkFilter(pExPtrs,
                 EXCEPTION_EXECUTE_HANDLER, _T("\\exception.log"));
       TCHAR lString[500];
       _stprintf(lString,
          _T("*** Unhandled Exception!\n")
          _T("   ExpCode: 0x%8.8X\n")
          _T("   ExpFlags: %d\n")
          _T("   ExpAddress: 0x%8.8X\n")
          _T("   Please report!"),
          pExPtrs->ExceptionRecord->ExceptionCode,
          pExPtrs->ExceptionRecord->ExceptionFlags,
          pExPtrs->ExceptionRecord->ExceptionAddress);
       FatalAppExit(-1,lString);
       return lRet;
    }
    
    int main()
    {
      InitAllocCheck(ACOutput_Advanced, FALSE);
      SetUnhandledExceptionFilter(MyUnhandlerExceptionFilter);
    
      // do some stuff...
    
    
      DeInitAlloocCheck();
    }

    常见错误

    使用此工具时最常见的错误之一是在 main 函数中静态实例化类。问题在于类的析构函数在调用 DeInitAllocCheck 之后才被调用。如果在该类中分配了内存,则这些内存将显示为泄漏。例如

    #include <windows.h>
    
    #include "Stackwalker.h"
    
    #include <string>
    
    
    void main()
    {
      InitAllocCheck();
      std::string szTemp;
      szTemp = "This is a really long string";
      DeInitAllocCheck();
    }

    对此有两种解决方案。您可以在调用 InitAllocCheck 后开始一个块,并在调用 DeInitAllocCheck 之前结束它。这样,您可以确保在生成泄漏文件之前调用了析构函数。例如

    #include <windows.h>
    
    #include "Stackwalker.h"
    
    #include <string>
    
    
    void main()
    {
      InitAllocCheck();
      {
        std::string szTemp;
        szTemp = "This is a really long string";
      }
      DeInitAllocCheck();
    }

    第二种解决方案是使用与 MFC 应用程序相同的方法(参见上文)。

    Visual Studio 7 和 Win2K / NT

    我发现了一个使用 VS7 构建并在 Win2K 或 NT 上运行的可执行文件的问题。问题是由于一个旧版本的 dbghelp.dll。从 VS7 生成的 PDB 文件是较新格式(DIA)。看来 VS 安装程序不会在 Win2K 上更新 dbghelp.dll。因此,系统中仍然存在原始版本(5.0.*)并被使用。但是使用此版本无法读取新的 PDB 格式。因此,无法显示调用堆栈。

    要使其正常工作,您需要执行以下操作

    下载最新的 Windows 调试工具(其中包含 dbghelp.dll)。您需要安装它才能获取文件。但您只需要 dbghelp.dll!现在我们还有另一个问题。安装程序不会替换原始的 dbghelp.dll。因此,我们需要将 dbghelp.dll 复制到我们的 EXE 目录。现在,为了确保加载的是正确的版本,您需要在 EXE 目录中放置一个名为 appname.local 的文件(请将 appname 替换为 EXE 名称(不带扩展名))。现在它应该在 WinNT/2K 上也能正常工作。

    已知问题

    • 内存泄漏仅在 lRequestID 不会回绕(32 位值)时才能正确工作。如果值回绕,则无法清楚地将给定的 lRequestID 分配给先前的分配,因为该 ID 可能被使用了两次(甚至更多)。但这只发生在 VC7 中,因为 VC6 在 C 运行时有一个错误,如果 lRequestID 回绕(如果未使用 _CrtBreakAlloc),它会调用 _DbgBreak
    • 如果在 VC7 中使用“检测 64 位可移植性问题 (/Wp64)”选项进行编译,它将生成一个警告。
    • 如果您在托管 C++ 中使用此工具,它将无法正确显示托管代码的调用堆栈。
    • 出于某种原因,COM alloc 调用堆栈无法显示真正调用 CoTaskMemAlloc 函数的堆栈条目。只能显示上层堆栈条目。

    参考文献

    历史

    • 2002 年 11 月 4 日
      • 初始版本。
    • 2002 年 11 月 5 日
      • 更新以兼容 MFC,“暂时禁用日志记录”、“MFC 用法”和“参考文献”已添加。
    • 2002 年 11 月 6 日
      • 添加了泄漏文件的“解释”、“关于泄漏”和“工作原理:详细说明”。
    • 2002 年 11 月 7 日
      • 实现了简单的泄漏输出。
      • 解释简单的泄漏输出。
      • 添加了“常见错误”。
    • 2002 年 11 月 8 日
      • 解释了 InitAllocCheck 参数。
      • 添加了 XML 输出,“即将推出”和“历史记录”已添加。
    • 2002 年 11 月 13 日
      • 更新了源代码,使其可以在 VC6 下重新编译(SymDIA 未定义,稍后更正)。
    • 2002 年 11 月 21 日
      • 更新了源代码,使其可以与 UNICODE 版本一起重新编译。
      • 在 MFC 演示中,使用 #include "stdafx.h" 而不是 #include <stdafx.h>
    • 2002 年 12 月 6 日
      • 更新以正确支持 UNICODE。
      • 添加了 MemLeakAnalyse 工具;删除了“即将推出”。
    • 2002 年 12 月 19 日
      • 现在仅使用 dbghelp.dll。NT 用户必须安装 NT 的 dbghelp.dll 可再发行版(请参阅上面的参考文献)。
      • 添加了关于如何在 VS7 的 NT/W2K 上使用它的注释...
    • 2003 年 1 月 8 日
      • 主要更新:添加了对 COM 泄漏的支持。
      • 对 MemLeakAnalyse 工具进行了小幅更新;修复了一些小错误。
    • 2003 年 1 月 9 日
      • 删除了 IMallocSpy 接口中的句柄泄漏。
    • 2003 年 8 月 23 日
      • 更新了几个链接。
    • 2003 年 8 月 28 日
      • 更新到较新版本的 dbghelp.dll (StackWalk64)。
      • 添加了 OnlyInstallUnhandeldExceptionFilter 函数;许可证澄清:LGPL。
    • 2003 年 9 月 3 日
      • 更新了源代码以捕获“堆栈溢出”。
      • 删除了内存泄漏查找器中的一些“内存泄漏”;哈希表大小现在是一个素数。
    • 2003 年 9 月 12 日
      • 修复了 PreRealloc 中的一个 bug;感谢 Christoph Weber。
      • 许可证更改为“zlib/libpng 许可证”。
      • AnalyseTool:添加了命令行支持(您可以在命令行中指定 XML 文件)。
      • AnalyseTool:支持用于查找源文件的附加搜索路径。
    • 2005 年 10 月 19 日
      • 由于 XP SP2 中的更改,将 GetThreadContext 更改为我自己的函数。
      • 将许可证更改为 LGPL
    • 2005 年 11 月 21 日
      • 对最新 dbghelp.dll (6.5.3.7/8) 中的 bug 进行规避。
    © . All rights reserved.