翼度科技»论坛 编程开发 .net 查看内容

记一次 .NET 某医院预约平台 非托管泄露分析

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
一:背景

1. 讲故事

前几天有位朋友找到我,说他的程序有内存泄露,让我帮忙排查一下,截图如下:

说实话看到 32bit, 1.5G 这些关键词之后,职业敏感告诉我,他这个可能是虚拟地址紧张所致,不管怎么说,有了 Dump 就可以上马分析。
二:WinDbg分析

1. 虚拟地址紧张所致吗

要看是不是虚拟地址紧张,可以用 !address -summary 观察下内存段统计信息,截图如下:

我去,用 WinDbg Preview 尽然分析不了,在加载 ntdll 的过程中死掉了,如果你是我们调试训练营的朋友,应该会深深的有体会,我们分析的第一个dump就存在这个情况,这个加载不了其实就预示着一种非托管泄露,这里暂不剧透。
用 WinDbg Preview 分析不了怎么办呢?可以用 Windbg 的其他版本哈,比如 Windbg10, WinDbg6 等等,这里就采用 WinDbg10 X86 版本打开吧。
  1. 0:000> !address -summary
  2. --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  3. Free                                    179          8cbb1000 (   2.199 GB)           54.97%
  4. Heap                                   6598          376f6000 ( 886.961 MB)  48.09%   21.65%
  5. <unknown>                              3091          31954000 ( 793.328 MB)  43.02%   19.37%
  6. Image                                   376           8c0d000 ( 140.051 MB)   7.59%    3.42%
  7. Stack                                    75           1780000 (  23.500 MB)   1.27%    0.57%
  8. Other                                     7             4e000 ( 312.000 kB)   0.02%    0.01%
  9. TEB                                      25             19000 ( 100.000 kB)   0.01%    0.00%
  10. PEB                                       1              1000 (   4.000 kB)   0.00%    0.00%
  11. --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  12. MEM_FREE                                179          8cbb1000 (   2.199 GB)           54.97%
  13. MEM_COMMIT                             9821          6bfad000 (   1.687 GB)  93.68%   42.18%
  14. MEM_RESERVE                             352           7492000 ( 116.570 MB)   6.32%    2.85%
复制代码
从卦中 MEM_COMMIT 的 %ofTotal= 42.18% 来看,提交内存占总的虚拟地址比重还不到一半,这说明我的猜测是错的,不存在虚拟地址紧张的情况,这里稍微提醒一下的是,这里不存在虚拟地址紧张是因为它开的是 Any CPU 模式,默认能吃到 4G 内存。
不管怎么说,现在被当头一棒,既然这条路走不通,那会是什么情况导致的呢?一般来说这个内存量我是不愿意分析的,但既然分析到这里也只能继续分析,接下来用 !eeheap -gc 观察下托管堆内存占用情况。
  1. 0:000> !eeheap -gc
  2. Number of GC Heaps: 1
  3. generation 0 starts at 0x777c0434
  4. generation 1 starts at 0x77781000
  5. generation 2 starts at 0x01861000
  6. ephemeral segment allocation context: none
  7. segment     begin  allocated      size
  8. 01860000  01861000  0285ffdc  0xffefdc(16773084)
  9. ...
  10. 77780000  77781000  77aa25c0  0x3215c0(3282368)
  11. Large object heap starts at 0x02861000
  12. segment     begin  allocated      size
  13. 02860000  02861000  031e5cc0  0x984cc0(9981120)
  14. Total Size:              Size: 0x1f7e47e4 (528369636) bytes.
  15. ------------------------------
  16. GC Heap Size:    Size: 0x1f7e47e4 (528369636) bytes.
复制代码
从卦中看当前托管堆也才 528M 和 提交内存 1.6G 相距甚远,所以这个 dump 大概率是存在非托管内存泄露,其实 !address -summary 中的 Heap 也能佐证,说到底就是 ntheap 泄露。
2. ntheap 怎么啦

深挖 ntheap 我就不挖了,省的误入歧途,文章开头我说过 ntdll 无法加载的现象预示着一种非托管泄露,对 ,就是 GC 的加载堆泄露,加载堆是 CLR 用来映射 C# 程序集,模块,类型,方法等用途的一块私有内存,那怎么去洞察它呢?可以使用 !eeheap -loader 命令洞察。
  1. 0:000> !eeheap -loader
  2. Loader Heap:
  3. --------------------------------------
  4. ...
  5. Module 05829f78: Size: 0x0 (0) bytes.
  6. Module 0582a8f8: Size: 0x0 (0) bytes.
  7. Module 0582b278: Size: 0x0 (0) bytes.
  8. Module 0582bbf8: Size: 0x0 (0) bytes.
  9. Module 0582c578: Size: 0x0 (0) bytes.
  10. Module 0582cef8: Size: 0x0 (0) bytes.
  11. Module 0582d878: Size: 0x0 (0) bytes.
  12. ...
  13. Module 362ea420: Size: 0x0 (0) bytes.
  14. Total size:      Size: 0x0 (0) bytes.
  15. --------------------------------------
  16. Total LoaderHeap size:   Size: 0x7e7e000 (132636672) bytes total, 0x28000 (163840) bytes wasted.
  17. =======================================
