乐趣区

关于.net:记一次-NET-某桌面奇侠游戏-非托管内存泄漏分析

一:背景

1. 讲故事

说实话,这篇 dump 我原本是不筹备上一篇文章来解读的,但它有两点深深的打动了我。

  1. 无数次的据说用 Unity 可做游戏开发,但百闻不如一见。
  2. 游戏中有很多金庸武侠小说才有的名字,太赏心悦目了。

000000df315978a8    0          3   玉骨扇
000000df31597cd8    0          3   云龙枪
000000df31596d88    0          3   阴风爪
000000df315967a8    0          4   雪魂丝链
000000df31596ad0    0          4   乙木神剑
000000df31596040    0          3   星耀冠
000000df31595328    0          3   乌金锤
...

所以说这么好的一个 dump,我得给它留下点什么。

好了,话说回来这个缘分起于上个月有位敌人说它的程序虚拟内存占用十分大,征询如何解决,如下图:

先甭管是什么问题,多抓几个 dump 总不会错的,几经折腾后发了一个 dump 过去。

二:Windbg 剖析

1. 到底是哪里的透露

剖析内存方面的问题,还是那句话,一分为二看一下到底是哪一块的内存透露(托管还是非托管)。

先看一下过程总内存,应用 !address -summary 命令。


0:087> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free                                    458     7ffe`9e6a8000 (127.995 TB)          100.00%
Heap                                  48514        1`005fd000 (4.006 GB)  72.51%    0.00%
<unknown>                              2504        0`2c6ad000 (710.676 MB)  12.56%    0.00%
Stack                                   504        0`2a000000 (672.000 MB)  11.88%    0.00%
Image                                   410        0`0a971000 (169.441 MB)   3.00%    0.00%
Other                                    18        0`001dc000 (1.859 MB)   0.03%    0.00%
TEB                                     168        0`00150000 (1.312 MB)   0.02%    0.00%
PEB                                       1        0`00001000 (4.000 kB)   0.00%    0.00%

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE                           51581        1`5130f000 (5.269 GB)  95.36%    0.00%
MEM_IMAGE                               416        0`0aa6b000 (170.418 MB)   3.01%    0.00%
MEM_MAPPED                              122        0`05bce000 (91.805 MB)   1.62%    0.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                458     7ffe`9e6a8000 (127.995 TB)          100.00%
MEM_COMMIT                            51465        1`1c741000 (4.445 GB)  80.45%    0.00%
MEM_RESERVE                             654        0`45207000 (1.080 GB)  19.55%    0.00%

从卦中得悉 MEM_COMMIT=4.4G, 接下来再看下托管堆的内存占用,能够用命令 !eeheap -gc 命令。


0:087> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x000000df3118dc48
generation 1 starts at 0x000000df3118b098
generation 2 starts at 0x000000df30fc1000
ephemeral segment allocation context: none
         segment             begin         allocated              size
000000df30fc0000  000000df30fc1000  000000df3178cae0  0x7cbae0(8174304)
Large object heap starts at 0x000000df40fc1000
         segment             begin         allocated              size
000000df40fc0000  000000df40fc1000  000000df410637b8  0xa27b8(665528)
Total Size:              Size: 0x86e298 (8839832) bytes.
------------------------------
GC Heap Size:            Size: 0x86e298 (8839832) bytes.

从卦中得悉 GC Heap Size= 8839832 Byte = 8M,我去,才这么点,有点开玩笑哈!!!🤣🤣🤣🤣,很显著这是非托管内存透露,既然方向已定,那就排查下非托管区域吧!

2. 探索非托管透露

依照教训,寻找非托管透露,首先看下 loader 堆,很多程序往往是因为动态创建了太多程序集所致,比方经典的 Castle, XmlSerializer,有趣味的敌人能够网上找下这方面的材料,这里应用 !eeheap -loader 命令查看。


0:087> !eeheap -loader

