一:背景
1. 講故事
前段時間有位朋友找到我,說他的程序內(nèi)存會出現(xiàn)暴漲,讓我看下是怎么事情?而且還告訴我是在 Linux 環(huán)境下,說實話在Linux上分析.NET程序難度會很大,難度大的原因在于Linux上的各種開源工具主要是針對 C/C++, 和 .NET 一毛錢關(guān)系都沒有,說到底微軟在 Linux 上的調(diào)試領(lǐng)域支持度還遠遠不夠。
雖然知道分析起來難度可能會很大,但該分析還是要分析的,讓朋友抓一個 dump 過來,上 WinDbg 說話。
二:WinDbg 分析
1. 到底是哪里的泄露
只要是進程都會有內(nèi)存段的,所以分析Linux的dump一樣可以使用 !address -summary
命令來觀察。
0:000> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unknown> 1607 ffffffff`cd7a9e00 ( 16.000 EB) 100.00% 100.00%
Image 41699 0`31e57200 ( 798.340 MB) 0.00% 0.00%
--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
1247 fffffffe`1c910000 ( 16.000 EB) 100.00%
MEM_PRIVATE 42059 1`e2cf1000 ( 7.544 GB) 0.00% 0.00%
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
1247 fffffffe`1c910000 ( 16.000 EB) 100.00% 100.00%
MEM_COMMIT 42059 1`e2cf1000 ( 7.544 GB) 0.00% 0.00%
--- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal
PAGE_READWRITE 41067 1`cff54000 ( 7.249 GB) 0.00% 0.00%
PAGE_READONLY 644 0`07268000 ( 114.406 MB) 0.00% 0.00%
PAGE_EXECUTE_READ 223 0`06d1f000 ( 109.121 MB) 0.00% 0.00%
PAGE_EXECUTE_WRITECOPY 125 0`04e16000 ( 78.086 MB) 0.00% 0.00%
--- Largest Region by Usage ----------- Base Address -------- Region Size ----------
<unknown> 7ffc`78f8e000 ffff8003`86672000 ( 16.000 EB)
Image 7ff8`49102000 0`10000000 ( 256.000 MB)
這里簡單提一下,我發(fā)現(xiàn)有很多朋友搞不清楚這里的 16.000 EB
是什么意思,它其實是2的64次方,即程序的用戶態(tài)空間的尋址范圍。
從卦中的 MEM_COMMIT=7.544G
來看當前提交內(nèi)存不小,接下來用 !eeheap -gc
觀察下托管堆內(nèi)存占用。
0:000> !eeheap -gc
========================================
Number of GC Heaps: 1
----------------------------------------
generation 0 starts at 7ff688f78f10
generation 1 starts at 7ff688484e70
generation 2 starts at 7ff8f7fff000
ephemeral segment allocation context: none
Small object heap
segment begin allocated committed allocated size committed size
7ff63fffa000 7ff63fffb000 7ff64fe51d80 7ff64fe5d000 0xfe56d80 (266694016) 0xfe63000 (266743808)
7ff6772c8000 7ff6772c9000 7ff6872c2d38 7ff6872c8000 0xfff9d38 (268410168) 0x10000000 (268435456)
7ff74bffe000 7ff74bfff000 7ff75bffdfc0 7ff75bffe000 0xfffefc0 (268431296) 0x10000000 (268435456)
7ff773ffe000 7ff773fff000 7ff783ffdfc8 7ff783ffe000 0xfffefc8 (268431304) 0x10000000 (268435456)
7ff849102000 7ff849103000 7ff859101fe8 7ff859102000 0xfffefe8 (268431336) 0x10000000 (268435456)
7ff8f7ffe000 7ff8f7fff000 7ff907ffce88 7ff907ffe000 0xfffde88 (268426888) 0x10000000 (268435456)
7ff6872ca000 7ff6872cb000 7ff68a768438 7ff68aa4b000 0x349d438 (55170104) 0x3781000 (58200064)
Large object heap starts at 7ff907fff000
segment begin allocated committed allocated size committed size
7ff733ff8000 7ff733ff9000 7ff73aedd058 7ff73aefe000 0x6ee4058 (116277336) 0x6f06000 (116416512)
7ff743ffc000 7ff743ffd000 7ff744358f10 7ff744379000 0x35bf10 (3522320) 0x37d000 (3657728)
7ff7a3ffe000 7ff7a3fff000 7ff7a9d63ee0 7ff7a9d84000 0x5d64ee0 (97930976) 0x5d86000 (98066432)
7ff7bbffe000 7ff7bbfff000 7ff7c3dc1090 7ff7c3de2000 0x7dc2090 (131866768) 0x7de4000 (132005888)
7ff907ffe000 7ff907fff000 7ff90f048b30 7ff90f069000 0x7049b30 (117742384) 0x706b000 (117878784)
Pinned object heap starts at 7ff90ffff000
segment begin allocated committed allocated size committed size
7ff90fffe000 7ff90ffff000 7ff9102d15b0 7ff9102d2000 0x2d25b0 (2958768) 0x2d4000 (2965504)
------------------------------
GC Allocated Heap Size: Size: 0x7f36bca0 (2134293664) bytes.
GC Committed Heap Size: Size: 0x7f710000 (2138112000) bytes.
從卦中看當前提交內(nèi)存也僅有 2.13G
,這和 7.5G
相距甚遠,說明這是最復雜的 非托管內(nèi)存泄漏
。
2. 非托管泄露分析
作為一個.NET調(diào)試者,需要像醫(yī)生一樣盡自己最大可能救治病人,那接下來我們的研究方向在哪里呢?大家需要知道所有的內(nèi)存占用的基本盤都在 虛擬地址
上,結(jié)果一搜索,發(fā)現(xiàn)有大概 4w+ 的 dll,一個程序怎么可能會有這么多動態(tài)鏈接庫呢?截圖如下:
既然找到了可疑之處那就繼續(xù)挖吧,接下來就是要考慮這個dll是托管代碼創(chuàng)建的還是非托管代碼創(chuàng)建的,用排除法就好了,如果是托管代碼創(chuàng)建的,那就肯定屬于 Assembly 下的某一個module,可以查下加載堆看看。
0:000> !eeheap -loader
Loader Heap:
--------------------------------------
...
Module 00007ff95e265778: Size: 0x0 (0) bytes.
Module 00007ff95e2661e0: Size: 0x0 (0) bytes.
Module 00007ff95e266c48: Size: 0x0 (0) bytes.
Module 00007ff95e2676b0: Size: 0x0 (0) bytes.
Module 00007ff95e268118: Size: 0x0 (0) bytes.
Module 00007ff95e268b80: Size: 0x0 (0) bytes.
Module 00007ff95e2695e8: Size: 0x0 (0) bytes.
Total size: Size: 0x0 (0) bytes.
--------------------------------------
Total LoaderHeap size: Size: 0x4bf4b000 (1274327040) bytes total, 0x4da000 (5087232) bytes wasted.
=======================================
0:000> !dumpmodule 00007ff95e2695e8
Name: *75db8939-8b3a-4075-94ac-e9bb52acf9d1#40147-0.dll
Attributes: PEFile IsInMemory IsFileLayout
...
MetaData start address: 00007FF85BBCD330 (2068 bytes)
0:000> !DumpAssembly /d 00007ff80d71bba0
Parent Domain: 0000559009249080
Name: Unknown
ClassLoader: 00007FF80D71BC00
Module
00007ff95e2695e8 *75db8939-8b3a-4075-94ac-e9bb52acf9d1#40147-0.dll
從卦中看雖然加載堆只有 1.27G
,但它還有很多關(guān)聯(lián)的內(nèi)存,而且動態(tài)module高多4w+,接下來用 !dumpmodule -mt
觀察內(nèi)部是什么類型。
0:000> !dumpmodule -mt 00007ff95e2695e8
Name: *75db8939-8b3a-4075-94ac-e9bb52acf9d1#40147-0.dll
Attributes: PEFile IsInMemory IsFileLayout
...
Types defined in this module
MT TypeDef Name
------------------------------------------------------------------------------
00007ff95e269d80 0x02000004 Submission#0
00007ff95e269ec0 0x02000005 Submission#0+<>d__0
Types referenced in this module
MT TypeRef Name
------------------------------------------------------------------------------
00007ff92d6152a8 0x0200000c System.Object
00007ff92ead4d18 0x0200000d System.Threading.Tasks.Task`1
00007ff93133f298 0x0200000e System.Runtime.CompilerServices.IAsyncStateMachine
00007ff92d6cec90 0x0200000f System.Exception
00007ff93133f648 0x02000010 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1
0:000> !dumpmt -md 00007ff95e269ec0
...
MethodDesc Table
Entry MethodDesc JIT Name
00007FF92D620030 00007ff92d615238 JIT System.Object.Finalize()
00007FF92D620038 00007ff92d615248 PreJIT System.Object.ToString()
00007FF92D620040 00007ff92d615258 JIT System.Object.Equals(System.Object)
00007FF92D620058 00007ff92d615298 JIT System.Object.GetHashCode()
00007FF95DFFB0E0 00007ff95e269e58 JIT Submission#0+<>d__0.MoveNext()
00007FF95DFF9B70 00007ff95e269e78 NONE Submission#0+<>d__0.SetStateMachine(System.Runtime.CompilerServices.IAsyncStateMachine)
00007FF95DFF9B60 00007ff95e269e48 JIT Submission#0+<>d__0..ctor()
從卦中看也只能看到一些 Submission
為前綴的類與之相關(guān)的狀態(tài)機類,也看不出來是誰創(chuàng)建的,結(jié)果又入了困境。
3. 到底是誰作的孽
要想獲取動態(tài)程序集的創(chuàng)建事件,有一個好辦法就是用跨平臺的 dotnet-trace
,讓它捕獲程序集的加載事件即可,詳情可參考:https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/collect-details ,然后讓朋友跑 30min 看看,參考命令如下:
dotnet-trace collect -p 4108 --clrevents loader --duration 00:00:30:00
有了生成好的 dotnet_xxxx.nettrace
之后就可以用 perfview 觀察了,打開 Event視圖,搜索 AssemblyLoad
事件,截圖如下:
通過 Time MSec
的748前綴來看,這1s種能生成幾十個動態(tài)程序集,接下來右鍵選擇 Open Any Stacks
觀察是什么代碼調(diào)用的,截圖如下:
從 perfivew 的輸出看,原來是 XXXCusDis 方法內(nèi)部調(diào)用 Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.EvaluateAsync
生成了非常多的程序集。
最后就是把 CSharpScript.EvaluateAsync
告訴朋友,能不能給剔除掉做個排查?文章來源:http://www.zghlxwxcb.cn/news/detail-632821.html
三:總結(jié)
網(wǎng)上查了下 Microsoft.CodeAnalysis.CSharp.Scripting
可以用來生成C#腳本代碼,大家在用的時候小心點吧。文章來源地址http://www.zghlxwxcb.cn/news/detail-632821.html

到了這里,關(guān)于記一次 .NET某報關(guān)系統(tǒng) 非托管泄露分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!