前段时间有位朋友咨询说他的程序出现了非托管内存泄漏,说里面有很多的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
从卦中可知,当前内存都是Heap给吃掉了,往细处说就是10600000这个进程堆,接下来使用!heap-h10600000把堆上的segment和block都显示出来。
因为前段堆相当于堆中堆,所以我们观察下有没有开启LFH,有两种方法。
原理浅析
把程序跑起来,然后抓一个dump文件。
熟悉C++的朋友一眼就能看出会存在内存泄露的情况,因为c没有进行delete[]。
内存都去了哪里
0:000> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 70 e1292000 ( 3.518 GB) 87.95%
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
-----------------------------------------------------------------------------
其实这个涉及到了NTHeap的一些基础知识。
为了能够记录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
从中可以看到,全是这种Internel的标记,而且requestsize=41fe8=270312byte=263k,很显然我并没有做27wbyte的内存分配,那这些源自于哪里呢?
千言万语不及一张。
从代码中可以看到,我做了1w次的分配,而且len=1w,即1wbyte,高频且固定,这完全符合进入LFH堆的特性。
接下来我们验证下这个说法到底对不对?写一个测试程序,让其在NTHeap上生成大量的Internel。
案例演示
接下来将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();
}
}
从中可以清晰的看到,当Heap_Entry标记了Internel,其实是给前段堆LFH做内部存储用的,当然这里的大块内存是按有序的segment和block切分,相当于堆中堆。
WinDbg分析Internel
首先来一段C++代码,根据len参数来分配char[]数组大小。
#include "iostream"
#include
using namespace std;
extern "C"
{
_declspec(dllexport) int __stdcall InitData(int len);
}
int __stdcall InitData(int len) {
char* c = new char[len];
return 1;
}
NTHeap分配架构
文章为作者独立观点,不代表股票交易接口观点