Windows 程序内存泄漏 ( Memory Leak ) 分析之Windbg
扫描二维码
随时随地手机看文章
- 实践证明,当程序复杂,内存频繁的申请释放,通过
UMDH
对比的文件将会非常的大,并且很难直接看出内存泄露所在。 UMDH
在收集信息的需要符号文件,不太适合于在客户的机器上进行操作。
样例代码
这个样例代码中循环调用一个Memory Leak的函数:#include #include #include class TestClass{public: char m_str[100];};void MemoryLeakObj(){ TestClass * pObj = new TestClass; strcpy_s(pObj->m_str, 100, "Memory Leak Sample"); std::cout << pObj->m_str << std::endl;}int main(){ while (true) { MemoryLeakObj(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } return 0;}
基础知识
这个章节了解下堆的一些基本知识。一个进程可以有若干个堆,包括CRT库中malloc
也是从堆中申请内存,也可以自己通过Windows API HeapCreate
创建堆。在windbg中查看所有的堆, 一般主要通过查看commit
的内存来确定是否有内存泄露。0:008> !heap -s
*****************************************************************************************************
NT HEAP STATS BELOW
*****************************************************************************************************
NtGlobalFlag enables following debugging aids for new heaps:
tail checking
free checking
validate parameters
LFH Key : 0x3f0f03d02e6012eb
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-------------------------------------------------------------------------------------
0000026349b50000 40000062 2040 1088 2040 2 26 2 1 0
00000263499d0000 40008060 64 4 64 2 1 1 0 0
0000026349b30000 40001062 60 20 60 2 2 1 0 0
000002634b440000 40001062 1080 88 1080 2 4 2 0 0
-------------------------------------------------------------------------------------
Windows中,一个堆本身并不只是由一个连续的空间组成,而是可以由多个连续的空间组成,而每一个连续的空间我们称之为Segment
。我们挑选一个堆来查看他的Segment
。可以看到这个堆目前由两个Segment
构成,并且列出了每个Segment
的地址范围。0:008> !heap 0000026349b50000
Index Address Name Debugging options enabled
1: 26349b50000
Segment at 0000026349b50000 to 0000026349c4f000 (000ff000 bytes committed)
Segment at 000002634bef0000 to 000002634bfef000 (00011000 bytes committed)
可以通过heap -a
来查看各个Segment
中申请内存。我们申请的内存的时候便是占用每一个Entry
,有时候也叫做block
。0:008> !heap -a 26349b50000
Index Address Name Debugging options enabled
1: 26349b50000
Segment at 0000026349b50000 to 0000026349c4f000 (000ff000 bytes committed)
Segment at 000002634bef0000 to 000002634bfef000 (00011000 bytes committed)
Flags: 40000062
ForceFlags: 40000060
Granularity: 16 bytes
Segment Reserve: 00200000
Segment Commit: 00002000
DeCommit Block Thres: 00000100
DeCommit Total Thres: 00001000
Total Free Size: 0000009f
Max. Allocation Size: 00007ffffffdefff
Lock Variable at: 0000026349b502a0
Next TagIndex: 0000
Maximum TagIndex: 0000
Tag Entries: 00000000
PsuedoTag Entries: 00000000
Virtual Alloc List: 26349b50110
000002634ba79000: 00100000 [commited 101000, unused 1000] - busy (b)
Uncommitted ranges: 26349b500f0
2634bf01000: 000ee000 (974848 bytes)
FreeList[ 00 ] at 0000026349b50150: 000002634bf00a30 . 0000026349bd9fb0
0000026349bd9fa0: 00050 . 00020 [104] - free
0000026349bd4670: 00050 . 00020 [104] - free
0000026349bd8630: 000b0 . 00020 [104] - free
0000026349bd80c0: 00050 . 00020 [104] - free
0000026349bd60b0: 00060 . 00020 [104] - free
0000026349bd53f0: 000b0 . 00020 [104] - free
0000026349b5f4c0: 00060 . 00020 [104] - free
0000026349b5dea0: 00050 . 00020 [104] - free
0000026349b61860: 00090 . 00020 [104] - free
0000026349b57ae0: 00080 . 00020 [104] - free
0000026349b53990: 00080 . 00020 [104] - free
0000026349b6a800: 00050 . 00030 [104] - free
0000026349b629c0: 00050 . 00030 [104] - free
0000026349b5f610: 00070 . 00030 [104] - free
0000026349b60a90: 00070 . 00030 [104] - free
0000026349b62390: 00070 . 00030 [104] - free
0000026349b5f940: 000c0 . 00030 [104] - free
0000026349b668b0: 00070 . 00030 [104] - free
0000026349b65230: 00040 . 00030 [104] - free
0000026349b65ad0: 00040 . 00030 [104] - free
0000026349b57e70: 00080 . 00030 [104] - free
0000026349b57cb0: 00070 . 00030 [104] - free
0000026349b57930: 00050 . 00030 [104] - free
0000026349bd9c70: 000a0 . 00040 [104] - free
0000026349bd9ea0: 00040 . 00070 [104] - free
000002634bf00a20: 000a0 . 005a0 [104] - free
Segment00 at 49b50000:
Flags: 00000000
Base: 26349b50000
First Entry: 49b50720
Last Entry: 26349c4f000
Total Pages: 000000ff
Total UnCommit: 00000000
Largest UnCommit:00000000
UnCommitted Ranges: (1)
Heap entries for Segment00 in Heap 0000026349b50000
address: psize . size flags state (requested size)
0000026349b50000: 00000 . 00720 [101] - busy (71f)
0000026349b50720: 00720 . 00130 [107] - busy (12f), tail fill Internal
0000026349b50850: 00130 . 00130 [107] - busy (100), tail fill
.......
0000026349c4ede0: 000a0 . 000a0 [107] - busy (64), tail fill
0000026349c4ee80: 000a0 . 000a0 [107] - busy (64), tail fill
0000026349c4ef20: 000a0 . 000a0 [107] - busy (64), tail fill
0000026349c4efc0: 000a0 . 00040 [111] - busy (3d)
0000026349c4f000: 00000000 - uncommitted bytes.
Segment01 at 4bef0000:
Flags: 00000000
Base: 2634bef0000
First Entry: 4bef0070
Last Entry: 2634bfef000
Total Pages: 000000ff
Total UnCommit: 000000ee
Largest UnCommit:00000000
UnCommitted Ranges: (1)
Heap entries for Segment01 in Heap 0000026349b50000
address: psize . size flags state (requested size)
000002634bef0000: 00000 . 00070 [101] - busy (6f)
000002634bef0070: 00070 . 000a0 [107] - busy (64), tail fill
.......
000002634bf00700: 000a0 . 000a0 [107] - busy (64), tail fill
000002634bf00840: 000a0 . 000a0 [107] - busy (64), tail fill
000002634bf008e0: 000a0 . 000a0 [107] - busy (64), tail fill
000002634bf00980: 000a0 . 000a0 [107] - busy (64), tail fill
000002634bf00a20: 000a0 . 005a0 [104] free fill
000002634bf00fc0: 005a0 . 00040 [111] - busy (3d)
000002634bf01000: 000ee000 - uncommitted bytes.
但是Entry
的地址并不等同于我们通过malloc
返回的地址,比如通过heap -x
来查看刚刚Entry
的信息,注意到Entry
的地址和User
(也就是我们通过malloc
申请的内存地址啦)不同,那是堆通过Entry
开头_HEAP_ENTRY
数据结构进行Entry
管理。0:008> !heap -x 000002634bf00980
Entry User Heap Segment Size PrevSize Unused Flags
-------------------------------------------------------------------------------------------------------------
000002634bf00980 000002634bf00990 0000026349b50000 000002634bef0000 a0 a0 3c busy extra fill
那么假设我们知道泄漏的内存地址了,如何知道申请内存的函数调用栈呢?在进行运行前,使用gflag设置记录函数调用栈信息: "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\gflags" -i MemoryLeakAnalysisViaWindbg.exe ust
。然后调用heap -p -a
,就可以看到泄露的内存地址对应的函数调用栈了。那么接下来我们一起来看看是如何分析内存泄露的。Windbg内存泄露分析
第一步
要做的和UMDH
分析一样,调用以下命令对MemoryLeakAnalysisViaWindbg.exe
程序在申请堆上内存的时候记录其函数调用栈"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\gflags" -i MemoryLeakAnalysisViaWindbg.exe ust
。第二步
开始运行程序一段时间,查看当前堆的使用情况, 主要查看commit
的大小,再用g
指令运行一段后,查看是哪个对的commit
的大小增加比较快。这里锁定到了堆000001471ba50000
。0:006> !heap -s
************************************************************************************************************************
NT HEAP STATS BELOW
************************************************************************************************************************
NtGlobalFlag enables following debugging aids for new heaps:
stack back traces
LFH Key : 0xe82e55f3a47de176
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-------------------------------------------------------------------------------------
000001471ba50000 08000002 1220 820 1020 48 25 1 1 0 LFH
000001471a110000 08008000 64 4 64 2 1 1 0 0
000001471bd50000 08001002 260 36 60 7 2 1 0 0 LFH
000001471bd10000 08001002 1280 112 1080 4 3 2 0 0 LFH
-------------------------------------------------------------------------------------
通过指令!heap -stat [-h Handle [-grp GroupBy [MaxDisplay]]]
来做统计信息。这里按照block
的数量进行排序筛选出前5的。这里注意有时候数量多不一定就是泄露的点,如果运行时间足够长也可以使用-grp S
选项来根据同种类型的内存申请的总和进行排序。0:006> !heap -stat -h 000001471ba50000 -grp B 5
heap @ 000001471ba50000
group-by: BLOCKCOUNT max-display: 5
size #blocks total ( %) (percent of totalblocks)
64 1fa - c5a8 (30.43)
30 12c - 3840 (18.04)
48 d1 - 3ac8 (12.57)
20 7f - fe0 (7.64)
10 3c - 3c0 (3.61)
第三步
运行一段时间,足够明显的感觉到内存的增长,此时中断调试,继续按照block
的数量进行排序。此时观察到大小为0x64
的对象从数量0x1fa
增长到0x849
,增加了1615次申请。那么如此数量的增长,或者上面如果是用-grp S
进行观测,则寻找内存增加较多的Entry Size
0:009> !heap -stat -h 000001471ba50000 -grp B 5
heap @ 000001471ba50000
group-by: BLOCKCOUNT max-display: 5
size #blocks total ( %) (percent of totalblocks)
64 849 - 33c84 (64.14)
30 12c - 3840 (9.07)
48 d1 - 3ac8 (6.32)
20 7e - fc0 (3.81)
10 3c - 3c0 (1.81)
第四步
然后根据这个特定的大小,查看所有对应的entry
。此时可能有很多的entry, 如果想保存下来windbg 提供.logopen
和.logclose
来保存命令输出结果。0:009> !heap -flt s 64
_HEAP @ 1471ba50000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001471ba61790 0009 0000 [00] 000001471ba617c0 00064 - (busy)
000001471ba66d80 0009 0009 [00] 000001471ba66db0 00064 - (busy)
000001471bafaa80 0009 0009 [00] 000001471bafaab0 00064 - (busy)
000001471bafab10 0009 0009 [00] 000001471bafab40 00064 - (busy)
......
000001471df9fd10 0009 0009 [00] 000001471df9fd40 00064 - (busy)
000001471df9fda0 0009 0009 [00] 000001471df9fdd0 00064 - (busy)
000001471df9fe30 0009 0009 [00] 000001471df9fe60 00064 - (busy)
000001471df9fec0 0009 0009 [00] 000001471df9fef0 00064 - (busy)
000001471df9ff50 0009 0009 [00] 000001471df9ff80 00064 - (busy)
000001471df9ffe0 0009 0009 [00] 000001471dfa0010 00064 - (busy)
_HEAP @ 1471a110000
_HEAP @ 1471bd50000
_HEAP @ 1471bd10000
第五步
随便找几个Entry
的地址查看其函数调用栈,比如这里查看000001471df9ff50
。比较容易就定位到了申请内存的代码。不过这里注意一下为什么函数栈是main
而不是MemoryLeakObj
,这是因为我们的编译进行的优化,不过这也不妨碍我们找到问题。0:009> !heap -p -a 000001471df9ff50
address 000001471df9ff50 found in
_HEAP @ 1471ba50000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001471df9ff50 0009 0000 [00] 000001471df9ff80 00064 - (busy)
7ff8350fbe47 ntdll!RtlpCallInterceptRoutine 0x000000000000003f
7ff8350baa6f ntdll!RtlpAllocateHeapInternal 0x000000000009192f
7ff8315b9686 ucrtbase!_malloc_base 0x0000000000000036
7ff6558613a3 MemoryLeakAnalysisViaWindbg!operator new 0x000000000000001f
7ff65586102d MemoryLeakAnalysisViaWindbg!main 0x000000000000002d
7ff6558615b0 MemoryLeakAnalysisViaWindbg!__scrt_common_main_seh 0x000000000000010c
7ff834e84034 KERNEL32!BaseThreadInitThunk 0x0000000000000014
7ff835083691 ntdll!RtlUserThreadStart 0x0000000000000021
总结
- 本文所阐述的方式是针对同一种大小的内存申请导致的内存泄露。而内存泄露在大型工程中还有可能是可变大小的,那么这种方法就不适合。这也是为什么内存泄露问题写了两篇文章还没写完: 内存泄露各式各样,在客户环境如何定位问题,也是难上加难。计划后面还会写几篇比如vmmap, DebugDialog,以及其他的一些非使用工具的一些方法。
- 上面的例子是笔者attach到进程调试的结果。如果碰到在客户环境有这样的问题,显然在线调试是不太可能的,可以用gflag开启
ust
后收集两次Dump来查找问题(这两次dump的间隔时间要足以观测到内存泄露,根据实际情况而定)。 - 编写代码的时候尽量使用智能指针
unique_ptr
和shared_ptr
,埋坑简单,但找到问题的原因可能比写代码的时间都长。
- EOF -