當(dāng)前位置:首頁(yè) > IT技術(shù) > Web編程 > 正文

記一次 .NET 某桌面奇?zhèn)b游戲 非托管內(nèi)存泄漏分析
2021-09-17 11:47:17

一:背景

1. 講故事

說(shuō)實(shí)話,這篇dump我本來(lái)是不準(zhǔn)備上一篇文章來(lái)解讀的,但它有兩點(diǎn)深深的感動(dòng)了我。

  1. 無(wú)數(shù)次的聽(tīng)說(shuō)用 Unity 可做游戲開(kāi)發(fā),但百聞不如一見(jiàn)。

  2. 游戲中有很多金庸武俠小說(shuō)才有的名字,太賞心悅目了。


000000df315978a8    0          3   玉骨扇
000000df31597cd8    0          3   云龍槍
000000df31596d88    0          3   陰風(fēng)爪
000000df315967a8    0          4   雪魂絲鏈
000000df31596ad0    0          4   乙木神劍
000000df31596040    0          3   星耀冠
000000df31595328    0          3   烏金錘
...

所以說(shuō)這么好的一個(gè)dump,我得給它留下點(diǎn)什么。

好了,話說(shuō)回來(lái)這個(gè)緣分起于上個(gè)月有位朋友說(shuō)它的程序虛擬內(nèi)存占用非常大,咨詢(xún)?nèi)绾谓鉀Q,如下圖:

記一次 .NET 某桌面奇?zhèn)b游戲 非托管內(nèi)存泄漏分析_非托管

先甭管是什么問(wèn)題,多抓幾個(gè)dump總不會(huì)錯(cuò)的,幾經(jīng)折騰后發(fā)了一個(gè)dump過(guò)來(lái)。

二: Windbg 分析

1. 到底是哪里的泄漏

分析內(nèi)存方面的問(wèn)題,還是那句話,一分為二看一下到底是哪一塊的內(nèi)存泄漏(托管還是非托管)。

先看一下進(jìn)程總內(nèi)存,使用 !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, 接下來(lái)再看下托管堆的內(nèi)存占用,可以用命令 !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,我去,才這么點(diǎn),有點(diǎn)開(kāi)玩笑哈?。?! ???????????????? ,很明顯這是非托管內(nèi)存泄漏,既然方向已定,那就排查下非托管區(qū)域吧!

2. 探究非托管泄漏

按照經(jīng)驗(yàn),尋找非托管泄漏,首先看下 loader 堆,很多程序往往是因?yàn)閯?dòng)態(tài)創(chuàng)建了太多程序集所致,比如經(jīng)典的 Castle, XmlSerializer ,有興趣的朋友可以網(wǎng)上找下這方面的資料,這里使用 !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,看樣子這次踏空了,那就進(jìn)困難模式看看 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
-------------------------------------------------------------------------------------

從輸出信息看:原來(lái)程序的內(nèi)存都被 heap=000000df2e680000 給吸走了,那就深挖它吧,這里用 !heap -stat -h 000000df2e680000 命令看一下該heap的統(tǒng)計(jì)信息。


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 給填滿(mǎn)了,畢竟他們占比 68.76 + 24.17 = 92.93,所以挖他們很有必要,接下來(lái)用命令 !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 個(gè),沒(méi)法一一列出,接下來(lái)就是用 dc 去觀察這些 block 的內(nèi)存塊內(nèi)容來(lái)發(fā)現(xiàn)其中規(guī)律,手工肯定太麻煩了,還是得借助下腳本,這里還是取前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);
}

執(zhí)行腳本生成到txt之后,截圖如下:

記一次 .NET 某桌面奇?zhèn)b游戲 非托管內(nèi)存泄漏分析_windbg_02

通過(guò)觀察發(fā)現(xiàn),這個(gè)heap中有大量的用戶(hù)信息,然后就拿這些信息求證朋友了。

記一次 .NET 某桌面奇?zhèn)b游戲 非托管內(nèi)存泄漏分析_內(nèi)存泄漏_03

和朋友簡(jiǎn)單溝通后,我也只能幫到這里,到此結(jié)案。

三:總結(jié)

本次事故的原因是由于 C# 調(diào)用 Lua 后,Lua 未作合理的內(nèi)存釋放造成的非托管泄漏,具體怎么在代碼層進(jìn)行釋放,這個(gè)要看朋友的造化了。

最后上一個(gè)小彩蛋,朋友太客氣了。

記一次 .NET 某桌面奇?zhèn)b游戲 非托管內(nèi)存泄漏分析_非托管_04

沒(méi)見(jiàn)過(guò)這么大的紅包,我居然收了,反手就給公司研發(fā)小伙伴一人一杯下午茶,在這里對(duì)朋友說(shuō)一聲感謝?

記一次 .NET 某桌面奇?zhèn)b游戲 非托管內(nèi)存泄漏分析_windbg_05
?
?
?
?

本文摘自 :https://blog.51cto.com/u

開(kāi)通會(huì)員,享受整站包年服務(wù)立即開(kāi)通 >