本文記錄我將應(yīng)用遷移到 dotnet 6 之后,在 Win7 系統(tǒng)上,因?yàn)槭褂?HttpWebRequest 訪問一個(gè)本地服務(wù),此本地服務(wù)開啟 https 且證書鏈在此 Win7 系統(tǒng)上錯(cuò)誤,導(dǎo)致應(yīng)用內(nèi)存泄露問題。本文記錄此問題的原因以及調(diào)查過程
核心原因
核心原因是在 CRYPT32.dll 上的 CertGetCertificateChain 方法存在內(nèi)存泄露,更底層的原因未知
在 .NET 6 里,更新了 https 訪問方法邏輯,詳細(xì)請(qǐng)看?Announcing .NET 6 - The Fastest .NET Yet - .NET Blog?和?What’s new in .NET 6 Microsoft Docs
核心問題是調(diào)用進(jìn)入?ChainPal.BuildChain?時(shí),將會(huì)調(diào)用?Crypt32.CertGetCertificateChain
?方法的調(diào)用邏輯有所變更,此進(jìn)入邏輯和 .NET Framework 4.5 有所不同。準(zhǔn)確來說,此差異不是 .NET 6 與 .NET Framework 4.5 的差異,而是 .NET Framework 4.6 以及更高版本與 .NET Framework 4.5 的差異
在 .NET Framework 4.6 時(shí)引入?Switch.System.Net.DontEnableSchUseStrongCrypto
?變更是導(dǎo)致此問題的關(guān)鍵,在 .NET Framework 4.5 下,默認(rèn)是 true 的值,但是在 .NET Framework 4.6 和更高版本下都是 false 的值。這就導(dǎo)致了整體邏輯的行為差異。此邏輯差異只和 SDK 相關(guān),而和用戶端所安裝的運(yùn)行時(shí)無關(guān)
但是此差異是否一定導(dǎo)致內(nèi)存泄露,這是未知的。但內(nèi)存泄露必定走了此調(diào)用邏輯
解決方法
如 SDK 提示,使用 WebRequest.Create 等方法創(chuàng)建 HttpWebRequest 用來進(jìn)行網(wǎng)絡(luò)請(qǐng)求邏輯是一個(gè)過時(shí)的方法,應(yīng)該換用 HttpClient 等代替。經(jīng)過實(shí)際的測(cè)試,換用 HttpClient 即可完美解決內(nèi)存泄露問題,順帶提升了不少的性能
也就是說此內(nèi)存泄露從業(yè)務(wù)上說是使用了一個(gè)過時(shí)的 API 導(dǎo)致的問題
調(diào)查過程
在開始記錄調(diào)查過程之前,還請(qǐng)看一下背景
如上一篇博客?記將一個(gè)大型客戶端應(yīng)用項(xiàng)目遷移到 dotnet 6 的經(jīng)驗(yàn)和決策 - lindexi - 博客園?我在完成了遷移了此大型應(yīng)用到 dotnet 6 發(fā)布到內(nèi)測(cè)用戶端,有內(nèi)測(cè)小白鼠反饋說第二天過來就看到應(yīng)用掛掉了
一開始沒有認(rèn)為這是一個(gè)問題。等到第二個(gè)用戶反饋時(shí)才開始認(rèn)為這是一個(gè)坑,開始進(jìn)行調(diào)查
以下調(diào)試過程非新手友好,請(qǐng)新手一定不要閱讀下文,如果閱讀了也一定不要在調(diào)試內(nèi)存泄露使用下面的方法
通過分析應(yīng)用本身的日志,了解到應(yīng)用是被閃退的。詢問內(nèi)測(cè)的用戶了解到,應(yīng)用閃退的時(shí)候,都是在晚上掛機(jī)的時(shí)候,這時(shí)候沒有任何的用戶動(dòng)作。為了盡可能干掉環(huán)境問題帶來的干擾,我搭建了虛擬機(jī),使用?cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso
?安裝了純凈的系統(tǒng),再加上 KB2533623 補(bǔ)丁讓 dotnet 6 應(yīng)用跑起來,最后部署上應(yīng)用,進(jìn)行掛機(jī)
十分符合預(yù)期的,第二天應(yīng)用掛掉了,而且系統(tǒng)提示 Xx 應(yīng)用停止工作。通過 系統(tǒng)日志 可以看到存在應(yīng)用錯(cuò)誤異常,異常信息是 CLR Exception E0434352 也就是在 CLR 層面出現(xiàn)異常
我錯(cuò)誤認(rèn)為這是升級(jí)到 dotnet 6 時(shí),由于 dotnet 6 和 Win7 的兼容性導(dǎo)致的問題,開始著手根據(jù)?CLR Exception E0434352 Microsoft Docs?官方文檔的方法開始調(diào)查,然而卻沒有找到任何有用的信息
繼續(xù)掛機(jī)到第三天,我這次采用任務(wù)管理器在 Xx 應(yīng)用停止工作時(shí),對(duì)應(yīng)用抓一個(gè) DUMP 傳到我開發(fā)設(shè)備上,使用 VisualStudio 的混合調(diào)試進(jìn)行調(diào)試,此時(shí)發(fā)現(xiàn)錯(cuò)誤信息和第二天的不相同了,這次顯示的是 OutOfMemory 相關(guān)異常。但是我在 Win7 虛擬機(jī)上,使用任務(wù)管理器看到的 Xx 應(yīng)用占用的內(nèi)存實(shí)際上才 250 MB 而已,這一定是在諷刺我
好在我反應(yīng)過來,任務(wù)管理器上面看到的應(yīng)用占用 250MB 內(nèi)存,完全不等于應(yīng)用使用的內(nèi)存是 250MB 的空間。為什么呢?這是一個(gè)復(fù)雜的問題,我不想在本文這里聊 Windows 下的應(yīng)用內(nèi)存知識(shí),也許后續(xù)會(huì)另外開一篇很長(zhǎng)的博客來說明。需要了解的是,如果一個(gè)應(yīng)用 OOM 了,那除了系統(tǒng)本身給不到應(yīng)用足夠的內(nèi)存之外,還有另一個(gè)問題就是應(yīng)用本身用到了平臺(tái)限制的最大內(nèi)存數(shù)量。別忘了 x86 和 x64 的差異
剛好,此 Xx 應(yīng)用是一個(gè) x86 應(yīng)用。在通過系統(tǒng)日志了解到此 Win7 虛擬機(jī)上沒有存在一刻是內(nèi)存不足的情況,而且此純凈的虛擬機(jī)也就跑了 Xx 一個(gè)應(yīng)用,要是內(nèi)存不足,也是 Xx 應(yīng)用的鍋?;貞浺幌拢褂?x86 應(yīng)用,默認(rèn)的進(jìn)程空間是 4G 大小,其中有 1 到 2G 需要給系統(tǒng)交稅,也就是應(yīng)用在開啟大內(nèi)存感知時(shí),最大能用到 3G 的內(nèi)存。如果應(yīng)用在到達(dá) 3G 內(nèi)存占用附近時(shí),依然向系統(tǒng)申請(qǐng)內(nèi)存,那此時(shí)就 OOM 了
任務(wù)管理器說應(yīng)用占用了多少內(nèi)存,實(shí)際上如果是以上的申請(qǐng)內(nèi)存超過 x86 平臺(tái)限制的導(dǎo)致的問題,那完全必須無視任務(wù)管理器說的話。特別是在用戶端,別忘了還有?EmptyWorkingSet?這樣安慰人的方法
我通過拿到 DUMP 文件的大小,看到 DUMP 文件是接近 4G 的大小,猜測(cè)是 Xx 應(yīng)用申請(qǐng)內(nèi)存超過 x86 平臺(tái)限制。調(diào)查此問題需要用到微軟極品工具箱的?VMMap?工具
通過 vmmap 可以看到此時(shí)的應(yīng)用的 Private Data 占用達(dá)到接近 3G 的大小,因此可以定位到 Xx 應(yīng)用閃退的原因是因?yàn)樯暾?qǐng)內(nèi)存超過 x86 平臺(tái)限制
也就是說有兩個(gè)分支導(dǎo)致 Private Data 占用過多,第一個(gè)原因就是業(yè)務(wù)需要申請(qǐng)大量的內(nèi)存空間,第一個(gè)原因不算是內(nèi)存泄露問題,只能算是性能優(yōu)化問題,某個(gè)業(yè)務(wù)邏輯空間復(fù)雜度過高。第二個(gè)原因就是應(yīng)用內(nèi)存泄露,應(yīng)用不斷運(yùn)行過程中,不斷泄露內(nèi)存,運(yùn)行的時(shí)間長(zhǎng)了,自然多少內(nèi)存都不夠用
換句話說,不是所有的 OOM 問題,都是內(nèi)存泄露問題,可能還是業(yè)務(wù)需要申請(qǐng)大量的內(nèi)存空間問題。但顯然,本次遇到的問題,應(yīng)該就是內(nèi)存泄露問題了。畢竟只是掛機(jī)就讓應(yīng)用掛掉了,那大概確定是內(nèi)存泄露了。但是這只能說大概,萬一有一個(gè)定時(shí)任務(wù)是從后臺(tái)拉取某個(gè)數(shù)據(jù),剛好這個(gè)數(shù)據(jù)導(dǎo)致了某個(gè)處理業(yè)務(wù)需要申請(qǐng)大量的內(nèi)存,從而讓應(yīng)用掛掉。為了確定是哪個(gè)方式導(dǎo)致的 OOM 了,可以先使用排除的方式,如果是某個(gè)業(yè)務(wù)申請(qǐng)大量的內(nèi)存導(dǎo)致內(nèi)存泄露,這是非常好也非常方便調(diào)試出來的,只需要使用 dotMemory 工具分析一下即可
在開始使用 dotMemory 之前,還遇到一個(gè)小問題,那就是 dotMemory 不能在我的 Win7 虛擬機(jī)上運(yùn)行,而我又不想去污染此虛擬機(jī)環(huán)境。好在 dotMemory 可以分析 DUMP 文件,于是我就拿來剛才使用 任務(wù)管理器 抓的 DUMP 文件進(jìn)行分析。可惜,由于 Win7 虛擬機(jī)采用的是 X64 系統(tǒng),而應(yīng)用是 X86 應(yīng)用,導(dǎo)致任務(wù)管理器抓的 DUMP 文件無法被 dotMemory 識(shí)別,只能再次換用專業(yè)?ProcDump?工具去抓進(jìn)程的 DUMP 文件
換用?ProcDump?工具去抓應(yīng)用的 DUMP 文件用起來比任務(wù)管理器更加方便,我也推薦使用 ProcDump 去抓 DUMP 文件,這個(gè)工具是十分強(qiáng)大的,本文用到的只是很少的功能。由于這個(gè)工具太強(qiáng)大了,要介紹的話,也是另一篇博客了,本文也不會(huì)包含此工具的更多使用方法
在虛擬機(jī)上面使用?procdump -ma <PID>
?命令,這里的?<PID>
?就是要抓取的進(jìn)程的 Id 號(hào),將 Xx 應(yīng)用抓取 DUMP 文件,然后再用 7z 壓縮一下,傳回到我的開發(fā)設(shè)備上,用 dotMemory 打開分析。使用 7z 是因?yàn)榭梢院艽蟮膲嚎s DUMP 文件。通過 dotMemory 分析沒有看到有哪個(gè)業(yè)務(wù)使用了大量的內(nèi)存,總的 .NET 內(nèi)存占用實(shí)際上才不到 100MB 大小。因此大概可以確定不是因?yàn)槟硞€(gè)業(yè)務(wù)申請(qǐng)大量的內(nèi)存導(dǎo)致內(nèi)存泄露,至少不是申請(qǐng)托管內(nèi)存
繼續(xù)回到確定 OOM 導(dǎo)致的原因上,我重新運(yùn)行 Xx 應(yīng)用,通過 VMMap 工具不斷按 F5 刷新,經(jīng)過三個(gè)小時(shí)間斷追蹤,可以看到 Private Data 緩慢上漲。通過此,可以判斷是內(nèi)存泄露問題
內(nèi)存泄露通用處理方法就是先抓取泄露點(diǎn),通過泄露點(diǎn)了解泄露模塊。抓取泄露點(diǎn)的通用方法就是對(duì)比幾段時(shí)間點(diǎn),有哪些對(duì)象被創(chuàng)建且不被回收。依然是使用?ProcDump?工具抓取 DUMP 文件,然后通過 dotMemory 的導(dǎo)入 DUMP 功能,以及對(duì)比內(nèi)存功能,進(jìn)行分析
如果要是 dotMemory 可以符合預(yù)期的讓我看到業(yè)務(wù)模塊上有哪些對(duì)象沒有被釋放,那自然就不會(huì)有本文的記錄,畢竟如此簡(jiǎn)單就能解決的問題,要是還水一篇博客就太水了。通過 dotMemory 抓取可以看到不同的時(shí)間點(diǎn)上,沒有任何業(yè)務(wù)代碼的對(duì)象泄露。唯一新建的幾個(gè)對(duì)象都是 System.Net 命名空間下的,而且占用的托管內(nèi)存也特別小,這幾個(gè)對(duì)象的根引用都是 Ssl 相關(guān)的底層模塊,看起來似乎沒有問題
也如一開始的調(diào)查,泄露的部分似乎不在 .NET 托管上,而是非托管的泄露。對(duì)一個(gè)純 .NET 應(yīng)用來說,可以認(rèn)定所有的非托管泄露都是由托管導(dǎo)致的。但是可惜 Xx 應(yīng)用是一個(gè)復(fù)雜的應(yīng)用里面包含了其他團(tuán)隊(duì)寫的一點(diǎn)庫(kù)邏輯。于是先嘗試定位一下是否遷移過程,修改了部分的?C++\CLI
?邏輯導(dǎo)致的內(nèi)存泄露。定位的方法是采用二分法,也就是干掉這些引入的庫(kù)的邏輯。我重新寫了代碼,用 Fake 的方式重新實(shí)現(xiàn)了假邏輯,將所有的其他團(tuán)隊(duì)寫的非 .NET 的庫(kù)的文件都刪掉
可惜刪除了其他團(tuán)隊(duì)寫的非 .NET 的庫(kù)之后,依然存在內(nèi)存泄露。也就是說可以確定是在托管層存在內(nèi)存泄露的,此時(shí)我特別怕是遷移到 dotnet 6 導(dǎo)致的,和 Win7 的適配問題。而用 dotMemory 也無法給我?guī)砀嗟膸椭?,?dotMemory 最預(yù)期的能拿到的信息就是業(yè)務(wù)端有某些對(duì)象被泄露,可惜沒有找到任何業(yè)務(wù)端的對(duì)象泄露。那此時(shí)用 VisualStudio 是否有更多信息?不會(huì)有的,放心吧,在調(diào)試內(nèi)存泄露方面,使用 VisualStudio 和 dotMemory 的能力是完全相同的,只是 VisualStudio 的交互做的太過垃圾,完全不如 dotMemory 的交互形式。因此用 dotMemory 沒有帶來更多幫助,同理使用 VisualStudio 也不會(huì)有更多幫助
為了確定是否 dotnet 6 底層帶來的問題,我先在 dotnet 開源倉(cāng)庫(kù)?https://github.com/dotnet/runtime/?里翻 dotnet 6 的內(nèi)存相關(guān)的帖子,好在沒有找到任何有關(guān)聯(lián)的有幫助的,那就側(cè)面證明了,應(yīng)該是沒有其他人遇到了此問題,這是一個(gè)好消息。但也許不是,那就是我是第一個(gè)遇到的人。其次,由于我采用的是 dotnet 6.0.1 版本,分發(fā)給用戶端的不敢那么頭鐵用剛發(fā)布的版本,官方最新的是 dotnet 6.0.4 版本,也許在某個(gè)安全更新修復(fù)了此問題,安全更新有一些是保密的,也就是說我沒有能找到,如果強(qiáng)行去找,可以用 MVP 權(quán)限去尋找,但這個(gè)響應(yīng)速度就沒有那么快
接下來可以調(diào)查的方向如下
是否 dotnet 6 底層帶來的問題
是否 dotnet 6.0.1 帶來的問題,但在 dotnet 6.0.4 修復(fù)了
確認(rèn)是否 dotnet 6 底層帶來的問題剛好在我這個(gè)項(xiàng)目上,沒有那么麻煩。我對(duì)比測(cè)試了在 Win10 的設(shè)備上,發(fā)現(xiàn)沒有內(nèi)存泄露。剛好 Xx 應(yīng)用是從 .NET Framework 遷移過來的,現(xiàn)在改改代碼還能跑 .NET Framework 的版本,于是也就同步在出現(xiàn)問題的 Win7 上跑 .NET Framework 的版本,結(jié)果發(fā)現(xiàn)在 Win7 上使用 .NET Framework 版本沒有任何問題。于是大概可以確定,這和 dotnet 6 底層是有所關(guān)聯(lián),但不能說這是 dotnet 6 底層的鍋
接下來確定是否 dotnet 6.0.1 帶來的問題,但在 dotnet 6.0.4 修復(fù)了的問題。我在此出現(xiàn)問題的 Win7 上,使用 dotnet 6.0.4 版本代替原先的 6.0.1 版本,好在 dotnet 6 是不需要安裝的,替換文件即可。結(jié)果依然存在內(nèi)存泄露,這是一個(gè)壞消息。也就是說也許我是第一個(gè)遇到此問題的人,或者說這是一個(gè)官方也不知道的問題。我就嘗試去面向群編程,詢問了幾位大佬是否遇到過此問題,然而所有的回答都和本次遇到的不是相同的問題,且沒有一位大佬遇到 dotnet 6 底層的內(nèi)存泄露問題,這也算是好消息
回到測(cè)試 dotnet 6 底層帶來的問題上,既然對(duì)比了 .NET Framework 和 dotnet 6 兩個(gè)框架,發(fā)現(xiàn)只有在 dotnet 6 框架才出現(xiàn)問題。那可能的原因?qū)嶋H上可以分為三個(gè):
遷移 dotnet 6 過程中,與 .NET Framework 的變更導(dǎo)致的問題
由于 dotnet 6 的機(jī)制變更,與 .NET Framework 的不相同,導(dǎo)致的內(nèi)存回收策略變更的內(nèi)存泄露問題,例如之前遇到的委托問題
這就是 dotnet 6 底層與 Win7 適配的問題
由于 Xx 應(yīng)用是一個(gè)足夠復(fù)雜的大型應(yīng)用,不好定位以上的三個(gè)原因。于是采用對(duì)比測(cè)試法,先創(chuàng)建一個(gè)空白的 dotnet 6 的 WPF 應(yīng)用,在此 Win7 上運(yùn)行。十分符合預(yù)期的,沒有內(nèi)存泄露問題。這能證明,不是那么簡(jiǎn)單的 dotnet 6 的底層的問題。假如使用空的 dotnet 6 的 WPF 應(yīng)用也能存在內(nèi)存泄露,那就能快速定位是 dotnet 6 底層的問題,接下來的步驟就是看是否 WPF 的問題還是 dotnet 更底層的問題,畢竟這個(gè) WPF 是我定制的版本,改了不少的內(nèi)容
再定位是否遷移 dotnet 6 過程中,與 .NET Framework 的變更導(dǎo)致的問題,我尋找了所有的變更邏輯,逐個(gè)還原,或者使用 Fake 邏輯,干掉對(duì)應(yīng)的功能。這個(gè)過程相當(dāng)于一個(gè)二分,也就是說如果在干掉了某些功能之后,沒有出現(xiàn)內(nèi)存泄露,那就能定位內(nèi)存泄露和被干掉的功能相關(guān)。完成之后,同時(shí)構(gòu)建出 dotnet 6 和 .NET Framework 兩個(gè)版本,在此 Win7 上運(yùn)行。結(jié)果依然是 dotnet 6 版本存在內(nèi)存泄露,而 .NET Framework 版本沒有內(nèi)存泄露
這就證明了原因可能就是 由于 dotnet 6 的機(jī)制變更,與 .NET Framework 的不相同,導(dǎo)致的內(nèi)存泄露。但經(jīng)過以上的測(cè)試,不能說明一定是 內(nèi)存回收策略變更的內(nèi)存泄露問題
到這里,其實(shí)基本沒有了通用套路可以定位的方法了。除了使用二分法,使用二分法逐個(gè)模塊干掉,看干掉到哪個(gè)模塊就不存在內(nèi)存泄露問題。但在此 Xx 應(yīng)用上使用二分法是一個(gè)大工程,再加上內(nèi)存泄露的判斷是需要等待一段時(shí)間的。而不是快速就能定位出來,需要通過 VMMap 經(jīng)過一段時(shí)間,按照小時(shí)為單位,看 Private Data 的占用,才能了解到是否內(nèi)存泄露。以上的測(cè)試都是可以并行多個(gè)同時(shí)開始的,盡管每個(gè)測(cè)試都需要占用半天的時(shí)間,好在多個(gè)測(cè)試并行,以上的測(cè)試都在一天內(nèi)完成。但如果采用二分,那就意味著需要進(jìn)行串行測(cè)試,在上次沒有測(cè)試完成之前,是無法進(jìn)行下一個(gè)二分的。我就將二分作為最后的方法,繼續(xù)找找其他的方法
回顧一下,使用 .NET Framework 沒有問題,只有 dotnet 6 版本存在內(nèi)存泄露。通過 dotMemory 和 DUMP 沒有找到業(yè)務(wù)對(duì)象的內(nèi)存泄露,只有某幾個(gè) System.Net 命名空間下的對(duì)象存在,這些對(duì)象不確定是否泄露。更新了 dotnet 6.0.4 也沒有解決,也沒有搜到帖子,問了大佬們也沒有遇到相同的問題,也就是說不是 dotnet 的官方已知問題
既然看到了存在 System.Net 命名空間下的對(duì)象存在,那可以猜測(cè)是和網(wǎng)絡(luò)相關(guān)的問題,剛才的 dotnet 6 的空 WPF 測(cè)試應(yīng)用只能證明和基礎(chǔ)的 dotnet 6 無關(guān),但沒有證明和網(wǎng)絡(luò)模塊無關(guān)。繼續(xù)寫一個(gè)訪問網(wǎng)絡(luò)的 demo 項(xiàng)目,運(yùn)行發(fā)現(xiàn)沒有內(nèi)存泄露問題,看起來此內(nèi)存泄露問題也不是那么簡(jiǎn)單能復(fù)現(xiàn),一半是好消息,一半是壞消息。剛好?waterlv?大佬有空回復(fù)我了,他告訴我,內(nèi)存不會(huì)無緣無故上漲的,一定是有某些業(yè)務(wù)邏輯在跑。于是另一個(gè)方向是放棄內(nèi)存的方向,而是調(diào)查空閑的時(shí)候運(yùn)行了哪些邏輯
調(diào)查某個(gè)應(yīng)用在某段時(shí)間運(yùn)行了哪些邏輯,這是一個(gè) CPU 性能調(diào)試問題,相當(dāng)于調(diào)查一段時(shí)間內(nèi),有哪些邏輯占用了 CPU 資源。調(diào)查這個(gè)問題最好用的工具就是 dotTrace 工具了。我準(zhǔn)備在此 Win7 使用 dotTrace 工具抓 Xx 應(yīng)用的信息,可惜 dotTrace 工具無法在此 Win7 運(yùn)行,原因有兩個(gè),一個(gè)是需要 .NET Framework 4.7 的環(huán)境,另一個(gè)就是 ETW 準(zhǔn)備失敗。其中 ETW 準(zhǔn)備失敗也就無法抓取信息,于是我放棄了 dotTrace 工具
剛好 dotnet 系里面有 dotnet trace 工具,此工具可以完美在 Win7 運(yùn)行。于是我換用 dotnet trace 工具去抓取,雖然是抓取到了信息,但是 dotnet trace 工具比 dotTrace 工具還是差太遠(yuǎn)了,差距大概是一個(gè)是記事本,一個(gè)是 SublimeText 的差距,我沒有成功分析出來什么,反而又過去了一天
那換一個(gè)方式,通過 DUMP 抓取瞬時(shí)的線程調(diào)用堆棧,可以看到有很多線程存在,但是基本上都是不在運(yùn)行的線程。唯一一個(gè)看起來稍微相關(guān)的堆棧如下
> ntdll.dll!_ZwWaitForMultipleObjects@20() Unknown
KERNELBASE.dll!_WaitForMultipleObjectsEx@20() Unknown
kernel32.dll!_WaitForMultipleObjectsExImplementation@20() Unknown
kernel32.dll!_WaitForMultipleObjects@16() Unknown
winhttp.dll!HANDLE_OBJECT::IsInvalidated(void) Unknown
winhttp.dll!OutProcGetProxyForUrl(class INTERNET_SESSION_HANDLE_OBJECT *,unsigned short const *,struct WINHTTP_AUTOPROXY_OPTIONS const *,struct WINHTTP_PROXY_INFO *) Unknown
winhttp.dll!_WinHttpGetProxyForUrl@16() Unknown
cryptnet.dll!InetGetProxy(void *,void *,unsigned short const *,unsigned long,struct WINHTTP_PROXY_INFO * *) Unknown
cryptnet.dll!InetSendAuthenticatedRequestAndReceiveResponse(void *,void *,unsigned short const *,unsigned short const *,unsigned char const *,unsigned long,unsigned long,struct WINHTTP_PROXY_INFO *,struct _CRYPT_CREDENTIALS *,struct _CRYPT_RETRIEVE_AUX_INFO *) Unknown
cryptnet.dll!_InetSendReceiveUrlRequest@32() Unknown
cryptnet.dll!CInetSynchronousRetriever::RetrieveObjectByUrl(unsigned short const *,char const *,unsigned long,unsigned long,struct _CRYPT_BLOB_ARRAY *,void (**)(char const *,struct _CRYPT_BLOB_ARRAY *,void *),void * *,void *,struct _CRYPT_CREDENTIALS *,struct _CRYPT_RETRIEVE_AUX_INFO *) Unknown
cryptnet.dll!_InetRetrieveEncodedObject@40() Unknown
cryptnet.dll!CObjectRetrievalManager::RetrieveObjectByUrl(unsigned short const *,char const *,unsigned long,unsigned long,void * *,void *,struct _CRYPT_CREDENTIALS *,void *,struct _CRYPT_RETRIEVE_AUX_INFO *) Unknown
cryptnet.dll!CryptRetrieveObjectByUrlWithTimeoutThreadProc(void *) Unknown
kernel32.dll!@BaseThreadInitThunk@12() Unknown
看起來和系統(tǒng)的 cryptnet.dll 有幾毛錢關(guān)系,也許這是 Win7 一個(gè)已知的問題,也許更新了某個(gè)補(bǔ)丁能解決。到這里想要繼續(xù)就只能通過 WinDbg 了,玩 WinDbg 工具需要花太多的時(shí)間,于是我先掛著 WinDbg 在 Win7 系統(tǒng)上,拉符號(hào)文件,將我本機(jī)的符號(hào)文件夾共享給他。拉取符號(hào)和共享符號(hào)文件夾需要半天的時(shí)間,我也不能摸魚。似乎走 CPU 分析這個(gè)路是不可行的。繼續(xù)回到分析內(nèi)存的方法
繼續(xù)猜測(cè)是網(wǎng)絡(luò)相關(guān)問題,好在使用的是虛擬機(jī),我聽了?waterlv?大佬的方法,禁用了網(wǎng)卡,跑了一個(gè)晚上,沒有內(nèi)存泄露。那基本可以定位和網(wǎng)絡(luò)問題是強(qiáng)相關(guān)了。于是開啟 Fiddler 準(zhǔn)備抓數(shù)據(jù),默認(rèn)的 Fiddler 是沒有抓 Https 的請(qǐng)求的,我分為兩個(gè)階段,先抓 http 的請(qǐng)求,結(jié)果發(fā)現(xiàn) Xx 應(yīng)用沒有任何 http 請(qǐng)求。開啟 Fiddler 的抓取 https 請(qǐng)求,結(jié)果發(fā)現(xiàn)有某些請(qǐng)求發(fā)出,但是此時(shí)詭異的是 Xx 應(yīng)用不再有內(nèi)存泄露了
我根據(jù) Fiddler 抓 Https 請(qǐng)求的原理猜測(cè)是因?yàn)?Fiddler 為了抓取 Https 安裝的證書導(dǎo)致 Xx 應(yīng)用的行為和之前不同,從而沒有內(nèi)存泄露問題。于是做對(duì)比測(cè)試,關(guān)掉 Fiddler 的抓 https 功能,重啟 Xx 應(yīng)用,跑了半天,內(nèi)存泄露
大概可以定位到和證書相關(guān),繼續(xù)定位是和請(qǐng)求哪個(gè)鏈接相關(guān),從代碼里面進(jìn)行二分邏輯,從 Fiddler 里面抓到的各個(gè)請(qǐng)求的代碼,逐個(gè)干掉,終于被我定位到核心的問題所在。我的另一個(gè)本機(jī)的服務(wù)應(yīng)用,這是一個(gè)在本機(jī)開啟的進(jìn)程服務(wù),通過 Https 進(jìn)行 IPC 本機(jī)跨進(jìn)程通訊。業(yè)務(wù)模塊和這個(gè)本地服務(wù)應(yīng)用有心跳通訊,每次通訊都是內(nèi)存泄露。那為什么這個(gè)本地服務(wù)應(yīng)用的通訊會(huì)讓 Xx 應(yīng)用內(nèi)存泄露,根據(jù) Fidder 的證書問題我猜測(cè)和證書相關(guān)。重新閱讀這個(gè)服務(wù)應(yīng)用的代碼,以及請(qǐng)教了?lsj?證書相關(guān)知識(shí)點(diǎn)之后,了解到這個(gè)服務(wù)應(yīng)用,采用的證書有點(diǎn)問題,這個(gè)服務(wù)應(yīng)用的證書鏈?zhǔn)遣煌暾?,剛好在?Win7 系統(tǒng)上,證書也都沒有更新
解決的方法有幾個(gè):
換用 http 通訊,都是本機(jī)了,還用什么 https 通訊
換用 HttpClient 通訊,默認(rèn)明確拋出?
System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: PartialChain
?異常
換用 HttpClient 通訊時(shí),可以使用如下代碼忽略證書錯(cuò)誤問題,但是此方式是不受推薦的
var handler = new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = delegate { return true; }
};
var httpClient = new HttpClient(handler);
于是我將 Https 換成 Http 的方式,再次測(cè)試,跑了一段時(shí)間,沒有內(nèi)存泄露??雌饋砭褪亲C書導(dǎo)致的問題
邏輯上也是對(duì)的,一次對(duì)本機(jī)的服務(wù)應(yīng)用訪問,不需要?jiǎng)?chuàng)建任何業(yè)務(wù)端的對(duì)象,全部使用的都是 System.Net 的對(duì)象,這就是使用 dotMemory 工具失敗的原因,而且請(qǐng)求的速度也足夠快,無法讓 DUMP 抓到信息,再加上異步是沒有 DUMP 的線程堆棧,這就讓上面使用 DUMP 調(diào)試的方法掛掉。其實(shí)要是 dotTrace 能跑起來,是可以快速定位到此模塊的,可惜 dotnet trace 還是比較渣。在了解到是這個(gè)模塊的時(shí)候,我換用 PerfView 去調(diào)試 dotnet trace 抓的文件,其實(shí)依然能看到這個(gè)模塊的邏輯,可惜如果沒有了解到是這個(gè)模塊的問題時(shí),應(yīng)該是無法通過 PerfView 定位的。也就是說,實(shí)際上 dotnet trace 是具備此定位的能力的,能收集到足夠的信息,但上層的分析工具卻是渣的很,無論是 VisualStudio 還是 PerfView 工具,在界面和交互上都渣
不過說 VisualStudio 還是 PerfView 工具渣,我還是需要和 dotTrace 對(duì)比一下。和這個(gè)本地服務(wù)應(yīng)用的通訊模塊,在我的開發(fā)設(shè)備上也是相同運(yùn)行的,和在 Win7 系統(tǒng)上一樣,差別只是我的開發(fā)設(shè)備上沒有內(nèi)存泄露。但是如上文,其實(shí)只是調(diào)查某段時(shí)間的 CPU 占用,和內(nèi)存泄露沒有關(guān)系。我在開發(fā)設(shè)備上開啟 dotTrace 工具,抓了 Xx 應(yīng)用,果然迅速就看到了和這個(gè)本地服務(wù)應(yīng)用的通訊模塊的執(zhí)行邏輯。也就是說如果有 dotTrace 工具一開始就能跑起來,應(yīng)該可以半天內(nèi)搞定
噴完了 VisualStudio 工具渣,剛好此時(shí) WinDbg 的符號(hào)也下載完成了,可以繼續(xù)調(diào)查更底層的邏輯,依然從內(nèi)存的角度調(diào)查。在 VMMap 工具上,通過 Private Data 的數(shù)據(jù)可以看到堆上有很多大小相同的數(shù)據(jù),根據(jù) Win32 內(nèi)存調(diào)試的套路,基本上可以確定這就是某個(gè)相同的模塊申請(qǐng)的,而且也沒有釋放
為了確定是哪個(gè)模塊申請(qǐng)了某個(gè)非托管內(nèi)存,我使用了 gflags 工具的輔助,這個(gè)工具就放在 WinDbg 所在的文件夾里面,在命令行執(zhí)行下面命令,執(zhí)行的時(shí)候?qū)?huì)提示管理員權(quán)限,執(zhí)行完成之后是不會(huì)有任何界面的
gflags.exe /i Xx.exe +ust
使用以上命令,即可讓 gflags 輔助抓取 Xx 應(yīng)用的內(nèi)存申請(qǐng)的調(diào)用堆棧。以上命令的?Xx.exe
?是不需要也不能使用絕對(duì)路徑的,只是一個(gè)進(jìn)程的文件名即可,因?yàn)閷?shí)際上的抓取邏輯還是在 WinDbg 下執(zhí)行。詳細(xì)請(qǐng)看?官方文檔
接下來是將 Xx 應(yīng)用跑起來,由于 Xx 應(yīng)用是在空閑的時(shí)候,沒有用戶交互,就出現(xiàn)內(nèi)存泄露,為了減少 WinDbg 的復(fù)雜調(diào)試,我在應(yīng)用跑起來,啟動(dòng)完成,才使用 WinDbg 附加調(diào)試
盡管知道是某個(gè)大小的數(shù)據(jù)占用了 Private Data 內(nèi)存,但我對(duì) VMMap 工具不夠熟悉,不敢作為結(jié)果使用,但是可以作為方向。我重新通過 WinDbg 定位是否某個(gè)模塊申請(qǐng)了內(nèi)存沒有釋放,步驟就是先找到哪個(gè)內(nèi)存在變更,對(duì)應(yīng)的堆里面的內(nèi)容,是否某個(gè)大小的數(shù)據(jù)是在不斷泄露的,這些大小的數(shù)據(jù)的申請(qǐng)的調(diào)用堆棧是什么
先通過?!heap -s
?命令多次執(zhí)行,了解是那個(gè)內(nèi)存在變更
按照慣例是執(zhí)行至少兩次進(jìn)行對(duì)比,對(duì)于大型應(yīng)用,基本上都推薦是三次以上。不過我通過 VMMap 工具大概了解到方向了,于是就只使用三次。首次執(zhí)行的命令和輸出如下
0:024> !heap -s
LFH Key : 0x5327c840
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
00420000 00000002 48768 43096 48768 1929 715 16 0 3 LFH
006b0000 00001002 1088 680 1088 8 21 2 0 0 LFH
00e30000 00001002 256 204 256 2 21 1 0 0 LFH
00df0000 00041002 256 4 256 2 1 1 0 0
01170000 00001002 1088 196 1088 16 8 2 0 0 LFH
05970000 00041002 256 4 256 2 1 1 0 0
05920000 00001002 256 160 256 3 7 1 0 0 LFH
083a0000 00001002 256 172 256 118 3 1 0 0
0b240000 00001002 256 168 256 5 10 1 0 0 LFH
0a3f0000 00041002 256 16 256 5 1 1 0 0
0e510000 00011002 256 12 256 9 6 1 0 0
0ec10000 00001002 256 148 256 6 5 1 0 0 LFH
0ee20000 00001002 256 256 256 111 11 1 0 0 LFH
0ed10000 00001002 64 52 64 7 3 1 0 0
0f990000 00001002 256 4 256 1 2 1 0 0
0fdb0000 00001002 12096 4048 12096 2601 32 8 0 0 LFH
External fragmentation 64 % (32 free blocks)
08700000 00001002 64 4 64 2 1 1 0 0
-----------------------------------------------------------------------------
在 WinDbg 按下 g 命令讓應(yīng)用繼續(xù)運(yùn)行一段時(shí)間
0:024> g
(7c0.1874): CLR exception - code e0434352 (first chance)
(7c0.1874): CLR exception - code e0434352 (first chance)
(7c0.e64): Break instruction exception - code 80000003 (first chance)
eax=fff9c000 ebx=00000000 ecx=00000000 edx=7743f7ea esi=00000000 edi=00000000
eip=773b000c esp=0a5efe4c ebp=0a5efe78 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!DbgBreakPoint:
773b000c cc int 3
可以看到存在一些 CLR 異常,這就是本文開頭所抓到的 CLR 異常的部分,但不是相同的異常信息。這些是可以忽略的,而且我也大概定位到方向,加上前幾天也嘗試定位了 CLR 異常沒有收獲,就沒有繼續(xù)定位
讓 Xx 應(yīng)用跑了一段時(shí)間,在 WinDbg 工具按下暫停,繼續(xù)執(zhí)行?!heap -s
?命令
0:007> !heap -s
LFH Key : 0x5327c840
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
00420000 00000002 81152 67244 81152 1992 723 18 0 3 LFH
006b0000 00001002 1088 680 1088 8 22 2 0 0 LFH
00e30000 00001002 256 204 256 2 21 1 0 0 LFH
00df0000 00041002 256 4 256 2 1 1 0 0
01170000 00001002 1088 196 1088 16 9 2 0 0 LFH
05970000 00041002 256 4 256 2 1 1 0 0
05920000 00001002 256 160 256 3 7 1 0 0 LFH
083a0000 00001002 256 172 256 118 3 1 0 0
0b240000 00001002 256 168 256 5 10 1 0 0 LFH
0a3f0000 00041002 256 16 256 5 1 1 0 0
0e510000 00011002 256 12 256 9 6 1 0 0
0ec10000 00001002 256 148 256 6 5 1 0 0 LFH
0ee20000 00001002 256 256 256 111 11 1 0 0 LFH
0ed10000 00001002 64 52 64 7 3 1 0 0
0f990000 00001002 256 4 256 1 2 1 0 0
0fdb0000 00001002 12096 4048 12096 2601 32 8 0 0 LFH
External fragmentation 64 % (32 free blocks)
08700000 00001002 64 4 64 2 1 1 0 0
-----------------------------------------------------------------------------
大概可以看到?00420000
?的大小從?48768
?到?81152
?的大小
使用?!heap -stat -h 00420000
?了解這個(gè)內(nèi)存里面的數(shù)據(jù)分布情況
0:007> !heap -stat -h 00420000
heap @ 00420000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)
27994 71 - 117aa54 (37.88)
269f8 6f - 10bf288 (36.29)
fdcc 67 - 661d14 (13.83)
10 7560 - 75600 (0.99)
1c 2fec - 53dd0 (0.71)
49a9c 1 - 49a9c (0.62)
390 e3 - 328b0 (0.43)
711 68 - 2dee8 (0.39)
284 108 - 29820 (0.35)
618 64 - 26160 (0.32)
40 934 - 24d00 (0.31)
20 11f8 - 23f00 (0.30)
70 49e - 20520 (0.27)
50 639 - 1f1d0 (0.26)
60 4b2 - 1c2c0 (0.24)
dce0 2 - 1b9c0 (0.23)
84 2d7 - 176dc (0.20)
15f13 1 - 15f13 (0.19)
15eee 1 - 15eee (0.19)
30 6c5 - 144f0 (0.17)
可以看到大小為?27994
?的數(shù)據(jù)有 0x71 個(gè),而大小為?269f8
?的數(shù)據(jù)有 0x6f 個(gè)。其實(shí)這兩個(gè)不能說明問題,繼續(xù)讓 Xx 應(yīng)用執(zhí)行一段時(shí)間,再輸入?!heap -s
?命令
0:019> !heap -s
LFH Key : 0x5327c840
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
00420000 00000002 97344 91356 97344 2082 730 19 0 3 LFH
006b0000 00001002 1088 680 1088 9 22 2 0 0 LFH
00e30000 00001002 256 204 256 2 21 1 0 0 LFH
00df0000 00041002 256 4 256 2 1 1 0 0
01170000 00001002 1088 196 1088 17 9 2 0 0 LFH
05970000 00041002 256 4 256 2 1 1 0 0
05920000 00001002 256 160 256 3 7 1 0 0 LFH
083a0000 00001002 256 172 256 118 3 1 0 0
0b240000 00001002 256 172 256 5 11 1 0 0 LFH
0a3f0000 00041002 256 16 256 5 1 1 0 0
0e510000 00011002 256 12 256 9 6 1 0 0
0ec10000 00001002 256 148 256 6 5 1 0 0 LFH
0ee20000 00001002 256 256 256 111 11 1 0 0 LFH
0ed10000 00001002 64 52 64 7 3 1 0 0
0f990000 00001002 256 4 256 1 2 1 0 0
0fdb0000 00001002 12096 4048 12096 2601 32 8 0 0 LFH
External fragmentation 64 % (32 free blocks)
08700000 00001002 64 4 64 2 1 1 0 0
-----------------------------------------------------------------------------
可以看到 00420000 占用的內(nèi)存更加多了,使用?!heap -stat -h 00420000
?查看
0:019> !heap -stat -h 00420000
heap @ 00420000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)
27994 b1 - 1b60f54 (39.25)
269f8 af - 1a67088 (37.85)
fdcc a6 - a49248 (14.75)
10 757a - 757a0 (0.66)
1c 2ff4 - 53eb0 (0.47)
49a9c 1 - 49a9c (0.41)
711 97 - 42b07 (0.37)
618 86 - 33090 (0.29)
390 e3 - 328b0 (0.28)
284 108 - 29820 (0.23)
40 935 - 24d40 (0.21)
20 1236 - 246c0 (0.20)
70 4a2 - 206e0 (0.18)
50 63a - 1f220 (0.17)
60 4b2 - 1c2c0 (0.16)
dce0 2 - 1b9c0 (0.15)
84 2d7 - 176dc (0.13)
15f13 1 - 15f13 (0.12)
15eee 1 - 15eee (0.12)
30 6c5 - 144f0 (0.11)
可以看到前面兩個(gè)變更了,也就是大小為?27994
?的數(shù)據(jù)和大小為?269f8
?的數(shù)據(jù)的數(shù)量變更了
原先:
27994 71 - 117aa54 (37.88)
269f8 6f - 10bf288 (36.29)
當(dāng)前:
27994 b1 - 1b60f54 (39.25)
269f8 af - 1a67088 (37.85)
也就是說大小 Size 為 27994 的存在很多重復(fù)項(xiàng)
接下來就是獲取到這些被分配內(nèi)存的地址,使用命令?!heap -flt s 27994
?過濾其它的內(nèi)存塊,只顯示大小為 27994 的內(nèi)存塊信息
0:019> !heap -flt s 27994
_HEAP @ 420000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
05fd2880 4f34 0000 [00] 05fd2888 27994 - (busy)
06020c20 4f34 4f34 [00] 06020c28 27994 - (busy)
0614cc18 4f34 4f34 [00] 0614cc20 27994 - (busy)
08a719d0 4f34 4f34 [00] 08a719d8 27994 - (busy)
08b05028 4f34 4f34 [00] 08b05030 27994 - (busy)
08b9e4f0 4f34 4f34 [00] 08b9e4f8 27994 - (busy)
.....
0b493108 4f34 4f34 [00] 0b493110 27994 - (busy)
.....
0b366408 4f34 4f34 [00] 106b9378 27994 - (busy)
.....
1e2abff8 4f34 4f34 [00] 1e2ac000 27994 - (busy)
1e31a178 4f34 4f34 [00] 1fa93750 27994 - (busy)
1e3782f0 4f34 4f34 [00] 1e3782f8 27994 - (busy)
1e3d6468 4f34 4f34 [00] 2004dc80 27994 - (busy)
_HEAP @ 6b0000
_HEAP @ e30000
_HEAP @ df0000
_HEAP @ 1170000
_HEAP @ 5970000
_HEAP @ 5920000
_HEAP @ 83a0000
_HEAP @ b240000
_HEAP @ a3f0000
_HEAP @ e510000
_HEAP @ ec10000
_HEAP @ ee20000
_HEAP @ ed10000
_HEAP @ f990000
_HEAP @ fdb0000
_HEAP @ 8700000
輸出的內(nèi)容太多了,我忽略了一些信息
剛才開啟了 GFlags 工具,可以通過?!heap -p -a <UserPtr>
?了解內(nèi)存塊的申請(qǐng)調(diào)用堆棧,也就是哪個(gè)模塊申請(qǐng)的內(nèi)存。此命令的?<UserPtr>
?請(qǐng)?zhí)鎿Q為 UserPtr 這一列的內(nèi)存地址。需要抓幾個(gè)內(nèi)存塊地址來進(jìn)行統(tǒng)計(jì)才能了解是哪個(gè)模塊申請(qǐng)而且泄露的
我先抓取了 2004dc80 地址的信息
!heap -p -a 2004dc80
address 2004dc80 found in
_HEAP @ 490000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
2004dc68 4f36 0000 [00] 2004dc80 27994 - (busy)
7741df42 ntdll!RtlAllocateHeap+0x00000274
76874ec3 KERNELBASE!LocalAlloc+0x0000005f
76424b84 CRYPT32!PkiAlloc+0x00000032
764516b3 CRYPT32!ChainCreateCyclicPathObject+0x000000b8
764515c7 CRYPT32!ExtractEncodedCtlFromCab+0x000001b0
7645142c CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x00000041
764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
76437934 CRYPT32!ChainCreatePathObject+0x0000005e
76437da9 CRYPT32!CCertIssuerList::AddIssuer+0x0000006c
764387ac CRYPT32!CChainPathObject::FindAndAddIssuersFromStoreByMatchType+0x0000018b
764386bd CRYPT32!CChainPathObject::FindAndAddIssuersByMatchType+0x00000096
7643bbc6 CRYPT32!CChainPathObject::FindAndAddIssuers+0x00000063
764697e0 CRYPT32!CChainPathObject::CChainPathObject+0x0000035b
76437934 CRYPT32!ChainCreatePathObject+0x0000005e
76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
76436d42 CRYPT32!CertGetCertificateChain+0x00000072
然后再選中間的 1fa93750 地址
0:042> !heap -p -a 1fa93750
address 1fa93750 found in
_HEAP @ 490000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
1fa93738 4f36 0000 [00] 1fa93750 27994 - (busy)
7741df42 ntdll!RtlAllocateHeap+0x00000274
76874ec3 KERNELBASE!LocalAlloc+0x0000005f
76424b84 CRYPT32!PkiAlloc+0x00000032
764516b3 CRYPT32!ChainCreateCyclicPathObject+0x000000b8
764515c7 CRYPT32!ExtractEncodedCtlFromCab+0x000001b0
7645142c CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x00000041
764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
76437934 CRYPT32!ChainCreatePathObject+0x0000005e
76437da9 CRYPT32!CCertIssuerList::AddIssuer+0x0000006c
764387ac CRYPT32!CChainPathObject::FindAndAddIssuersFromStoreByMatchType+0x0000018b
764386bd CRYPT32!CChainPathObject::FindAndAddIssuersByMatchType+0x00000096
7643bbc6 CRYPT32!CChainPathObject::FindAndAddIssuers+0x00000063
764697e0 CRYPT32!CChainPathObject::CChainPathObject+0x0000035b
76437934 CRYPT32!ChainCreatePathObject+0x0000005e
76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
76436d42 CRYPT32!CertGetCertificateChain+0x00000072
最后選了比較前面的地址
0:042> !heap -p -a 106b9378
address 106b9378 found in
_HEAP @ 490000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
106b9360 4f36 0000 [00] 106b9378 27994 - (busy)
7741df42 ntdll!RtlAllocateHeap+0x00000274
76874ec3 KERNELBASE!LocalAlloc+0x0000005f
76424b84 CRYPT32!PkiAlloc+0x00000032
764516b3 CRYPT32!ChainCreateCyclicPathObject+0x000000b8
764515c7 CRYPT32!ExtractEncodedCtlFromCab+0x000001b0
7645142c CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x00000041
764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
76437934 CRYPT32!ChainCreatePathObject+0x0000005e
76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
76436d42 CRYPT32!CertGetCertificateChain+0x00000072
可以看到都是 CRYPT32.dll 的 CertGetCertificateChain 函數(shù)申請(qǐng)的,對(duì)比剛才的 DUMP 抓到的線程調(diào)用堆棧,似乎 CRYPT32.dll 這個(gè)系統(tǒng)組件就是有鍋的。而且 CRYPT32.dll 就是處理證書相關(guān)的邏輯。 通過官方文檔了解到 CertGetCertificateChain 就是證書鏈相關(guān)邏輯
根據(jù)上文使用二分調(diào)試到的,和本地服務(wù)應(yīng)用的通訊模塊的證書鏈在 Win7 系統(tǒng)上損壞導(dǎo)致的內(nèi)存泄露?,F(xiàn)在根據(jù) WinDbg 可以看到是 CertGetCertificateChain 處理證書鏈申請(qǐng)的內(nèi)存沒有釋放,那就證明一定是證書鏈的問題
剛才通過 WinDbg 抓到的內(nèi)存變更的內(nèi)存塊大小有兩個(gè),接下來再看 269f8 大小的內(nèi)存塊的地址
0:042> !heap -flt s 269f8
_HEAP @ 490000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
084e4400 4d42 0000 [00] 084e4418 269f8 - (busy)
0b810470 4d42 4d42 [00] 0b810488 269f8 - (busy)
0b8cb7e8 4d42 4d42 [00] 0b8cb800 269f8 - (busy)
0b90b900 4d42 4d42 [00] 0b90b918 269f8 - (busy)
0b96b990 4d42 4d42 [00] 0b96b9a8 269f8 - (busy)
0b9cba20 4d42 4d42 [00] 0b9cba38 269f8 - (busy)
0ba3f108 4d42 4d42 [00] 0ba3f120 269f8 - (busy)
105650b8 4d42 4d42 [00] 105650d0 269f8 - (busy)
10692950 4d42 4d42 [00] 10692968 269f8 - (busy)
10754ec0 4d42 4d42 [00] 10754ed8 269f8 - (busy)
107f2630 4d42 4d42 [00] 107f2648 269f8 - (busy)
10c28f90 4d42 4d42 [00] 10c28fa8 269f8 - (busy)
10c8d038 4d42 4d42 [00] 10c8d050 269f8 - (busy)
10cc4670 4d42 4d42 [00] 10cc4688 269f8 - (busy)
10e0dbd0 4d42 4d42 [00] 10e0dbe8 269f8 - (busy)
10e5bf90 4d42 4d42 [00] 10e5bfa8 269f8 - (busy)
.....
201783a8 4d42 4d42 [00] 201783c0 269f8 - (busy)
201ff188 4d42 4d42 [00] 201ff1a0 269f8 - (busy)
2025d330 4d42 4d42 [00] 2025d348 269f8 - (busy)
20329698 4d42 4d42 [00] 203296b0 269f8 - (busy)
_HEAP @ 760000
_HEAP @ a20000
_HEAP @ ec0000
_HEAP @ 1060000
_HEAP @ 4e50000
_HEAP @ 1010000
_HEAP @ bd10000
_HEAP @ e5c0000
_HEAP @ e7f0000
_HEAP @ 11900000
_HEAP @ 11c10000
_HEAP @ 12030000
_HEAP @ 12750000
_HEAP @ 12880000
_HEAP @ 13410000
_HEAP @ 1a2b0000
先隨意選擇 201ff1a0 內(nèi)存地址,通過?!heap -p -a 201ff1a0
?了解是哪個(gè)模塊申請(qǐng)
0:042> !heap -p -a 201ff1a0
address 201ff1a0 found in
_HEAP @ 490000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
201ff188 4d42 0000 [00] 201ff1a0 269f8 - (busy)
7741df42 ntdll!RtlAllocateHeap+0x00000274
76874ec3 KERNELBASE!LocalAlloc+0x0000005f
76424b84 CRYPT32!PkiAlloc+0x00000032
76447489 CRYPT32!ICM_GetListSignedData+0x000000fa
76447299 CRYPT32!ICM_UpdateDecodingSignedData+0x0000006d
764475cc CRYPT32!CryptMsgUpdate+0x000001e0
764464c4 CRYPT32!FastCreateCtlElement+0x00000221
76446252 CRYPT32!CertCreateContext+0x000000f1
76451464 CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x000000b0
764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
76437934 CRYPT32!ChainCreatePathObject+0x0000005e
76437da9 CRYPT32!CCertIssuerList::AddIssuer+0x0000006c
764387ac CRYPT32!CChainPathObject::FindAndAddIssuersFromStoreByMatchType+0x0000018b
764386bd CRYPT32!CChainPathObject::FindAndAddIssuersByMatchType+0x00000096
7643bbc6 CRYPT32!CChainPathObject::FindAndAddIssuers+0x00000063
764697e0 CRYPT32!CChainPathObject::CChainPathObject+0x0000035b
76437934 CRYPT32!ChainCreatePathObject+0x0000005e
76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
76436d42 CRYPT32!CertGetCertificateChain+0x00000072
依然是 CertGetCertificateChain 申請(qǐng)的,這是一個(gè)利好消息。繼續(xù)再隨意找了 10e0dbe8 地址,通過?!heap -p -a 10e0dbe8
?了解是哪個(gè)模塊申請(qǐng)
0:042> !heap -p -a 10e0dbe8
address 10e0dbe8 found in
_HEAP @ 490000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
10e0dbd0 4d42 0000 [00] 10e0dbe8 269f8 - (busy)
7741df42 ntdll!RtlAllocateHeap+0x00000274
76874ec3 KERNELBASE!LocalAlloc+0x0000005f
76424b84 CRYPT32!PkiAlloc+0x00000032
76447489 CRYPT32!ICM_GetListSignedData+0x000000fa
76447299 CRYPT32!ICM_UpdateDecodingSignedData+0x0000006d
764475cc CRYPT32!CryptMsgUpdate+0x000001e0
764464c4 CRYPT32!FastCreateCtlElement+0x00000221
76446252 CRYPT32!CertCreateContext+0x000000f1
76451464 CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x000000b0
764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
76437934 CRYPT32!ChainCreatePathObject+0x0000005e
76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
76436d42 CRYPT32!CertGetCertificateChain+0x00000072
可以看到依然是 CertGetCertificateChain 申請(qǐng)的
現(xiàn)在可以完全證明內(nèi)存泄露問題是證書鏈損壞導(dǎo)致 CertGetCertificateChain 內(nèi)存泄露
但是無法確定 CertGetCertificateChain 內(nèi)存泄露的更底層原因,也無法確定這是否是 Win7 這個(gè)版本存在的問題,是否安裝了補(bǔ)丁可以修復(fù),還是因?yàn)?dotnet 6 調(diào)用的問題。我嘗試去搜以上的堆棧,找到了 2013 的帖子?IE crashes due to SSL certificate check - Problem with MSVCR80.dll, - Microsoft Community
看起來和上面說的是相同的一個(gè)問題,我預(yù)計(jì)是有補(bǔ)丁可以解決。而且讓 Win7 修復(fù)證書預(yù)計(jì)也能解決此問題
繼續(xù)調(diào)查是否因?yàn)?dotnet 6 調(diào)用的問題,從 WinDbg 上看到的堆棧只是到 CertGetCertificateChain 函數(shù),這是因?yàn)槲覜]有加載 dotnet 6 的 sos 因此無法拿到 .NET 層的調(diào)用信息。如何加載 dotnet 6 的 sos 請(qǐng)看?WinDbg 加載 dotnet core 的 sos.dll 輔助調(diào)試方法
在調(diào)試到 CertGetCertificateChain 申請(qǐng)的內(nèi)存沒有泄露,后續(xù)的調(diào)試我也不用 WinDbg 了,也不需要去加載 dotnet 6 的 sos 了。我通過靜態(tài)代碼分析,閱讀 dotnet 6 的底層代碼,看到了下面代碼
internal sealed partial class ChainPal
{
internal static partial IChainPal? BuildChain()
{
// 忽略代碼
if (!Interop.Crypt32.CertGetCertificateChain(storeHandle.DangerousGetHandle(), certificatePal.CertContext, &ft, extraStoreHandle, ref chainPara, flags, IntPtr.Zero, out chain))
{
return null;
}
}
}
根據(jù)官方文檔,需要使用?CertFreeCertificateChain?釋放上面代碼的?chain
?變量。然而如上面代碼,在 CertGetCertificateChain 方法返回 false 值,就返回了,沒有對(duì) chain 調(diào)用釋放
我不了解是否在 CertGetCertificateChain 方法返回 false 值,就不需要調(diào)用?CertFreeCertificateChain?的問題,我反饋給了 dotnet 官方,詳細(xì)請(qǐng)看?CertGetCertificateChain memory leak in pure Windows 7 system · Issue #68892 · dotnet/runtime
通過閱讀 mozilla 的代碼,看到了 mozilla 在 CertGetCertificateChain 方法返回 false 值,也是立刻返回,沒有調(diào)用 CertFreeCertificateChain 方法,詳細(xì)請(qǐng)看 https://hg.mozilla.org/releases/mozilla-release/rev/d9659c22b3c5#l3.347
但是 Xx 應(yīng)用的內(nèi)存泄露問題已解決,后續(xù)就交給 dotnet 官方
那為什么 .NET Framework 就不存在問題?我繼續(xù)閱讀 dotent 代碼和考古 .NET Framework 的代碼,看到了這個(gè)邏輯是在 .NET Framework 4.6 變更的,也就是本文開始說的內(nèi)容。剛好 Xx 應(yīng)用是從 .NET Framework 4.5 升級(jí)到 dotnet 6 的,剛好就踩到這個(gè)坑
我回顧了本次的調(diào)試,用了五天,實(shí)際上方向錯(cuò)了。如果開始聽?waterlv?大佬,內(nèi)存不會(huì)無緣無故上漲的,一定是有某些業(yè)務(wù)邏輯在跑,通過調(diào)試 CPU 占用的方法,是能在一天內(nèi)完成。而如上文的調(diào)試過程,我調(diào)試的方向都是去調(diào)試內(nèi)存,這是不對(duì)的。通過 Fiddler 定位是證書問題和定位是 IPC 使用 Https 通訊且證書鏈損壞,也是定位有哪些業(yè)務(wù)模塊在執(zhí)行,也就是調(diào)試 CPU 占用。通過任務(wù)管理器可以看到,大概每間隔 3 秒就有 CPU 占用,也就是說可以認(rèn)為在 Xx 應(yīng)用,所有定時(shí)任務(wù)小于 10 秒的,都是可能導(dǎo)致本次內(nèi)存泄露的邏輯,我再次閱讀 Xx 應(yīng)用的代碼,看到了定時(shí)任務(wù)小于 10 秒的任務(wù),才只有 5 個(gè)。通過二分的方法,逐個(gè)定時(shí)任務(wù)干掉,讓這些定時(shí)任務(wù)一個(gè)個(gè)都不跑,看哪個(gè)定時(shí)任務(wù)不跑就沒有內(nèi)存泄露,就可以定位到具體的模塊。了解到是哪個(gè)模塊就可以快速了解到具體原因。如果開始使用這個(gè)方法,可以在一天內(nèi)完成,而不是花了兩周時(shí)間
這就是本次我用 dotnet 6 在 Win7 系統(tǒng)上運(yùn)行,由于用到了詭異的方式實(shí)現(xiàn)的邏輯,導(dǎo)致了觸發(fā)了一個(gè)系統(tǒng)組件或者是 dotnet 底層的坑,讓應(yīng)用內(nèi)存泄露了,我記錄了調(diào)試的過程,以及調(diào)試使用的工具,讓大家看的更加無聊
更多請(qǐng)看
ServicePointManager Class (System.Net) Microsoft Docs
無法連接到一臺(tái)服務(wù)器升級(jí)到.NET Framework 4.6 后使用 ServicePointManager 或 SslStream Api
CLR Exception E0434352 Microsoft Docs
EmptyWorkingSet function (psapi.h) - Win32 apps Microsoft Docs
使用 ProcDump 解決 VMM 服務(wù)問題 - Virtual Machine Manager Microsoft Docs
ProcDump - Windows Sysinternals Microsoft Docs
GFlags - Windows drivers Microsoft Docs文章來源:http://www.zghlxwxcb.cn/news/detail-457611.html
CertGetCertificateChain function (wincrypt.h) - Win32 apps Microsoft Docs文章來源地址http://www.zghlxwxcb.cn/news/detail-457611.html
到了這里,關(guān)于.NET 6 在 Win7 系統(tǒng)證書鏈錯(cuò)誤導(dǎo)致 HttpWebRequest 內(nèi)存泄露的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!