复制代码
虽然加载堆只统计到了 132M,但其中的 module 高达 2.3w 个,其实这里会有一些相关内存是加载堆之外无法统计到的,一般正常的程序不可能有这么多的module,所以这就是我们接下来突破的点,那怎么突破呢?最好的办法就是观察下这个 module 中到底有什么 type,使用 !dumpmodule 命令即可。
  1. 0:000> !dumpmodule -mt 0582d878
  2. Name:       Unknown Module
  3. Attributes: Reflection
  4. Assembly:   0c229d38
  5. LoaderHeap:              00000000
  6. TypeDefToMethodTableMap: 050676e4
  7. TypeRefToMethodTableMap: 050676f8
  8. MethodDefToDescMap:      0506770c
  9. FieldDefToDescMap:       05067734
  10. MemberRefToDescMap:      00000000
  11. FileReferencesMap:       05067784
  12. AssemblyReferencesMap:   05067798
  13. Types defined in this module
  14.       MT  TypeDef Name
  15. ------------------------------------------------------------------------------
  16. 0582dcb0 0x02000002
  17. 0582df90 0x02000003
  18. 0582e018 0x02000004
  19. 0582e0b8 0x02000005
  20. 0582e194 0x02000006
  21. Types referenced in this module
  22.       MT    TypeRef Name
  23. ------------------------------------------------------------------------------
复制代码
从模块中并没有看到类型的文字描述,那怎么办呢,我们随便抽一个 mt 看下这个 mt 下有什么方法,使用 !dumpmt 命令即可。
  1. 0:000> !dumpmt -md 0582dcb0
  2. EEClass:         05068980
  3. Module:          0582d878
  4. Name:            
  5. mdToken:         02000002
  6. File:            Unknown Module
  7. BaseSize:        0x44
  8. ComponentSize:   0x0
  9. Slots in VTable: 8
  10. Number of IFaces in IFaceMap: 0
  11. --------------------------------------
  12. MethodDesc Table
  13.    Entry MethodDe    JIT Name
  14. 739819c8 735e61fc PreJIT System.Object.ToString()
  15. 73987850 735e6204 PreJIT System.Object.Equals(System.Object)
  16. 7398bd80 735e6224 PreJIT System.Object.GetHashCode()
  17. 738ddbe8 735e6238 PreJIT System.Object.Finalize()
  18. 0583b529 0582dc8c   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCallBack.InitCallbacks()
  19. 0583b52d 0582dc94   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCallBack..ctor()
  20. 0583c7d0 0582dc74    JIT Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCallBack.Write3_root(System.Object)
  21. 0583c868 0582dc80    JIT Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCallBack.Write2_CallBack(System.String, System.String, xxx.Models.xxxBack, Boolean, Boolean)
复制代码
看到卦中的这些信息,我相信有很多朋友知道是怎么回事了,对,就是 Serialization 泄露,那它序列化什么类型呢 ? 从卦中看就是 xxx.Models.xxxBack 类,即 xmlSerializer.Serialize(xxx.Models.xxxBack) 的相关逻辑,接下来就需要逆向看下到底是哪里写的,结果发现是他的底层库封装的,有些方法有问题,有些没问题,真的是无语哈。
  1.     //有问题的方法
  2.     public static string Serialize(object o, Encoding encoding, string rootName)
  3.     {
  4.         XmlSerializer xmlSerializer = new XmlSerializer(o.GetType(), new XmlRootAttribute(rootName));
  5.         ...
  6.         xmlSerializer.Serialize(memoryStream, o, xmlSerializerNamespaces);
  7.         return encoding.GetString(memoryStream.ToArray());
  8.     }
  9.     //正确的方法
  10.     public static string Serialize(object Obj, Encoding encoding)
  11.     {
  12.         ...
  13.         using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream, xmlWriterSettings))
  14.         {
  15.             XmlSerializerNamespaces xmlSerializerNamespaces = new XmlSerializerNamespaces();
  16.             xmlSerializerNamespaces.Add("", "");
  17.             new XmlSerializer(Obj.GetType()).Serialize(xmlWriter, Obj, xmlSerializerNamespaces);
  18.         }
  19.         return encoding.GetString(memoryStream.ToArray());
  20.     }
复制代码
这是一个老生常谈的问题,如果你用 new XmlSerializer(o.GetType(), new XmlRootAttribute(rootName)); 模式的话,一定要缓存起来,否则就会泄露,只能说是微软造的一个大坑吧,多少人都踩上去了。
三:总结

在我分析的真实dump案例中,见过 Castle ProxyGenerator 的泄露,也见过 CodeAnalysis.CSharp.Scripting 的泄露,还真没见过 XmlSerializer 的泄露,算是完美的补充了我的案例库!

来源:https://www.cnblogs.com/huangxincheng/archive/2023/07/05/17528211.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

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

x

举报 回复 使用道具