一:背景
1. 背景
前段时间有位朋友咨询说他的程序出现了非托管内存泄漏,说里面有很多的 HEAP_BLOCK
都被标记成了 Internal
状态,而且 size 都很大, 让我帮忙看下怎么回事? 比如下面这样。
1cbea000: 42000 . 42000 [101] - busy (41fe8) Internal 1cc2c000: 42000 . 42000 [101] - busy (41fe8) Internal 1cc6e000: 42000 . 42000 [101] - busy (41fe8) Internal 1ccb0000: 42000 . 42000 [101] - busy (41fe8) Internal 1ccf2000: 42000 . 42000 [101] - busy (41fe8) Internal 1cd34000: 42000 . 42000 [101] - busy (41fe8) Internal 1cd76000: 42000 . 42000 [101] - busy (41fe8) Internal 1cdb8000: 42000 . 42000 [101] - busy (41fe8) Internal 1cdfa000: 42000 . 42000 [101] - busy (41fe8) Internal 1ce3c000: 42000 . 42000 [101] - busy (41fe8) Internal
其实这个涉及到了 NTHeap 的一些基础知识。
二:原理浅析
1. NTHeap 分配架构图
千言万语不及一张图。
从图中可以清晰的看到,当 Heap_Entry 标记了 Internel
,其实是给 前段堆 LFH
做内部存储用的,当然这里的大块内存是按有序的 segment
和 block
切分,相当于堆中堆
。
接下来我们验证下这个说法到底对不对? 写一个测试程序,让其在 NTHeap 上生成大量的 Internel
。
2. 案例演示
首先来一段 C++ 代码,根据 len 参数来分配 char[]
数组大小。
#include "iostream" #include <Windows.h> using namespace std; extern "C" { _declspec(dllexport) int __stdcall InitData(int len); } int __stdcall InitData(int len) { char* c = new char[len]; return 1; }
熟悉 C++ 的朋友一眼就能看出会存在内存泄露的情况,因为 c 没有进行 delete[]
。
接下来将 InitData
引入到 C# 上,代码如下:
internal class Program { [DllImport("Example_16_1_7", CallingConvention = CallingConvention.StdCall)] private static extern int InitData(int len); static void Main(string[] args) { var task = Task.Factory.StartNew(() => { for (int i = 0; i < 10000; i++) { InitData(10000); Console.WriteLine($"i={i} 次操作!"); } }); Console.ReadLine(); } }
从代码中可以看到,我做了 1w 次的分配,而且 len=1w,即 1wbyte,高频且固定,这完全符合进入 LFH 堆的特性。
为了能够记录 block
是谁分配的,在注册表中配置一个 GlobalFlag
项。
SET ApplicationName=Example_16_1_6.exe REG DELETE "HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionImage File Execution Options%ApplicationName% " /f ECHO 已删除注册项 REG ADD "HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionImage File Execution Options%ApplicationName%" /v GlobalFlag /t REG_SZ /d 0x00001000 /f REG ADD "HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionImage File Execution Options%ApplicationName%" /v StackTraceDatabaseSizeInMb /t REG_DWORD /d 0x00000400 /f ECHO 已启动用户栈跟踪 PAUSE
把程序跑起来,然后抓一个 dump 文件。
三:WinDbg 分析 Internel
1. 内存都去了哪里
0:000> !address -summary --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal Free 70 e1292000 ( 3.518 GB) 87.95% <unknown> 138 c42f000 ( 196.184 MB) 39.76% 4.79% Other 11 805d000 ( 128.363 MB) 26.02% 3.13% Heap 832 6f55000 ( 111.332 MB) 22.57% 2.72% Image 280 3061000 ( 48.379 MB) 9.81% 1.18% Stack 27 900000 ( 9.000 MB) 1.82% 0.22% TEB 9 19000 ( 100.000 kB) 0.02% 0.00% PEB 1 3000 ( 12.000 kB) 0.00% 0.00% --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal MEM_FREE 70 e1292000 ( 3.518 GB) 87.95% MEM_RESERVE 94 14830000 ( 328.188 MB) 66.52% 8.01% MEM_COMMIT 1204 a52e000 ( 165.180 MB) 33.48% 4.03% 0:000> !heap -s ************************************************************************************************************************ NT HEAP STATS BELOW ************************************************************************************************************************ NtGlobalFlag enables following debugging aids for new heaps: stack back traces LFH Key : 0x38843509 Termination on corruption : ENABLED Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast (k) (k) (k) (k) length blocks cont. heap ----------------------------------------------------------------------------- 10600000 08000002 113704 107896 113492 1679 72 11 0 6 LFH 10560000 08001002 60 16 60 3 2 1 0 0 10a70000 08001002 60 16 60 2 2 1 0 0 12450000 08001002 60 4 60 0 1 1 0 0 123b0000 08041002 60 4 60 2 1 1 0 0 15ef0000 08041002 60 4 60 0 1 1 0 0 -----------------------------------------------------------------------------
从卦中可知,当前内存都是 Heap 给吃掉了,往细处说就是 10600000
这个进程堆,接下来使用 !heap -h 10600000
把堆上的 segment 和 block 都显示出来。
从图中可以看到,全是这种 Internel
的标记,而且 request size = 41fe8 = 270312 byte= 263k
,很显然我并没有做 27w byte
的内存分配,那这些源自于哪里呢?
2. 源自于哪里?
因为 前段堆
相当于堆中堆,所以我们观察下有没有开启LFH,有两种方法。
-
观察
!heap -s
命令输出的Fast heap
列是不是带有 LFH ? -
观察
HEAP
的FrontEndHeap
字段是否为 null ?
0:000> dt nt!_HEAP 10600000 ntdll!_HEAP +0x0e4 FrontEndHeap : 0x10570000 Void +0x0e8 FrontHeapLockCount : 0 ...
接下来就是怎么把 FrontEndHeap
中的信息给导出来? 你完全可以根据这个首地址一步步的导出,也可以使用强大的 heap 扩展命令 -hl
, 这里的 l
就是 LFH
的意思。
0:000> !heap -hl 10600000 LFH data region at 193a0018 (subsegment 106e4a30): 193a0038: 02808 - busy (2734) 193a2840: 02808 - busy (2734) 193a5048: 02808 - busy (2734) 193a7850: 02808 - busy (2734) 193aa058: 02808 - busy (2734) 193ac860: 02808 - busy (2734) 193af068: 02808 - busy (2734) 193b1870: 02808 - busy (2734) ... LFH data region at 1cf02018 (subsegment 10695888): 1cf02038: 02808 - busy (2734) 1cf04840: 02808 - busy (2734) 1cf07048: 02808 - busy (2734) 1cf09850: 02808 - busy (2734) 1cf0c058: 02808 - busy (2734) ...
可以看到有大量的 alloc = 02808 = 10248 byte
大小的 block ,而且还有很多的 subsegment
字样,也说明了 Internel
的组成结构,由于记录了 ust,我们就可以使用 !heap -p -a
把这个block的调用栈给找出来。
0:000> !heap -p -a 193a0038 address 193a0038 found in _HEAP @ 10600000 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state 193a0038 0501 0000 [00] 193a0050 02734 - (busy) 76f377a4 ntdll!RtlpCallInterceptRoutine+0x00000026 76ef61ef ntdll!RtlpAllocateHeapInternal+0x00050ddf 76ea53fe ntdll!RtlAllocateHeap+0x0000003e 7b81bf35 ucrtbased!heap_alloc_dbg_internal+0x00000195 7b81bd46 ucrtbased!heap_alloc_dbg+0x00000036 7b81e4ba ucrtbased!_malloc_dbg+0x0000001a 7b81edd4 ucrtbased!malloc+0x00000014 7b7621fd Example_16_1_7!InitData+0x000010ea 7b7618cc Example_16_1_7!InitData+0x000007b9 7b76185e Example_16_1_7!InitData+0x0000074b ...
三:总结
本篇主要是解析了 Internel
标记的可能来源地,没有对 LFH 做进一步的讲解,更多的 NtHeap 知识可以参考 《深入解析 Windows 操作系统》 一书。