記一次 .NET 某桌面奇俠遊戲 非託管記憶體洩漏分析

一線碼農發表於2021-09-13

一:背景

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 未作合理的記憶體釋放造成的非託管洩漏,具體怎麼在程式碼層進行釋放,這個要看朋友的造化了。

最後上一個小彩蛋,朋友太客氣了。

沒見過這麼大的紅包,我居然收了 ???,反手就給公司研發小夥伴一人一杯下午茶,在這裡對朋友說一聲感謝 ???

圖片名稱

相關文章