--------------------------------------
Jit code heap:
LoaderCodeHeap:    0000000000000000(0:0) Size: 0x0 (0) bytes.
Total size:        Size: 0x0 (0) bytes.
--------------------------------------
Module Thunk heaps:
Module 00007ffda5fa1000: Size: 0x0 (0) bytes.
Module 00007ffd485c4148: Size: 0x0 (0) bytes.
Module 00007ffda2631000: Size: 0x0 (0) bytes.
Module 00007ffda5331000: Size: 0x0 (0) bytes.
Module 00007ffdac621000: Size: 0x0 (0) bytes.
Module 00007ffdac4e1000: Size: 0x0 (0) bytes.
Module 00007ffda48b1000: Size: 0x0 (0) bytes.
Module 00007ffda1791000: Size: 0x0 (0) bytes.
Module 00007ffd487b1858: Size: 0x0 (0) bytes.
Total size:              Size: 0x0 (0) bytes.
--------------------------------------
Module Lookup Table heaps:
Module 00007ffda5fa1000: Size: 0x0 (0) bytes.
Module 00007ffd485c4148: Size: 0x0 (0) bytes.
Module 00007ffda2631000: Size: 0x0 (0) bytes.
Module 00007ffda5331000: Size: 0x0 (0) bytes.
Module 00007ffdac621000: Size: 0x0 (0) bytes.
Module 00007ffdac4e1000: Size: 0x0 (0) bytes.
Module 00007ffda48b1000: Size: 0x0 (0) bytes.
Module 00007ffda1791000: Size: 0x0 (0) bytes.
Module 00007ffd487b1858: Size: 0x0 (0) bytes.
Total size:              Size: 0x0 (0) bytes.
--------------------------------------
Total LoaderHeap size:   Size: 0x99000 (626688) bytes total, 0x2000 (8192) bytes wasted.
=======================================

从输入看: Total LoaderHeap size= 626K,看样子这次踏空了,那就进艰难模式看看 Windows NT 堆,这里应用 !heap -s 命令。


0:087> !heap -s


************************************************************************************************************************
                                              NT HEAP STATS BELOW
************************************************************************************************************************
LFH Key                   : 0xb6c37b3e3a4a189e
Termination on corruption : ENABLED
          Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                            (k)     (k)    (k)     (k) length      blocks cont. heap 
-------------------------------------------------------------------------------------
000000df2e680000 00000002 4145084 4130108 4144304   1537   775   260    1      4   LFH
000000df2e1f0000 00008000      64      4     64      2     1     1    0      0      
000000df2e830000 00001002    1860    172   1080     15     5     2    0      0   LFH
000000df2ec80000 00001002    1860    236   1080      5     7     2    0      0   LFH
000000df309e0000 00001002      60      8     60      2     1     1    0      0      
000000df30bb0000 00041002      60      8     60      5     1     1    0      0      
000000df49bd0000 00001002     840     44     60      3     3     1    0      0   LFH
000000df49b20000 00041002    1860     96   1080      8     3     2    0      0   LFH
000000df30b40000 00001002      60     20     60      9     2     1    0      0      
000000df30b30000 00001002    1860    152   1080     11     8     2    0      0   LFH
000000df4bbb0000 00001002    3904   1292   3124     49     6     3    0      0   LFH
000000df89920000 00001002    1860    372   1080     14     7     2    0      0   LFH
000000df89be0000 00001006    1860    280   1080     23     2     2    0      0   LFH
000000df56f40000 00001006   32372  26204  31592   1434    21     6    0     6b   LFH
000000df56f10000 00001006    1860    176   1080     21     3     2    0      0   LFH
000000df89ac0000 00001006    3904   2160   3124     67     4     3    0     2e   LFH
-------------------------------------------------------------------------------------

从输入信息看:原来程序的内存都被 heap=000000df2e680000 给吸走了,那就深挖它吧,这里用 !heap -stat -h 000000df2e680000 命令看一下该 heap 的统计信息。


