C# 内存泄漏之 Internal 关键词代表什么?

打印 上一主题 下一主题

主题 897|帖子 897|积分 2695

一:背景

1. 背景

前段时间有位朋友咨询说他的程序出现了非托管内存泄漏,说里面有很多的 HEAP_BLOCK 都被标记成了 Internal 状态,而且 size 都很大, 让我帮忙看下怎么回事? 比如下面这样。
  1.         1cbea000: 42000 . 42000 [101] - busy (41fe8) Internal
  2.         1cc2c000: 42000 . 42000 [101] - busy (41fe8) Internal
  3.         1cc6e000: 42000 . 42000 [101] - busy (41fe8) Internal
  4.         1ccb0000: 42000 . 42000 [101] - busy (41fe8) Internal
  5.         1ccf2000: 42000 . 42000 [101] - busy (41fe8) Internal
  6.         1cd34000: 42000 . 42000 [101] - busy (41fe8) Internal
  7.         1cd76000: 42000 . 42000 [101] - busy (41fe8) Internal
  8.         1cdb8000: 42000 . 42000 [101] - busy (41fe8) Internal
  9.         1cdfa000: 42000 . 42000 [101] - busy (41fe8) Internal
  10.         1ce3c000: 42000 . 42000 [101] - busy (41fe8) Internal
复制代码
其实这个涉及到了 NTHeap 的一些基础知识。
二:原理浅析

1. NTHeap 分配架构图

千言万语不及一张图。

从图中可以清晰的看到,当 Heap_Entry 标记了  Internel ,其实是给 前段堆 LFH 做内部存储用的,当然这里的大块内存是按有序的 segment 和 block 切分,相当于堆中堆 。
接下来我们验证下这个说法到底对不对? 写一个测试程序,让其在 NTHeap 上生成大量的 Internel。
2. 案例演示

首先来一段 C++ 代码,根据 len 参数来分配 char[] 数组大小。
  1. #include "iostream"
  2. #include <Windows.h>
  3. using namespace std;
  4. extern "C"
  5. {
  6.         _declspec(dllexport) int  __stdcall InitData(int len);
  7. }
  8. int __stdcall InitData(int len) {
  9.         char* c = new char[len];
  10.         return 1;
  11. }
复制代码
熟悉 C++ 的朋友一眼就能看出会存在内存泄露的情况,因为 c 没有进行 delete[] 。
接下来将  InitData 引入到 C# 上,代码如下:
  1.     internal class Program
  2.     {
  3.         [DllImport("Example_16_1_7", CallingConvention = CallingConvention.StdCall)]
  4.         private static extern int InitData(int len);
  5.         static void Main(string[] args)
  6.         {
  7.             var task = Task.Factory.StartNew(() =>
  8.                {
  9.                    for (int i = 0; i < 10000; i++)
  10.                    {
  11.                        InitData(10000);
  12.                        Console.WriteLine($"i={i} 次操作!");
  13.                    }
  14.                });
  15.             Console.ReadLine();
  16.         }
  17.     }
复制代码
从代码中可以看到,我做了 1w 次的分配,而且 len=1w,即 1wbyte,高频且固定,这完全符合进入 LFH 堆的特性。
为了能够记录 block 是谁分配的,在注册表中配置一个 GlobalFlag 项。
  1. SET ApplicationName=Example_16_1_6.exe
  2. REG DELETE "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%ApplicationName% " /f
  3. ECHO 已删除注册项
  4. REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%ApplicationName%" /v GlobalFlag  /t REG_SZ  /d 0x00001000 /f
  5. REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%ApplicationName%" /v StackTraceDatabaseSizeInMb  /t REG_DWORD  /d 0x00000400 /f
  6. ECHO 已启动用户栈跟踪
  7. PAUSE
复制代码
把程序跑起来,然后抓一个 dump 文件。
三:WinDbg 分析 Internel

