C# 內存泄漏之 Internal 關鍵詞代表什麼?

一:背景

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 做內部存儲用的,當然這裏的大塊內存是按有序的 segmentblock 切分,相當於堆中堆

接下來我們驗證下這個說法到底對不對? 寫一個測試程序,讓其在 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,有兩種方法。

  1. 觀察 !heap -s 命令輸出的 Fast heap 列是不是帶有 LFH ?

  2. 觀察 HEAPFrontEndHeap 字段是否爲 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 操作系統》 一書。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章