查看原文
其他

Windows 程序内存泄漏 ( Memory Leak ) 分析之Windbg

CPP开发者 2021-09-08

The following article is from 一个程序员的修炼之路 Author 河边一枝柳

之前写了一篇<<Windows程序内存泄漏(Memory Leak)分析之UMDH>>。这种方法有一定的局限性:

  1. 实践证明,当程序复杂,内存频繁的申请释放,通过UMDH对比的文件将会非常的大,并且很难直接看出内存泄露所在。

  2. UMDH在收集信息的需要符号文件,不太适合于在客户的机器上进行操作。

调试方法很难一通百用,因为不同的工具都有自己的局限性,也有适合自己的分析场景,这个取决于碰到的问题。那么本文来介绍一种,使用Windbg分析内存泄露的方法。

样例代码

这个样例代码中循环调用一个Memory Leak的函数:

#include <iostream>#include <chrono>#include <thread>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 <heap address>来查看各个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 <address>来查看刚刚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 <address>,就可以看到泄露的内存地址对应的函数调用栈了。那么接下来我们一起来看看是如何分析内存泄露的。

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

总结

  1. 本文所阐述的方式是针对同一种大小的内存申请导致的内存泄露。而内存泄露在大型工程中还有可能是可变大小的,那么这种方法就不适合。这也是为什么内存泄露问题写了两篇文章还没写完: 内存泄露各式各样,在客户环境如何定位问题,也是难上加难。计划后面还会写几篇比如vmmap, DebugDialog,以及其他的一些非使用工具的一些方法。

  2. 上面的例子是笔者attach到进程调试的结果。如果碰到在客户环境有这样的问题,显然在线调试是不太可能的,可以用gflag开启ust后收集两次Dump来查找问题(这两次dump的间隔时间要足以观测到内存泄露,根据实际情况而定)。

  3. 编写代码的时候尽量使用智能指针unique_ptrshared_ptr,埋坑简单,但找到问题的原因可能比写代码的时间都长。

发送关键字 内存泄漏 获取内存泄漏系列文章


- EOF -

推荐阅读  点击标题可跳转

1、Linux 系统是如何用虚拟内存来欺骗应用程序的?

2、万字整理,肝翻 Linux 内存管理所有知识点

3、一起探索C++类内存分布


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

点赞和在看就是最大的支持❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存