当前位置:首页 > 单片机 > 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 -

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

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