如何使用 UMDH 分析进程内存泄漏
下面使用到的工具umdh.exe
和gflags.exe
,都在windbg.exe
同目录下,同属于Windows 调试工具集。
UMDH(User-Mode Dump Heap)使用步骤:
- 设置调试符号路径
设置调试符号路径,有两种不同的方法:
(1). 在系统环境变量中添加_NT_SYMBOL_PATH
变量,值为c:\mysymbols;srv*c:\mycache*https://msdl.microsoft.com/download/symbols
(2). 也可以直接在 cmd 里面运行set _NT_SYMBOL_PATH=c:\mysymbols;srv*c:\mycache*https://msdl.microsoft.com/download/symbols
其中,c:\mysymbols
为自己软件的符号表路径。
注:
c:\mycache
为 Windows 系统模块符号表的下载存储路径,可以自定义。- 如果使用
set _NT_SYMBOL_PATH=***
方式,cmd 窗口不要关闭,后面运行umdh.exe"
也必须在这个窗口,如果重新打开一个cmd,则前面设置的_NT_SYMBOL_PATH
值会丢失。
-
退出欲分析内存泄漏的目标进程
-
使用 gflags 给目标进程添加
ust
设置,也可双击运行 gflags.exe 打开图形界面进行设置
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\gflags.exe" -i ConsoleApplication1.exe +ust
-
重启目标进程
-
等目标进程初始化完成后,抓取初始内存快照
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\umdh.exe" -pn:ConsoleApplication1.exe -f:1.log
注意
64 位 umdh.exe 不能用于 32 位进程。 -
运行一段时间后,抓取当前内存快照
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\umdh.exe" -pn:ConsoleApplication1.exe -f:2.log
-
对比两次内存快照,生成内存泄漏分析结果
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\umdh.exe" -d 1.log 2.log -f:result.txt
注意比较结果里面的的数字是十六进制,参考 Interpreting a Log Comparison
- 使用 gflags 取消目标进程的 ust 设置
"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\gflags.exe" -i ConsoleApplication1.exe -ust
一个 demo 程序的分析结果
demo 程序代码:
#include <iostream>
#include <windows.h>
int main()
{
std::cout << "snapshot1 \n";
Sleep(10 * 1000); // 这里抓取一次内存快照
for (int i = 0; i < 15; ++i)
{
void* p = malloc(100);
}
std::cout << "snapshot2 \n";
Sleep(10 * 1000); // 这里抓取一次内存快照
}
两次内存快照分析结果:
// Debug library initialized ...
5C0000-5C6FFF DBGHELP: ConsoleApplication1 - private symbols & lines
.\ConsoleApplication1.pdb
77810000-779B3FFF DBGHELP: ntdll - export symbols
764F0000-765DFFFF DBGHELP: KERNEL32 - export symbols
76E50000-77089FFF DBGHELP: KERNELBASE - export symbols
55DE0000-55E1EFFF DBGHELP: InfraCore32 - export symbols
66770000-667D2FFF DBGHELP: ummon - export symbols
76620000-7669BFFF DBGHELP: ADVAPI32 - export symbols
76110000-761CEFFF DBGHELP: msvcrt - export symbols
75C20000-75C95FFF DBGHELP: sechost - export symbols
769D0000-76A8EFFF DBGHELP: RPCRT4 - export symbols
76AB0000-76C4BFFF DBGHELP: USER32 - export symbols
76480000-76497FFF DBGHELP: win32u - export symbols
777D0000-777F3FFF DBGHELP: GDI32 - export symbols
770C0000-771A4FFF DBGHELP: gdi32full - export symbols
76400000-7647AFFF DBGHELP: msvcp_win - export symbols
75CA0000-75DBFFFF DBGHELP: ucrtbase - export symbols
76C50000-76D4FFFF DBGHELP: CRYPT32 - export symbols
76310000-763F2FFF DBGHELP: ole32 - export symbols
76750000-769CFFFF DBGHELP: combase - export symbols
75DC0000-75E55FFF DBGHELP: OLEAUT32 - export symbols
70AD0000-70AE4FFF DBGHELP: VCRUNTIME140 - export symbols
6BC60000-6BCCCFFF DBGHELP: MSVCP140 - export symbols
77090000-770B4FFF DBGHELP: IMM32 - export symbols
7C210000-7C2DAFFF DBGHELP: rcscanner32 - no symbols loaded
75BD0000-75C14FFF DBGHELP: SHLWAPI - export symbols
77210000-777C5FFF DBGHELP: SHELL32 - export symbols
72AF0000-72B42FFF DBGHELP: OLEACC - export symbols
73E20000-73E27FFF DBGHELP: VERSION - export symbols
782E0000-7838AFFF DBGHELP: bdsecdll32 - no symbols loaded
75B60000-75BC2FFF DBGHELP: WS2_32 - export symbols
72980000-72AE6FFF DBGHELP: gdiplus - export symbols
70D50000-70D62FFF DBGHELP: CRYPTSP - export symbols
70DE0000-70E0EFFF DBGHELP: rsaenh - export symbols
762F0000-76308FFF DBGHELP: bcrypt - export symbols
6FEF0000-6FEF9FFF DBGHELP: CRYPTBASE - export symbols
75F40000-75F9EFFF DBGHELP: bcryptPrimitives - export symbols
//
// Each log entry has the following syntax:
//
// + BYTES_DELTA (NEW_BYTES - OLD_BYTES) NEW_COUNT allocs BackTrace TRACEID
// + COUNT_DELTA (NEW_COUNT - OLD_COUNT) BackTrace TRACEID allocations
// ... stack trace ...
//
// where:
//
// BYTES_DELTA - increase in bytes between before and after log
// NEW_BYTES - bytes in after log
// OLD_BYTES - bytes in before log
// COUNT_DELTA - increase in allocations between before and after log
// NEW_COUNT - number of allocations in after log
// OLD_COUNT - number of allocations in before log
// TRACEID - decimal index of the stack trace in the trace database
// (can be used to search for allocation instances in the original
// UMDH logs).
//
+ 4096 ( 4096 - 0) 1 allocs BackTrace509C798
+ 1 ( 1 - 0) BackTrace509C798 allocations
ntdll!RtlWalkHeap+194
ntdll!RtlAllocateHeap+10DC
ntdll!RtlAllocateHeap+3E
ucrtbase!malloc_base+26
ucrtbase!write+34E
ucrtbase!fwrite+CA
ucrtbase!fwrite+7F
ucrtbase!fwrite+4E
MSVCP140!std::basic_streambuf<char,std::char_traits<char> >::xsgetn+1BD
MSVCP140!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::sputn+21
ConsoleApplication1!std::operator<<<std::char_traits<char> >+143 (C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.29.30037\include\ostream, 756)
ConsoleApplication1!main+2F (C:\Users\xxx\Desktop\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.cpp, 15)
KERNEL32!BaseThreadInitThunk+19
InfraCore32!IfAddHook+197C9
ntdll!RtlGetAppContainerNamedObjectPath+11E
ntdll!RtlGetAppContainerNamedObjectPath+EE
泄漏 0x1500 个字节,注意这里是 16 进制!
分配了 0x15 次
+ 1500 ( 1500 - 0) 15 allocs BackTrace509C6F0
+ 15 ( 15 - 0) BackTrace509C6F0 allocations
ntdll!RtlWalkHeap+194
ntdll!RtlAllocateHeap+10DC
ntdll!RtlAllocateHeap+3E
ucrtbase!malloc_base+26
ConsoleApplication1!main+1C (C:\Users\xxx\Desktop\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.cpp, 12)
KERNEL32!BaseThreadInitThunk+19
InfraCore32!IfAddHook+197C9
ntdll!RtlGetAppContainerNamedObjectPath+11E
ntdll!RtlGetAppContainerNamedObjectPath+EE
Total increase == 5596 requested + 444 overhead = 6040
注意
注意分析结果文件中的每个 BackTrace(如 BackTrace509C6F0) 代表还驻留在内存中,没被释放。
可用下面的代码测试
#include <iostream>
#include <windows.h>
#include <vector>
int main()
{
std::cout << "snapshot1 \n";
Sleep(10 * 1000); // 这里抓取一次内存快照
std::vector<void*> v;
for (int i = 0; i < 100; ++i)
{
void* p = malloc(100);
v.push_back(p);
}
// 下面代码释放内存,最终在 UMDH 里显示没有内存分配(泄漏)。
// 如果去掉,就会报泄漏。
{
for (void* p : v)
{
free(p);
}
v.clear();
v.~vector();
}
std::cout << "snapshot2 \n";
Sleep(10 * 1000); // 这里抓取一次内存快照
}