关键词搜索

源码搜索 ×
×

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

发布2022-03-25浏览417次

详情内容

推荐教程

c# 2015 & 2017 视频教程|xin3721自学网c#入门经典视频教程icon-default.png?t=M276https://www.jxasp.com/blog

一:背景

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=2000 和 size=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 未作合理的内存释放造成的非托管泄漏,具体怎么在代码层进行释放,这个要看朋友的造化了。

最后上一个小彩蛋,朋友太客气了。

没见过这么大的红包,我居然收了 ???,反手就给公司研发小伙伴一人一杯下午茶,在这里对朋友说一声感谢 ???

相关技术文章

点击QQ咨询
开通会员
返回顶部
×
微信扫码支付
微信扫码支付
确定支付下载
请使用微信描二维码支付
×

提示信息

×

选择支付方式

  • 微信支付
  • 支付宝付款
确定支付下载