一:背景

1. 講故事

說實話,這篇dump我本來是不準備上一篇文章來解讀的,但它有兩點深深的感動了我。

  1. 無數次的聽說用 Unity 可做遊戲開發,但百聞不如一見。

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


  1. 000000df315978a8 0 3 玉骨扇
  2. 000000df31597cd8 0 3 雲龍槍
  3. 000000df31596d88 0 3 陰風爪
  4. 000000df315967a8 0 4 雪魂絲鏈
  5. 000000df31596ad0 0 4 乙木神劍
  6. 000000df31596040 0 3 星耀冠
  7. 000000df31595328 0 3 烏金錘
  8. ...

所以說這麼好的一個dump,我得給它留下點什麼。

好了,話說回來這個緣分起於上個月有位朋友說它的程式虛擬記憶體佔用非常大,諮詢如何解決,如下圖:

先甭管是什麼問題,多抓幾個dump總不會錯的,幾經折騰後發了一個dump過來。

二: Windbg 分析

1. 到底是哪裡的洩漏

分析記憶體方面的問題,還是那句話,一分為二看一下到底是哪一塊的記憶體洩漏(託管還是非託管)。

先看一下程序總記憶體,使用 !address -summary 命令。


  1. 0:087> !address -summary
  2. --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  3. Free 458 7ffe`9e6a8000 ( 127.995 TB) 100.00%
  4. Heap 48514 1`005fd000 ( 4.006 GB) 72.51% 0.00%
  5. <unknown> 2504 0`2c6ad000 ( 710.676 MB) 12.56% 0.00%
  6. Stack 504 0`2a000000 ( 672.000 MB) 11.88% 0.00%
  7. Image 410 0`0a971000 ( 169.441 MB) 3.00% 0.00%
  8. Other 18 0`001dc000 ( 1.859 MB) 0.03% 0.00%
  9. TEB 168 0`00150000 ( 1.312 MB) 0.02% 0.00%
  10. PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00%
  11. --- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  12. MEM_PRIVATE 51581 1`5130f000 ( 5.269 GB) 95.36% 0.00%
  13. MEM_IMAGE 416 0`0aa6b000 ( 170.418 MB) 3.01% 0.00%
  14. MEM_MAPPED 122 0`05bce000 ( 91.805 MB) 1.62% 0.00%
  15. --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  16. MEM_FREE 458 7ffe`9e6a8000 ( 127.995 TB) 100.00%
  17. MEM_COMMIT 51465 1`1c741000 ( 4.445 GB) 80.45% 0.00%
  18. MEM_RESERVE 654 0`45207000 ( 1.080 GB) 19.55% 0.00%

從卦中得知 MEM_COMMIT=4.4G, 接下來再看下託管堆的記憶體佔用,可以用命令 !eeheap -gc 命令。


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

從卦中得知 GC Heap Size= 8839832 Byte = 8M,我去,才這麼點,有點開玩笑哈!!! ,很明顯這是非託管記憶體洩漏,既然方向已定,那就排查下非託管區域吧!

2. 探究非託管洩漏

按照經驗,尋找非託管洩漏,首先看下 loader 堆,很多程式往往是因為動態建立了太多程式集所致,比如經典的 Castle, XmlSerializer ,有興趣的朋友可以網上找下這方面的資料,這裡使用 !eeheap -loader 命令檢視。


  1. 0:087> !eeheap -loader
  2. --------------------------------------
  3. Jit code heap:
  4. LoaderCodeHeap: 0000000000000000(0:0) Size: 0x0 (0) bytes.
  5. Total size: Size: 0x0 (0) bytes.
  6. --------------------------------------
  7. Module Thunk heaps:
  8. Module 00007ffda5fa1000: Size: 0x0 (0) bytes.
  9. Module 00007ffd485c4148: Size: 0x0 (0) bytes.
  10. Module 00007ffda2631000: Size: 0x0 (0) bytes.
  11. Module 00007ffda5331000: Size: 0x0 (0) bytes.
  12. Module 00007ffdac621000: Size: 0x0 (0) bytes.
  13. Module 00007ffdac4e1000: Size: 0x0 (0) bytes.
  14. Module 00007ffda48b1000: Size: 0x0 (0) bytes.
  15. Module 00007ffda1791000: Size: 0x0 (0) bytes.
  16. Module 00007ffd487b1858: Size: 0x0 (0) bytes.
  17. Total size: Size: 0x0 (0) bytes.
  18. --------------------------------------
  19. Module Lookup Table heaps:
  20. Module 00007ffda5fa1000: Size: 0x0 (0) bytes.
  21. Module 00007ffd485c4148: Size: 0x0 (0) bytes.
  22. Module 00007ffda2631000: Size: 0x0 (0) bytes.
  23. Module 00007ffda5331000: Size: 0x0 (0) bytes.
  24. Module 00007ffdac621000: Size: 0x0 (0) bytes.
  25. Module 00007ffdac4e1000: Size: 0x0 (0) bytes.
  26. Module 00007ffda48b1000: Size: 0x0 (0) bytes.
  27. Module 00007ffda1791000: Size: 0x0 (0) bytes.
  28. Module 00007ffd487b1858: Size: 0x0 (0) bytes.
  29. Total size: Size: 0x0 (0) bytes.
  30. --------------------------------------
  31. Total LoaderHeap size: Size: 0x99000 (626688) bytes total, 0x2000 (8192) bytes wasted.
  32. =======================================

從輸出看: Total LoaderHeap size= 626K,看樣子這次踏空了,那就進困難模式看看 Windows NT 堆,這裡使用 !heap -s 命令。


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

從輸出資訊看:原來程式的記憶體都被 heap=000000df2e680000 給吸走了,那就深挖它吧,這裡用 !heap -stat -h 000000df2e680000 命令看一下該heap的統計資訊。


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

從輸出資訊看,這塊heap主要是被 size=2000size=58 給填滿了,畢竟他們佔比 68.76 + 24.17 = 92.93,所以挖他們很有必要,接下來用命令 !heap -flt s 2000 找出heap中所有的這些block的首地址。


  1. 0:087> !ext.heap -flt s 2000
  2. _HEAP @ df2e680000
  3. HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
  4. 000000df2e702dd0 0201 0000 [00] 000000df2e702de0 02000 - (busy)
  5. 000000df2e72c7e0 0201 0201 [00] 000000df2e72c7f0 02000 - (busy)
  6. 000000df517400c0 0201 0201 [00] 000000df517400d0 02000 - (busy)
  7. 000000df517420d0 0201 0201 [00] 000000df517420e0 02000 - (busy)
  8. 000000df517440e0 0201 0201 [00] 000000df517440f0 02000 - (busy)
  9. 000000df517460f0 0201 0201 [00] 000000df51746100 02000 - (busy)
  10. 000000df51748100 0201 0201 [00] 000000df51748110 02000 - (busy)
  11. 000000df5174a110 0201 0201 [00] 000000df5174a120 02000 - (busy)
  12. 000000df5174c120 0201 0201 [00] 000000df5174c130 02000 - (busy)
  13. 000000df5174e130 0201 0201 [00] 000000df5174e140 02000 - (busy)
  14. 000000df51750140 0201 0201 [00] 000000df51750150 02000 - (busy)
  15. ...

上面的 HEAP_ENTRY 就是block的首地址,由於這樣的block大概有 4cfd2=31.5w 個,沒法一一列出,接下來就是用 dc 去觀察這些 block 的記憶體塊內容來發現其中規律,手工肯定太麻煩了,還是得藉助下指令碼,這裡還是取前1w條檢視。


  1. function show_all_blocksize() {
  2. var output = exec("!ext.heap -flt s 58").Take(10000);
  3. for (var line of output) {
  4. var heap_entry_address = line.trim().split(' ')[0];
  5. if (heap_entry_address.indexOf("00") == -1) continue;
  6. show_heap_entry(heap_entry_address);
  7. }
  8. }
  9. function show_heap_entry(heap_entry_address) {
  10. var pageIndex = (index++);
  11. var path = ".writemem D:\\file\\"+ pageIndex + ".txt " + heap_entry_address + " L?0x58";
  12. var output = exec(path);
  13. log("pageIndex=" + pageIndex);
  14. }

執行指令碼生成到txt之後,截圖如下:

通過觀察發現,這個heap中有大量的使用者資訊,然後就拿這些資訊求證朋友了。

和朋友簡單溝通後,我也只能幫到這裡,到此結案。

三:總結

本次事故的原因是由於 C# 呼叫 Lua 後,Lua 未作合理的記憶體釋放造成的非託管洩漏,具體怎麼在程式碼層進行釋放,這個要看朋友的造化了。

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

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