1. 内存都去了哪里
  1. 0:000> !address -summary
  2. --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  3. Free                                     70          e1292000 (   3.518 GB)           87.95%
  4. <unknown>                               138           c42f000 ( 196.184 MB)  39.76%    4.79%
  5. Other                                    11           805d000 ( 128.363 MB)  26.02%    3.13%
  6. Heap                                    832           6f55000 ( 111.332 MB)  22.57%    2.72%
  7. Image                                   280           3061000 (  48.379 MB)   9.81%    1.18%
  8. Stack                                    27            900000 (   9.000 MB)   1.82%    0.22%
  9. TEB                                       9             19000 ( 100.000 kB)   0.02%    0.00%
  10. PEB                                       1              3000 (  12.000 kB)   0.00%    0.00%
  11. --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  12. MEM_FREE                                 70          e1292000 (   3.518 GB)           87.95%
  13. MEM_RESERVE                              94          14830000 ( 328.188 MB)  66.52%    8.01%
  14. MEM_COMMIT                             1204           a52e000 ( 165.180 MB)  33.48%    4.03%
  15. 0:000> !heap -s
  16. ************************************************************************************************************************
  17.                                               NT HEAP STATS BELOW
  18. ************************************************************************************************************************
  19. NtGlobalFlag enables following debugging aids for new heaps:
  20.     stack back traces
  21. LFH Key                   : 0x38843509
  22. Termination on corruption : ENABLED
  23.   Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
  24.                     (k)     (k)    (k)     (k) length      blocks cont. heap
  25. -----------------------------------------------------------------------------
  26. 10600000 08000002  113704 107896 113492   1679    72    11    0      6   LFH
  27. 10560000 08001002      60     16     60      3     2     1    0      0      
  28. 10a70000 08001002      60     16     60      2     2     1    0      0      
  29. 12450000 08001002      60      4     60      0     1     1    0      0      
  30. 123b0000 08041002      60      4     60      2     1     1    0      0      
  31. 15ef0000 08041002      60      4     60      0     1     1    0      0      
  32. -----------------------------------------------------------------------------
复制代码
从卦中可知,当前内存都是 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 ?
  1. 0:000> dt nt!_HEAP 10600000
  2. ntdll!_HEAP
  3.    +0x0e4 FrontEndHeap     : 0x10570000 Void
  4.    +0x0e8 FrontHeapLockCount : 0
  5.    ...
复制代码
接下来就是怎么把 FrontEndHeap 中的信息给导出来? 你完全可以根据这个首地址一步步的导出,也可以使用强大的 heap 扩展命令 -hl , 这里的 l 就是 LFH 的意思。
  1. 0:000> !heap -hl 10600000
  2.         LFH data region at 193a0018 (subsegment 106e4a30):
  3.             193a0038: 02808 - busy (2734)
  4.             193a2840: 02808 - busy (2734)
  5.             193a5048: 02808 - busy (2734)
  6.             193a7850: 02808 - busy (2734)
  7.             193aa058: 02808 - busy (2734)
  8.             193ac860: 02808 - busy (2734)
  9.             193af068: 02808 - busy (2734)
  10.             193b1870: 02808 - busy (2734)
  11.             ...
  12.         LFH data region at 1cf02018 (subsegment 10695888):
  13.             1cf02038: 02808 - busy (2734)
  14.             1cf04840: 02808 - busy (2734)
  15.             1cf07048: 02808 - busy (2734)
  16.             1cf09850: 02808 - busy (2734)
  17.             1cf0c058: 02808 - busy (2734)
  18.             ...
复制代码
可以看到有大量的 alloc = 02808 = 10248 byte 大小的 block ,而且还有很多的 subsegment 字样,也说明了 Internel 的组成结构,由于记录了 ust,我们就可以使用 !heap -p -a 把这个block的调用栈给找出来。
  1. 0:000> !heap -p -a 193a0038
  2.     address 193a0038 found in
  3.     _HEAP @ 10600000
  4.       HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
  5.         193a0038 0501 0000  [00]   193a0050    02734 - (busy)
  6.         76f377a4 ntdll!RtlpCallInterceptRoutine+0x00000026
  7.         76ef61ef ntdll!RtlpAllocateHeapInternal+0x00050ddf
  8.         76ea53fe ntdll!RtlAllocateHeap+0x0000003e
  9.         7b81bf35 ucrtbased!heap_alloc_dbg_internal+0x00000195
  10.         7b81bd46 ucrtbased!heap_alloc_dbg+0x00000036
  11.         7b81e4ba ucrtbased!_malloc_dbg+0x0000001a
  12.         7b81edd4 ucrtbased!malloc+0x00000014
  13.         7b7621fd Example_16_1_7!InitData+0x000010ea
  14.         7b7618cc Example_16_1_7!InitData+0x000007b9
  15.         7b76185e Example_16_1_7!InitData+0x0000074b
  16.         ...
复制代码
三:总结

本篇主要是解析了 Internel 标记的可能来源地,没有对 LFH 做进一步的讲解,更多的 NtHeap 知识可以参考 《深入解析 Windows 操作系统》 一书。

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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

刘俊凯

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表