0:087>  !ext.heap -stat -h 000000df2e680000
 heap @ 000000df2e680000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     (%) (percent of total busy bytes)
    2000 4cfd2 - 99fa4000  (68.76)
    58 9d7492 - 36201230  (24.17)
    12c 267e8 - 2d1c3e0  (1.26)
    21d1 c46 - 19f0b26  (0.72)
    4020 634 - 18dc680  (0.69)
    a0 26d00 - 1842000  (0.68)
    a 1d3ebb - 124734e  (0.51)
    10 f8d99 - f8d990  (0.43)
    6 16adae - 881214  (0.24)
    b b3508 - 7b4758  (0.22)
    7 115125 - 793803  (0.21)
    5 17b833 - 7698ff  (0.21)
    c 86027 - 6481d4  (0.18)
    9 afef9 - 62f6c1  (0.17)
    d 6a80f - 5688c3  (0.15)
    f 4f5a9 - 4a64e7  (0.13)
    e 54814 - 49f118  (0.13)
    8 8b092 - 458490  (0.12)
    13 3139b - 3a7481  (0.10)
    15 25d06 - 31a17e  (0.09)

从输入信息看,这块 heap 次要是被 size=2000size=58 给填满了,毕竟他们占比 68.76 + 24.17 = 92.93,所以挖他们很有必要,接下来用命令 !heap -flt s 2000 找出 heap 中所有的这些 block 的首地址。


0:087>  !ext.heap -flt s 2000
    _HEAP @ df2e680000
              HEAP_ENTRY Size Prev Flags            UserPtr UserSize - state
        000000df2e702dd0 0201 0000  [00]   000000df2e702de0    02000 - (busy)
        000000df2e72c7e0 0201 0201  [00]   000000df2e72c7f0    02000 - (busy)
        000000df517400c0 0201 0201  [00]   000000df517400d0    02000 - (busy)
        000000df517420d0 0201 0201  [00]   000000df517420e0    02000 - (busy)
        000000df517440e0 0201 0201  [00]   000000df517440f0    02000 - (busy)
        000000df517460f0 0201 0201  [00]   000000df51746100    02000 - (busy)
        000000df51748100 0201 0201  [00]   000000df51748110    02000 - (busy)
        000000df5174a110 0201 0201  [00]   000000df5174a120    02000 - (busy)
        000000df5174c120 0201 0201  [00]   000000df5174c130    02000 - (busy)
        000000df5174e130 0201 0201  [00]   000000df5174e140    02000 - (busy)
        000000df51750140 0201 0201  [00]   000000df51750150    02000 - (busy)
        ...

下面的 HEAP_ENTRY 就是 block 的首地址,因为这样的 block 大略有 4cfd2=31.5w 个,没法一一列出,接下来就是用 dc 去察看这些 block 的内存块内容来发现其中法则,手工必定太麻烦了,还是得借助下脚本, 这里还是取前 1w 条查看。


function show_all_blocksize() {var output = exec("!ext.heap -flt s 58").Take(10000);
    for (var line of output) {var heap_entry_address = line.trim().split(' ')[0];

        if (heap_entry_address.indexOf("00") == -1) continue;

        show_heap_entry(heap_entry_address);
    }
}

function show_heap_entry(heap_entry_address) {var pageIndex = (index++);

    var path = ".writemem D:\\file\\"+ pageIndex + ".txt" + heap_entry_address + "L?0x58";

    var output = exec(path);

    log("pageIndex=" + pageIndex);
}

执行脚本生成到 txt 之后,截图如下:

通过观察发现,这个 heap 中有大量的用户信息,而后就拿这些信息求证敌人了。

和敌人简略沟通后,我也只能帮到这里,到此结案。

三:总结

本次事变的起因是因为 C# 调用 Lua 后,Lua 未作正当的内存开释造成的非托管透露,具体怎么在代码层进行开释,这个要看敌人的造化了。

最初上一个小彩蛋,敌人太客气了。

没见过这么大的红包,我竟然收了 🤣🤣🤣,反手就给公司研发小伙伴 一人一杯下午茶,在这里对敌人说一声感激 😘😘😘

退出移动版