当前位置:首页 > 公众号精选 > CPP开发者
[导读]之前写了一篇>"data-itemshowtype="11"tab="innerlink"data-linktype="2">。这种方法有一定的局限性:实践证明,当程序复杂,内存频繁的申请释放,通过UMDH对比的文件将会非常的大,并且很难直接看出内存泄露所在。UMDH在收集信息的...

之前写了一篇>" data-itemshowtype="11" tab="innerlink" data-linktype="2"><<Windows程序内存泄漏(Memory Leak)分析之UMDH>>。这种方法有一定的局限性:

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

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

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

样例代码

这个样例代码中循环调用一个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

总结

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

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

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

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

- EOF -

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

据韩联社报道,上周三星电子发布业绩报告显示,随着芯片价格反弹,预计今年第一季度营业利润同比骤增931.25%,为6.6万亿韩元(当前约合人民币354.6亿元),已经超过了2023年全年营业利润6.57万亿韩元。

关键字: 内存 三星

TDK 株式会社(TSE:6762)进一步扩充 Micronas 嵌入式电机控制器系列 HVC 5x,完全集成电机控制器与 HVC-5222D 和 HVC-5422D,以驱动小型有刷(BDC)、无刷(BLDC)或步进电机...

关键字: 嵌入式 电机控制器 内存

Apr. 04, 2024 ---- TrendForce集邦咨询针对403震后各半导体厂动态更新,由于本次地震大多晶圆代工厂都位属在震度四级的区域,加上台湾地区的半导体工厂多以高规格兴建,内部的减震措施都是世界顶尖水平...

关键字: 晶圆代工 内存

好用、高效的多合一传感器开发工具,支持给新一代高科技 MEMS 传感器产品开发应用软件

关键字: 传感器 Windows MacOS

美光坚持多元、平等、包容的企业文化,携手社区推行公益

关键字: 内存 存储 美光

今天,小编将在这篇文章中为大家带来虚拟内存的有关报道,通过阅读这篇文章,大家可以对虚拟内存具备清晰的认识,主要内容如下。

关键字: 内存 虚拟内存

双系统将是下述内容的主要介绍对象,通过这篇文章,小编希望大家可以对双系统的相关情况以及信息有所认识和了解,详细内容如下。

关键字: 双系统 Windows Linux

在这篇文章中,小编将对虚拟内存的相关内容和情况加以介绍以帮助大家增进对它的了解程度,和小编一起来阅读以下内容吧。

关键字: 内存 虚拟内存

以下内容中,小编将对物理内存的相关内容进行着重介绍和阐述,希望本文能帮您增进对物理内存的了解,和小编一起来看看吧。

关键字: 内存 物理内存

美光 LPDDR5X 和 UFS 4.0 以高带宽、高能效以及大容量助力荣耀人工智能创新

关键字: AI 内存 存储 智能手机
关闭
关闭