Windbg系列之定位内存爆高代码

  • 先写一个模拟程序,将内存拉高

    public class TestService
    {
        public List<byte[]> Datas { get; set; } = new List<byte[]>();
    
        public void Add()
        {
            for (int i = 0; i < 40000; i++)
            {
                Datas.Add(new byte[1024 * 1024]);
            }
        }
    }
    
    internal class Program
    {
        static TestService testService = new TestService();
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
            testService.Add();
            Debugger.Break();
            Console.ReadLine();
        }
    }
    
  • 运行后,内存将持续升高,待内存稳定后,就可以开始我们的分析了。

使用Visual Studio Profiler 来简单查看爆高的原因

  • 如果你拥有源码,且有VS开发环境,直接使用VS调试查看内存快照是最直接的方法

  • 打开诊断工具

  • 拍摄一个内存快照

  • 分析内存快照

    • 发现 TestService 中果然有一个 List<byte[]> 类型的成员 Datas,接下来只要查看 Datas 的所有引用即可

    • 对于自己创建的类型,可以直接在快照中点击查看所有引用

    Windbg 定位内存爆高原因

    • 但是大多数情况下,我们没有源码,或者在生产环境下无法用vs调试,可以直接对内存高的进程抓取一个dump文件,然后传回自己的电脑分析

    • 使用windbg打开dump文件之后,先用 !dumpheap -stat 查看托管内存信息

    • 随便找到一个 byte[] 类型对象,查看它的引用根,这里我们就选最后一个吧: !gcroot 015163100240

      0:011> !gcroot 015163100240
      HandleTable:
          000001474aae13e8 (strong handle)
                -> 01474b800028     System.Object[] 
                -> 01474e009de8     LargeMemLocationSample.TestService 
                -> 01474e009e00     System.Collections.Generic.List<System.Byte[]> 
                -> 014750360780     System.Byte[][] 
                -> 015163100240     System.Byte[] 
      
      Found 1 unique roots.
      
    • 可以看出,这个 byte[] 对象是 TestServcie 的一个 List<byte[]> 属性的成员如果这个时候我们有源码,可以查看 TestService 的引用,如果没有源码,继续使用windbg往下定位

    • 着重看一下这个 01474b800028 System.Object[],这是一个handle表,代码里所有的引用都是来自 handle 表,要想查找 TestService 的引用,先了解一下 AB 地址的概念

    • 接下来我们先在 Handle表中寻找内容为B地址的A地址

      0:011> !dumpobj /d 1474b800028
      Name:        System.Object[]
      MethodTable: 00007ffadff6c4d8
      EEClass:     00007ffadff6c440
      Tracked Type: false
      Size:        8184(0x1ff8) bytes
      Array:       Rank 1, Number of elements 1020, Type CLASS (Print Array)
      Fields:
      None
      0:011> s -q 01474b800028 L?0x1ff8 01474e009de8
      00000147`4b801d68  00000147`4e009de8 00000147`4e009790
      
      • !do 1474b800028 是为了查看handle表整个大小是多少
      • s -q 01474b800028 L?0x1ff8 01474e009de8 是在handle表中找b地址
        • s 表示搜索内存
        • -q 表示安静搜索
        • 01474b800028 表示搜索起始地址
        • L?0x1ff8 表示搜索的长度 将会在 [01474b800028, 01474b800028 + 1ff8] 中搜索
        • 01474e009de8 要搜索的内容
      • 可以得出 A 地址为 000001474b801d68
    • 现在我们得到了A地址,接下来就要查找所有引用A地址的地方,使用的方法也很简单、粗暴,搜索所有的内存,查看内容为 000001474b801d68 的地方,然后查看它的反汇编

      0:011> s -q 01474b800028 L?0x1ff8 01474e009de8
      00000147`4b801d68  00000147`4e009de8 00000147`4e009790
      0:011> s -b 0 L?ffffffff`ffffffff 68 1d 80 4b 47 01
      00000147`4b0d197b  68 1d 80 4b 47 01 00 00-48 8b 09 39 09 ff 15 3a  h..KG...H..9...:
      00000147`4b0d1a02  68 1d 80 4b 47 01 00 00-e8 01 e6 e8 ff 90 48 83  h..KG.........H.
      00007ffa`e003197b  68 1d 80 4b 47 01 00 00-48 8b 09 39 09 ff 15 3a  h..KG...H..9...:
      00007ffa`e0031a02  68 1d 80 4b 47 01 00 00-e8 01 e6 e8 ff 90 48 83  h..KG.........H.
      00007ffa`e00be498  68 1d 80 4b 47 01 00 00-01 00 00 00 00 00 00 00  h..KG...........
      
      • 直接从 7ffa 开头的地址看, !ip2md 00007ffae003197b 使用以上命令查找所有的引用之处
      0:011> !ip2md 00007ffa`e003197b
      MethodDesc:   00007ffae00e00d0
      Method Name:          LargeMemLocationSample.Program.Main(System.String[])
      Class:                00007ffae00cfbb8
      MethodTable:          00007ffae00e0108
      mdToken:              0000000006000001
      Module:               00007ffae00be0a0
      IsJitted:             yes
      Current CodeAddr:     00007ffae0031930
      Version History:
        ILCodeVersion:      0000000000000000
        ReJIT ID:           0
        IL Addr:            000001474b212050
           CodeAddr:           00007ffae0031930  (MinOptJitted)
           NativeCodeVersion:  0000000000000000
      Source file:  D:\files\blend\git pros\gitee\blog-codes\dotnet\DebugSamples\LargeMemLocationSample\Program.cs @ 11
      
      • 从结果来看 Program.cs 的第11行,使用了 TestService

      • 如果没有源码,可先导出module(导出一个dll)再通过 ILSpy 反编译

        0:011> !savemodule 00007ffae00be0a0 d:\test.dll
        3 sections in file
        section 0 - VA=2000, VASize=a70, FileAddr=200, FileSize=c00
        section 1 - VA=4000, VASize=630, FileAddr=e00, FileSize=800
        section 2 - VA=6000, VASize=c, FileAddr=1600, FileSize=200
        

summary

  • 对于内存爆高问题解决方案:
    • 如果有源码有vs环境,直接使用vs profiler 进行分析
    • 如果有源码没有开发环境,使用windbg分析dump文件,定位内存爆高的类型,再用源码查找引用
    • 如果没有源码,使用windbg分析dump文件,定位内存爆高的类型,再用windbg查找引用的函数,再导出dll使用ILSpy反编译查看函数源码
posted on 2025-04-23 11:56  baby-jie  阅读(60)  评论(0)    收藏  举报