使用 UMDH 查找用户模式内存泄漏

用户模式转储堆 (UMDH) 实用工具与操作系统配合使用,以分析特定进程的 Windows 堆分配。 UMDH 查找特定进程中哪个例程正在泄漏内存。

UMDH 包含在 Windows 调试工具中。 有关完整详细信息,请参阅 UMDH

准备使用 UMDH

如果尚未确定哪个进程正在泄漏内存,请先执行此操作。 有关详细信息,请参阅使用性能监视器查找User-Mode内存泄漏

UMDH 日志中最重要的数据是堆分配的堆栈跟踪。 若要确定某个进程是否正在泄漏堆内存,请分析这些堆栈跟踪。

在使用 UMDH 显示堆栈跟踪数据之前,必须使用 GFlags 正确配置系统。 GFlags 包含在 Windows 调试工具中。

以下 GFlags 设置启用 UMDH 堆栈跟踪:

  • 在 GFlags 图形界面中,选择“图像文件”选项卡,键入进程名称 (包括文件扩展名) ,按 TAB 键,选择“ 创建用户模式堆栈跟踪数据库”,然后选择“ 应用”。

    或者,同样,使用以下 GFlags 命令行,其中 ImageName 是进程名称 (包括文件扩展名) :

    gflags /i ImageName +ust 
    

    完成后,使用此命令清除 GFlag 设置。 有关详细信息,请参阅 GFlags 命令

    gflags /i ImageName -ust 
    
  • 默认情况下,Windows 收集的堆栈跟踪数据量在 x86 处理器上限制为 32 MB,在 x64 处理器上限制为 64 MB。 如果必须增加此数据库的大小,请在 GFlags 图形界面中选择“图像文件”选项卡,键入进程名称,按 TAB 键,检查“堆栈回溯 (Megs) 检查”框,在关联的文本框中键入以 MB) (值,然后选择“应用”。 仅在必要时增加此数据库,因为它可能会耗尽有限的 Windows 资源。 如果不再需要更大的大小,请将此设置返回到其原始值。

  • 如果在“ 系统注册表 ”选项卡上更改了任何标志,则必须重启 Windows 才能使这些更改生效。 如果更改了“ 图像文件 ”选项卡上的任何标志,则必须重启该过程才能使更改生效。 对“ 内核标志 ”选项卡的更改会立即生效,但在下次 Windows 重启时会丢失。

在使用 UMDH 之前,必须有权访问应用程序的正确符号。 UMDH 使用环境变量_NT_SYMBOL_PATH指定的符号路径。 将此变量设置为包含应用程序的符号的路径。 如果还包括 Windows 符号的路径,则分析可能更完整。 此符号路径的语法与调试器使用的语法相同;有关详细信息,请参阅 符号路径

例如,如果应用程序的符号位于 C:\MySymbols,并且你希望使用 C:\MyCache 作为下游存储区来使用 Windows 符号的公共 Microsoft 符号存储,则可以使用以下命令设置符号路径:

set _NT_SYMBOL_PATH=c:\mysymbols;srv*c:\mycache*https://msdl.microsoft.com/download/symbols 

此外,为了确保结果准确,必须禁用 BSTR 缓存。 为此,请将 OANOCACHE 环境变量设置为等于 1 (1) 。 在启动要跟踪其分配的应用程序之前进行此设置。

如果需要跟踪服务进行的分配,必须将 OANOCACHE 设置为系统环境变量,然后重启 Windows 才能使此设置生效。

使用 UMDH 检测堆分配的增加

完成这些准备工作后,可以使用 UMDH 捕获有关进程的堆分配的信息。 为此,请遵循以下过程:

  1. 确定要调查 的进程 (PID) 的进程 ID

  2. 使用 UMDH 分析此过程的堆内存分配,并将其保存到日志文件。 将 -p 开关与 PID 配合使用,将 -f 开关用于日志文件的名称。 例如,如果 PID 为 124,并且想要将日志文件命名为Log1.txt,请使用以下命令:

    umdh -p:124 -f:log1.txt 
    
  3. 使用记事本或其他程序打开日志文件。 此文件包含每个堆分配的调用堆栈、通过该调用堆栈进行的分配数以及通过该调用堆栈使用的字节数。

  4. 由于要查找内存泄漏,因此单个日志文件的内容不足。 必须比较在不同时间记录的日志文件,以确定哪些分配正在增加。

    UMDH 可以比较两个不同的日志文件,并按各自的分配大小显示更改。 可以使用大于符号 (>) 将结果重定向到第三个文本文件。 可能还需要包含 -d 选项,该选项将字节和分配计数从十六进制转换为十进制。 例如,若要比较Log1.txt和Log2.txt,将比较结果保存到文件LogCompare.txt,请使用以下命令:

    umdh log1.txt log2.txt > logcompare.txt 
    
  5. 打开LogCompare.txt文件。 其内容如下所示:

    + 5320 ( f110 - 9df0) 3a allocs BackTrace00B53 
    Total increase == 5320 
    

    对于 UMDH 日志文件中标记为“BackTrace”) 的每个调用堆栈 (,在两个日志文件之间进行了比较。 在此示例中,第一个日志文件 (Log1.txt) 记录了为 BackTrace00B53 分配的0x9DF0字节,而第二个日志文件记录0xF110个字节,这意味着在捕获两个日志的时间之间分配了0x5320个额外的字节。 这些字节来自 BackTrace00B53 标识的调用堆栈。

  6. 若要确定该回溯中的内容,请打开其中一个原始日志文件 (例如,Log2.txt) 并搜索“BackTrace00B53”。结果类似于以下数据:

    00005320 bytes in 0x14 allocations (@ 0x00000428) by: BackTrace00B53
    ntdll!RtlDebugAllocateHeap+0x000000FD
    ntdll!RtlAllocateHeapSlowly+0x0000005A
    ntdll!RtlAllocateHeap+0x00000808
    MyApp!_heap_alloc_base+0x00000069
    MyApp!_heap_alloc_dbg+0x000001A2
    MyApp!_nh_malloc_dbg+0x00000023
    MyApp!_nh_malloc+0x00000016
    MyApp!operator new+0x0000000E
    MyApp!DisplayMyGraphics+0x0000001E
    MyApp!main+0x0000002C
    MyApp!mainCRTStartup+0x000000FC
    KERNEL32!BaseProcessStart+0x0000003D 
    

    此 UMDH 输出显示0x5320 (十进制 21280) 从调用堆栈分配的总字节数。 这些字节是从0x14 (十进制 20) 0x428 (1064) 字节的单独分配分配的。

    调用堆栈的标识符为“BackTrace00B53”,并显示此堆栈中的调用。 在查看调用堆栈时,可以看到 DisplayMyGraphics 例程正在通过 new 运算符分配内存,该运算符调用例程 malloc,后者使用 Visual C++ 运行时库从堆中获取内存。

    确定其中哪一个调用是最后一个在源代码中显式显示的调用。 在这种情况下,可能是 运算符,因为对 malloc 的调用是 作为 new 实现的一部分发生的,而不是单独的分配。 因此,DisplayMyGraphics 例程中 new 运算符的此实例会重复分配未释放的内存。