一:背景
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_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%ApplicationName% " /f
ECHO 已刪除註冊項
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%ApplicationName%" /v GlobalFlag /t REG_SZ /d 0x00001000 /f
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image 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 操作系統》 一書。