一:背景
1.讲故事
最近分享了好几篇关于 非托管内存泄漏 的文章,有时候就是这么神奇,来求助的都是这类型的dump,一饮一啄,莫非前定。让我被迫加深对 NT堆, 页堆 的理解,这一篇就给大家再带来一篇内存泄漏。
前段时间有位朋友找到我,说他的程序出现了非托管泄漏,某一块的操作会导致非托管内存上涨的很快,让我帮忙逆向看下是哪里的操作没有释放资源? 既然找到我,那就上 WinDbg 分析吧。
二:WinDbg 分析
1. 哪里的内存泄漏
看内存泄漏还是老规矩,使用 !address -summary 命令就可以了。- 0:000> !address -summary
- --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
- Free 443 7fc`685d1000 ( 7.986 TB) 99.82%
- Heap 658 3`563aa000 ( 13.347 GB) 92.89% 0.16%
- <unknown> 770 0`1ff5a000 ( 511.352 MB) 3.48% 0.01%
- Image 1196 0`108ba000 ( 264.727 MB) 1.80% 0.00%
- Stack 108 0`08c40000 ( 140.250 MB) 0.95% 0.00%
- Other 31 0`081d8000 ( 129.844 MB) 0.88% 0.00%
- TEB 36 0`00048000 ( 288.000 kB) 0.00% 0.00%
- PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00%
- --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
- MEM_FREE 443 7fc`685d1000 ( 7.986 TB) 99.82%
- MEM_COMMIT 2464 3`67933000 ( 13.618 GB) 94.77% 0.17%
- MEM_RESERVE 336 0`300ec000 ( 768.922 MB) 5.23% 0.01%
复制代码 从卦中看,当前进程有 13.6 G 的提交内存,NtHeap 占用了 13G,很明显这是非托管内存泄漏,既然是非托管泄漏,那就需要二番战,也就是让朋友开启 ust,或者启用应用程序验证器 (Application Verifier) 开启页堆,目的就是记录分配这块内存的源头,这里就让朋友用 gflags 开启下 ust,具体怎么开,这里就不介绍了,大家可以网上搜一下。
2. 追踪 ust 加持下的调用栈
有了 ust 的加持,接下来就可以继续分析,使用 !heap -s 观察下 nt 堆的布局。- 0:000> !heap -s
- SEGMENT HEAP ERROR: failed to initialize the extention
- NtGlobalFlag enables following debugging aids for new heaps:
- stack back traces
- LFH Key : 0x0000004c4f657ebf
- Termination on corruption : ENABLED
- Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
- (k) (k) (k) (k) length blocks cont. heap
- -------------------------------------------------------------------------------------
- 0000000000060000 08000002 32576 17212 32576 430 161 6 1 0 LFH
- 0000000000010000 08008000 64 8 64 5 1 1 0 0
- 0000000008810000 08001002 1088 500 1088 15 5 2 0 0 LFH
- ...
- 0000000029fb0000 08001002 88320 67408 88320 32559 343 47 189 1b7 LFH
- External fragmentation 48 % (343 free blocks)
- 0000000029870000 08001002 512 8 512 3 1 1 0 0
- ...
- -------------------------------------------------------------------------------------
复制代码 从卦中看,commit 最大的也就是 67408k = 67M, 这和 13G 差的不是一星半点,如果你了解 NtHeap 的布局,应该知道当 分配内存 > 512k 的时候,会进入到 HEAP 的 VirtualAllocdBlocks 双向链表中,言外之意就是当你觉得内存对不上的时候,就要观察下这个链表了,即上图中的 Virt blocks 列,可以看到 handle=0000000029fb0000 的 Virt blocks=189,接下来继续下钻 handle=0000000029fb0000 这个堆。- 0:000> !heap -h 0000000029fb0000
- SEGMENT HEAP ERROR: failed to initialize the extention
- Index Address Name Debugging options enabled
- 23: 29fb0000
- Segment at 0000000029fb0000 to 000000002a7b0000 (007eb000 bytes committed)
- Segment at 0000000026070000 to 0000000026170000 (000ff000 bytes committed)
- Segment at 0000000027d10000 to 0000000027f10000 (001f7000 bytes committed)
- Segment at 00000000318a0000 to 0000000031ca0000 (00400000 bytes committed)
- Segment at 0000000044a00000 to 0000000045200000 (005f1000 bytes committed)
- Segment at 000000004ae90000 to 000000004be60000 (00efc000 bytes committed)
- Segment at 000000005b3b0000 to 000000005c380000 (00e2e000 bytes committed)
- Segment at 000000005d8c0000 to 000000005e890000 (00cf1000 bytes committed)
- Segment at 000000005c380000 to 000000005d350000 (002e7000 bytes committed)
- Flags: 08001002
- ForceFlags: 00000000
- Granularity: 16 bytes
- ...
- Virtual Alloc List: 29fb0118
- Unable to read nt!_HEAP_VIRTUAL_ALLOC_ENTRY structure at 0000000043500000
- Uncommitted ranges: 29fb00f8
复制代码 我去,卦中出现了不愿看到的 Unable to read nt!_HEAP_VIRTUAL_ALLOC_ENTRY structure at 0000000043500000,也就是说显示不出 _HEAP_VIRTUAL_ALLOC_ENTRY 结构,可以用 dt 验证一下。- 0:000> dt nt!_HEAP_VIRTUAL_ALLOC_ENTRY
- Symbol nt!_HEAP_VIRTUAL_ALLOC_ENTRY not found.
复制代码 为什么在他的机器上没记录到,可能和它生产服务器的 Windows 系统有关,这里就不细究原因,接下来的问题是: !heap 命令失效,该怎么把 VirtualAllocdBlocks 给挖出来呢?只能纯人肉了...
3. 如何人肉挖 VirtualAllocdBlocks
要想人肉挖,需要一些底层知识,比如下面三点。
VirtualAllocdBlocks 是一个记录大块内存的双向链表结构,可以用 dt nt!_HEAP 0000000029fb0000 命令从 HEAP 中找出来。- 0:000> dt nt!_HEAP 0000000029fb0000
- ntdll!_HEAP
- +0x118 VirtualAllocdBlocks : _LIST_ENTRY [ 0x00000000`43500000 - 0x00000000`32970000 ]
- +0x128 SegmentList : _LIST_ENTRY [ 0x00000000`29fb0018 - 0x00000000`5c380018 ]
- ...
- 0:000> dt _LIST_ENTRY 0000000029fb0000+0x118
- ntdll!_LIST_ENTRY
- [ 0x00000000`43500000 - 0x00000000`32970000 ]
- +0x000 Flink : 0x00000000`43500000 _LIST_ENTRY [ 0x00000000`47240000 - 0x00000000`29fb0118 ]
- +0x008 Blink : 0x00000000`32970000 _LIST_ENTRY [ 0x00000000`29fb0118 - 0x00000000`4ee90000 ]
复制代码 从卦中可以看到, VirtualAllocdBlocks 是一个拥有 Flink 和 Blink 的双向链表结构。
- _HEAP_VIRTUAL_ALLOC_ENTRY 是什么?
我们都知道 heap 的 block 512k 的块就是 _HEAP_VIRTUAL_ALLOC_ENTRY 结构,不信的话可以用 dt 导出来。- 0:016> dt nt!_HEAP_VIRTUAL_ALLOC_ENTRY
- ntdll!_HEAP_VIRTUAL_ALLOC_ENTRY
- +0x000 Entry : _LIST_ENTRY
- +0x010 ExtraStuff : _HEAP_ENTRY_EXTRA
- +0x020 CommitSize : Uint8B
- +0x028 ReserveSize : Uint8B
- +0x030 BusyBlock : _HEAP_ENTRY
复制代码 从卦中可以看到,除了真正的分配 BusyBlock 之外还有一些附属信息,比如 CommitSize , ReserveSize 等等,接下来就可以抽取 第一个节点地址 加上 +0x30 来找到这个真正的内存分配块,即 0x0000000043500000 + 0x30, 然后使用 !heap -p -a 就可以看到这个分配块的源头在哪里了。- 0:000> !heap -p -a 0x0000000043500000 + 0x30
- address 0000000043500030 found in
- _HEAP @ 29fb0000
- HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
- 0000000043500030 100100 0000 [00] 0000000043500060 1000040 - (busy VirtualAlloc)
- 775bc35b ntdll! ?? ::FNODOBFM::`string'+0x00000000000153eb
- 7fed230483b halcon!HXmalloc+0x000000000000008b
- 7fed22dd81d halcon!HXAllocRLTmp+0x000000000000265d
- 7fed22d6bd0 halcon!HXAllocTmp+0x0000000000000a80
- 7fed44a346a halcon!HCancelWait+0x000000000000007a
- 7fed2386b8f halcon!CCallHProc+0x000000000000073f
- 7fe83e3bcf6 +0x000007fe83e3bcf6
-
- 0:000> !ip2md 0x000007fe83e3bcf6
- MethodDesc: 000007fe83c39138
- Method Name: HalconDotNet.xxx
- Class: 000007fe83c6b890
- MethodTable: 000007fe83c3f300
- mdToken: 0000000006000df5
- Module: 000007fe83a7f498
- IsJitted: yes
- CodeAddr: 000007fe83e3bb90
- Transparency: Safe critical
复制代码 可以看到第一块 size= 0x1000040 byte = 16M 的内存是 HalconDotNet 分配的,接下来我们多抽几个,或者用脚本来归纳一下,发现有大量的 88M 内存占用,大体上归为两类:


最后就是把这个结果给了朋友,让朋友看下用 !ip2md 显示出来的托管方法,为什么没有释放,是不是漏了。
三: 总结
这个dump可以看出是因为对 halcon 做了一套 DotNet 版的封装上出现了一些瑕疵,这个 dump 的难点在于当 !heap 扩展命令失效的情况下,如何通过纯手工的方式把 NTHeap 剥离的明明白白。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |