主頁 > .NET開發 > dotnet 6 在 Win7 系統證書鏈錯誤導致 HttpWebRequest 記憶體泄露

dotnet 6 在 Win7 系統證書鏈錯誤導致 HttpWebRequest 記憶體泄露

2022-05-10 06:06:22 .NET開發

本文記錄我將應用遷移到 dotnet 6 之后,在 Win7 系統上,因為使用 HttpWebRequest 訪問一個本地服務,此本地服務開啟 https 且證書鏈在此 Win7 系統上錯誤,導致應用記憶體泄露問題,本文記錄此問題的原因以及調查程序

核心原因

核心原因是在 CRYPT32.dll 上的 CertGetCertificateChain 方法存在記憶體泄露,更底層的原因未知

在 .NET 6 里,更新了 https 訪問方法邏輯,詳細請看 Announcing .NET 6 - The Fastest .NET Yet - .NET Blog 和 What's new in .NET 6 Microsoft Docs

核心問題是呼叫進入 ChainPal.BuildChain 時,將會呼叫 Crypt32.CertGetCertificateChain 方法的呼叫邏輯有所變更,此進入邏輯和 .NET Framework 4.5 有所不同,準確來說,此差異不是 .NET 6 與 .NET Framework 4.5 的差異,而是 .NET Framework 4.6 以及更高版本與 .NET Framework 4.5 的差異

在 .NET Framework 4.6 時引入 Switch.System.Net.DontEnableSchUseStrongCrypto 變更是導致此問題的關鍵,在 .NET Framework 4.5 下,默認是 true 的值,但是在 .NET Framework 4.6 和更高版本下都是 false 的值,這就導致了整體邏輯的行為差異,此邏輯差異只和 SDK 相關,而和用戶端所安裝的運行時無關

但是此差異是否一定導致記憶體泄露,這是未知的,但記憶體泄露必定走了此呼叫邏輯

解決方法

如 SDK 提示,使用 WebRequest.Create 等方法創建 HttpWebRequest 用來進行網路請求邏輯是一個過時的方法,應該換用 HttpClient 等代替,經過實際的測驗,換用 HttpClient 即可完美解決記憶體泄露問題,順帶提升了不少的性能

也就是說此記憶體泄露從業務上說是使用了一個過時的 API 導致的問題

調查程序

在開始記錄調查程序之前,還請看一下背景

如上一篇博客 記將一個大型客戶端應用專案遷移到 dotnet 6 的經驗和決策 - lindexi - 博客園 我在完成了遷移了此大型應用到 dotnet 6 發布到內測用戶端,有內測小白鼠反饋說第二天過來就看到應用掛掉了

一開始沒有認為這是一個問題,等到第二個用戶反饋時才開始認為這是一個坑,開始進行調查

以下除錯程序非新手友好,請新手一定不要閱讀下文,如果閱讀了也一定不要在除錯記憶體泄露使用下面的方法

通過分析應用本身的日志,了解到應用是被閃退的,詢問內測的用戶了解到,應用閃退的時候,都是在晚上掛機的時候,這時候沒有任何的用戶動作,為了盡可能干掉環境問題帶來的干擾,我搭建了虛擬機,使用 cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso 安裝了純凈的系統,再加上 KB2533623 補丁讓 dotnet 6 應用跑起來,最后部署上應用,進行掛機

十分符合預期的,第二天應用掛掉了,而且系統提示 Xx 應用停止作業,通過 系統日志 可以看到存在應用錯誤例外,例外資訊是 CLR Exception E0434352 也就是在 CLR 層面出現例外

我錯誤認為這是升級到 dotnet 6 時,由于 dotnet 6 和 Win7 的兼容性導致的問題,開始著手根據 CLR Exception E0434352 Microsoft Docs 官方檔案的方法開始調查,然而卻沒有找到任何有用的資訊

繼續掛機到第三天,我這次采用任務管理器在 Xx 應用停止作業時,對應用抓一個 DUMP 傳到我開發設備上,使用 VisualStudio 的混合除錯進行除錯,此時發現錯誤資訊和第二天的不相同了,這次顯示的是 OutOfMemory 相關例外,但是我在 Win7 虛擬機上,使用任務管理器看到的 Xx 應用占用的記憶體實際上才 250 MB 而已,這一定是在諷刺我

好在我反應過來,任務管理器上面看到的應用占用 250MB 記憶體,完全不等于應用使用的記憶體是 250MB 的空間,為什么呢?這是一個復雜的問題,我不想在本文這里聊 Windows 下的應用記憶體知識,也許后續會另外開一篇很長的博客來說明,需要了解的是,如果一個應用 OOM 了,那除了系統本身給不到應用足夠的記憶體之外,還有另一個問題就是應用本身用到了平臺限制的最大記憶體數量,別忘了 x86 和 x64 的差異

剛好,此 Xx 應用是一個 x86 應用,在通過系統日志了解到此 Win7 虛擬機上沒有存在一刻是記憶體不足的情況,而且此純凈的虛擬機也就跑了 Xx 一個應用,要是記憶體不足,也是 Xx 應用的鍋,回憶一下,使用 x86 應用,默認的行程空間是 4G 大小,其中有 1 到 2G 需要給系統交稅,也就是應用在開啟大記憶體感知時,最大能用到 3G 的記憶體,如果應用在到達 3G 記憶體占用附近時,依然向系統申請記憶體,那此時就 OOM 了

任務管理器說應用占用了多少記憶體,實際上如果是以上的申請記憶體超過 x86 平臺限制的導致的問題,那完全必須無視任務管理器說的話,特別是在用戶端,別忘了還有 EmptyWorkingSet 這樣安慰人的方法

我通過拿到 DUMP 檔案的大小,看到 DUMP 檔案是接近 4G 的大小,猜測是 Xx 應用申請記憶體超過 x86 平臺限制,調查此問題需要用到微軟極品工具箱的 VMMap 工具

通過 vmmap 可以看到此時的應用的 Private Data 占用達到接近 3G 的大小,因此可以定位到 Xx 應用閃退的原因是因為申請記憶體超過 x86 平臺限制

也就是說有兩個分支導致 Private Data 占用過多,第一個原因就是業務需要申請大量的記憶體空間,第一個原因不算是記憶體泄露問題,只能算是性能優化問題,某個業務邏輯空間復雜度過高,第二個原因就是應用記憶體泄露,應用不斷運行程序中,不斷泄露記憶體,運行的時間長了,自然多少記憶體都不夠用

換句話說,不是所有的 OOM 問題,都是記憶體泄露問題,可能還是業務需要申請大量的記憶體空間問題,但顯然,本次遇到的問題,應該就是記憶體泄露問題了,畢竟只是掛機就讓應用掛掉了,那大概確定是記憶體泄露了,但是這只能說大概,萬一有一個定時任務是從后臺拉取某個資料,剛好這個資料導致了某個處理業務需要申請大量的記憶體,從而讓應用掛掉,為了確定是哪個方式導致的 OOM 了,可以先使用排除的方式,如果是某個業務申請大量的記憶體導致記憶體泄露,這是非常好也非常方便除錯出來的,只需要使用 dotMemory 工具分析一下即可

在開始使用 dotMemory 之前,還遇到一個小問題,那就是 dotMemory 不能在我的 Win7 虛擬機上運行,而我又不想去污染此虛擬機環境,好在 dotMemory 可以分析 DUMP 檔案,于是我就拿來剛才使用 任務管理器 抓的 DUMP 檔案進行分析,可惜,由于 Win7 虛擬機采用的是 X64 系統,而應用是 X86 應用,導致任務管理器抓的 DUMP 檔案無法被 dotMemory 識別,只能再次換用專業 ProcDump 工具去抓行程的 DUMP 檔案

換用 ProcDump 工具去抓應用的 DUMP 檔案用起來比任務管理器更加方便,我也推薦使用 ProcDump 去抓 DUMP 檔案,這個工具是十分強大的,本文用到的只是很少的功能,由于這個工具太強大了,要介紹的話,也是另一篇博客了,本文也不會包含此工具的更多使用方法

在虛擬機上面使用 procdump -ma <PID> 命令,這里的 <PID> 就是要抓取的行程的 Id 號,將 Xx 應用抓取 DUMP 檔案,然后再用 7z 壓縮一下,傳回到我的開發設備上,用 dotMemory 打開分析,使用 7z 是因為可以很大的壓縮 DUMP 檔案,通過 dotMemory 分析沒有看到有哪個業務使用了大量的記憶體,總的 .NET 記憶體占用實際上才不到 100MB 大小,因此大概可以確定不是因為某個業務申請大量的記憶體導致記憶體泄露,至少不是申請托管記憶體

繼續回到確定 OOM 導致的原因上,我重新運行 Xx 應用,通過 VMMap 工具不斷按 F5 重繪,經過三個小時間斷追蹤,可以看到 Private Data 緩慢上漲,通過此,可以判斷是記憶體泄露問題

記憶體泄露通用處理方法就是先抓取泄露點,通過泄露點了解泄露模塊,抓取泄露點的通用方法就是對比幾段時間點,有哪些物件被創建且不被回收,依然是使用 ProcDump 工具抓取 DUMP 檔案,然后通過 dotMemory 的匯入 DUMP 功能,以及對比記憶體功能,進行分析

如果要是 dotMemory 可以符合預期的讓我看到業務模塊上有哪些物件沒有被釋放,那自然就不會有本文的記錄,畢竟如此簡單就能解決的問題,要是還水一篇博客就太水了,通過 dotMemory 抓取可以看到不同的時間點上,沒有任何業務代碼的物件泄露,唯一新建的幾個物件都是 System.Net 命名空間下的,而且占用的托管記憶體也特別小,這幾個物件的根參考都是 Ssl 相關的底層模塊,看起來似乎沒有問題

也如一開始的調查,泄露的部分似乎不在 .NET 托管上,而是非托管的泄露,對一個純 .NET 應用來說,可以認定所有的非托管泄露都是由托管導致的,但是可惜 Xx 應用是一個復雜的應用里面包含了其他團隊寫的一點庫邏輯,于是先嘗試定位一下是否遷移程序,修改了部分的 C++\CLI 邏輯導致的記憶體泄露,定位的方法是采用二分法,也就是干掉這些引入的庫的邏輯,我重新寫了代碼,用 Fake 的方式重新實作了假邏輯,將所有的其他團隊寫的非 .NET 的庫的檔案都刪掉

可惜洗掉了其他團隊寫的非 .NET 的庫之后,依然存在記憶體泄露,也就是說可以確定是在托管層存在記憶體泄露的,此時我特別怕是遷移到 dotnet 6 導致的,和 Win7 的適配問題,而用 dotMemory 也無法給我帶來更多的幫助,用 dotMemory 最預期的能拿到的資訊就是業務端有某些物件被泄露,可惜沒有找到任何業務端的物件泄露,那此時用 VisualStudio 是否有更多資訊?不會有的,放心吧,在除錯記憶體泄露方面,使用 VisualStudio 和 dotMemory 的能力是完全相同的,只是 VisualStudio 的互動做的太過垃圾,完全不如 dotMemory 的互動形式,因此用 dotMemory 沒有帶來更多幫助,同理使用 VisualStudio 也不會有更多幫助

為了確定是否 dotnet 6 底層帶來的問題,我先在 dotnet 開源倉庫 https://github.com/dotnet/runtime/ 里翻 dotnet 6 的記憶體相關的帖子,好在沒有找到任何有關聯的有幫助的,那就側面證明了,應該是沒有其他人遇到了此問題,這是一個好訊息,但也許不是,那就是我是第一個遇到的人,其次,由于我采用的是 dotnet 6.0.1 版本,分發給用戶端的不敢那么頭鐵用剛發布的版本,官方最新的是 dotnet 6.0.4 版本,也許在某個安全更新修復了此問題,安全更新有一些是保密的,也就是說我沒有能找到,如果強行去找,可以用 MVP 權限去尋找,但這個回應速度就沒有那么快

接下來可以調查的方向如下

  • 是否 dotnet 6 底層帶來的問題
  • 是否 dotnet 6.0.1 帶來的問題,但在 dotnet 6.0.4 修復了

確認是否 dotnet 6 底層帶來的問題剛好在我這個專案上,沒有那么麻煩,我對比測驗了在 Win10 的設備上,發現沒有記憶體泄露,剛好 Xx 應用是從 .NET Framework 遷移過來的,現在改改代碼還能跑 .NET Framework 的版本,于是也就同步在出現問題的 Win7 上跑 .NET Framework 的版本,結果發現在 Win7 上使用 .NET Framework 版本沒有任何問題,于是大概可以確定,這和 dotnet 6 底層是有所關聯,但不能說這是 dotnet 6 底層的鍋

接下來確定是否 dotnet 6.0.1 帶來的問題,但在 dotnet 6.0.4 修復了的問題,我在此出現問題的 Win7 上,使用 dotnet 6.0.4 版本代替原先的 6.0.1 版本,好在 dotnet 6 是不需要安裝的,替換檔案即可,結果依然存在記憶體泄露,這是一個壞訊息,也就是說也許我是第一個遇到此問題的人,或者說這是一個官方也不知道的問題,我就嘗試去面向群編程,詢問了幾位大佬是否遇到過此問題,然而所有的回答都和本次遇到的不是相同的問題,且沒有一位大佬遇到 dotnet 6 底層的記憶體泄露問題,這也算是好訊息

回到測驗 dotnet 6 底層帶來的問題上,既然對比了 .NET Framework 和 dotnet 6 兩個框架,發現只有在 dotnet 6 框架才出現問題,那可能的原因實際上可以分為三個:

  • 遷移 dotnet 6 程序中,與 .NET Framework 的變更導致的問題
  • 由于 dotnet 6 的機制變更,與 .NET Framework 的不相同,導致的記憶體回收策略變更的記憶體泄露問題,例如之前遇到的委托問題
  • 這就是 dotnet 6 底層與 Win7 適配的問題

由于 Xx 應用是一個足夠復雜的大型應用,不好定位以上的三個原因,于是采用對比測驗法,先創建一個空白的 dotnet 6 的 WPF 應用,在此 Win7 上運行,十分符合預期的,沒有記憶體泄露問題,這能證明,不是那么簡單的 dotnet 6 的底層的問題,假如使用空的 dotnet 6 的 WPF 應用也能存在記憶體泄露,那就能快速定位是 dotnet 6 底層的問題,接下來的步驟就是看是否 WPF 的問題還是 dotnet 更底層的問題,畢竟這個 WPF 是我定制的版本,改了不少的內容

再定位是否遷移 dotnet 6 程序中,與 .NET Framework 的變更導致的問題,我尋找了所有的變更邏輯,逐個還原,或者使用 Fake 邏輯,干掉對應的功能,這個程序相當于一個二分,也就是說如果在干掉了某些功能之后,沒有出現記憶體泄露,那就能定位記憶體泄露和被干掉的功能相關,完成之后,同時構建出 dotnet 6 和 .NET Framework 兩個版本,在此 Win7 上運行,結果依然是 dotnet 6 版本存在記憶體泄露,而 .NET Framework 版本沒有記憶體泄露

這就證明了原因可能就是 由于 dotnet 6 的機制變更,與 .NET Framework 的不相同,導致的記憶體泄露,但經過以上的測驗,不能說明一定是 記憶體回收策略變更的記憶體泄露問題

到這里,其實基本沒有了通用套路可以定位的方法了,除了使用二分法,使用二分法逐個模塊干掉,看干掉到哪個模塊就不存在記憶體泄露問題,但在此 Xx 應用上使用二分法是一個大工程,再加上記憶體泄露的判斷是需要等待一段時間的,而不是快速就能定位出來,需要通過 VMMap 經過一段時間,按照小時為單位,看 Private Data 的占用,才能了解到是否記憶體泄露,以上的測驗都是可以并行多個同時開始的,盡管每個測驗都需要占用半天的時間,好在多個測驗并行,以上的測驗都在一天內完成,但如果采用二分,那就意味著需要進行串行測驗,在上次沒有測驗完成之前,是無法進行下一個二分的,我就將二分作為最后的方法,繼續找找其他的方法

回顧一下,使用 .NET Framework 沒有問題,只有 dotnet 6 版本存在記憶體泄露,通過 dotMemory 和 DUMP 沒有找到業務物件的記憶體泄露,只有某幾個 System.Net 命名空間下的物件存在,這些物件不確定是否泄露,更新了 dotnet 6.0.4 也沒有解決,也沒有搜到帖子,問了大佬們也沒有遇到相同的問題,也就是說不是 dotnet 的官方已知問題

既然看到了存在 System.Net 命名空間下的物件存在,那可以猜測是和網路相關的問題,剛才的 dotnet 6 的空 WPF 測驗應用只能證明和基礎的 dotnet 6 無關,但沒有證明和網路模塊無關,繼續寫一個訪問網路的 demo 專案,運行發現沒有記憶體泄露問題,看起來此記憶體泄露問題也不是那么簡單能復現,一半是好訊息,一半是壞訊息,剛好 waterlv 大佬有慷訓復我了,他告訴我,記憶體不會無緣無故上漲的,一定是有某些業務邏輯在跑,于是另一個方向是放棄記憶體的方向,而是調查空閑的時候運行了哪些邏輯

調查某個應用在某段時間運行了哪些邏輯,這是一個 CPU 性能除錯問題,相當于調查一段時間內,有哪些邏輯占用了 CPU 資源,調查這個問題最好用的工具就是 dotTrace 工具了,我準備在此 Win7 使用 dotTrace 工具抓 Xx 應用的資訊,可惜 dotTrace 工具無法在此 Win7 運行,原因有兩個,一個是需要 .NET Framework 4.7 的環境,另一個就是 ETW 準備失敗,其中 ETW 準備失敗也就無法抓取資訊,于是我放棄了 dotTrace 工具

剛好 dotnet 系里面有 dotnet trace 工具,此工具可以完美在 Win7 運行,于是我換用 dotnet trace 工具去抓取,雖然是抓取到了資訊,但是 dotnet trace 工具比 dotTrace 工具還是差太遠了,差距大概是一個是記事本,一個是 SublimeText 的差距,我沒有成功分析出來什么,反而又過去了一天

那換一個方式,通過 DUMP 抓取瞬時的執行緒呼叫堆疊,可以看到有很多執行緒存在,但是基本上都是不在運行的執行緒,唯一一個看起來稍微相關的堆疊如下

> 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

看起來和系統的 cryptnet.dll 有幾毛錢關系,也許這是 Win7 一個已知的問題,也許更新了某個補丁能解決,到這里想要繼續就只能通過 WinDbg 了,玩 WinDbg 工具需要花太多的時間,于是我先掛著 WinDbg 在 Win7 系統上,拉符號檔案,將我本機的符號檔案夾共享給他,拉取符號和共享符號檔案夾需要半天的時間,我也不能摸魚,似乎走 CPU 分析這個路是不可行的,繼續回到分析記憶體的方法

繼續猜測是網路相關問題,好在使用的是虛擬機,我聽了 waterlv 大佬的方法,禁用了網卡,跑了一個晚上,沒有記憶體泄露,那基本可以定位和網路問題是強相關了,于是開啟 Fiddler 準備抓資料,默認的 Fiddler 是沒有抓 Https 的請求的,我分為兩個階段,先抓 http 的請求,結果發現 Xx 應用沒有任何 http 請求,開啟 Fiddler 的抓取 https 請求,結果發現有某些請求發出,但是此時詭異的是 Xx 應用不再有記憶體泄露了

我根據 Fiddler 抓 Https 請求的原理猜測是因為 Fiddler 為了抓取 Https 安裝的證書導致 Xx 應用的行為和之前不同,從而沒有記憶體泄露問題,于是做對比測驗,關掉 Fiddler 的抓 https 功能,重啟 Xx 應用,跑了半天,記憶體泄露

大概可以定位到和證書相關,繼續定位是和請求哪個鏈接相關,從代碼里面進行二分邏輯,從 Fiddler 里面抓到的各個請求的代碼,逐個干掉,終于被我定位到核心的問題所在,我的另一個本機的服務應用,這是一個在本機開啟的行程服務,通過 Https 進行 IPC 本機跨行程通訊,業務模塊和這個本地服務應用有心跳通訊,每次通訊都是記憶體泄露,那為什么這個本地服務應用的通訊會讓 Xx 應用記憶體泄露,根據 Fidder 的證書問題我猜測和證書相關,重新閱讀這個服務應用的代碼,以及請教了 lsj 證書相關知識點之后,了解到這個服務應用,采用的證書有點問題,這個服務應用的證書鏈是不完整的,剛好在此 Win7 系統上,證書也都沒有更新

解決的方法有幾個:

  • 換用 http 通訊,都是本機了,還用什么 https 通訊
  • 換用 HttpClient 通訊,默認明確拋出 System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: PartialChain 例外

換用 HttpClient 通訊時,可以使用如下代碼忽略證書錯誤問題,但是此方式是不受推薦的

var handler = new HttpClientHandler()
{
   ServerCertificateCustomValidationCallback = delegate { return true; }
};
var httpClient = new HttpClient(handler);

于是我將 Https 換成 Http 的方式,再次測驗,跑了一段時間,沒有記憶體泄露,看起來就是證書導致的問題

邏輯上也是對的,一次對本機的服務應用訪問,不需要創建任何業務端的物件,全部使用的都是 System.Net 的物件,這就是使用 dotMemory 工具失敗的原因,而且請求的速度也足夠快,無法讓 DUMP 抓到資訊,再加上異步是沒有 DUMP 的執行緒堆疊,這就讓上面使用 DUMP 除錯的方法掛掉,其實要是 dotTrace 能跑起來,是可以快速定位到此模塊的,可惜 dotnet trace 還是比較渣,在了解到是這個模塊的時候,我換用 PerfView 去除錯 dotnet trace 抓的檔案,其實依然能看到這個模塊的邏輯,可惜如果沒有了解到是這個模塊的問題時,應該是無法通過 PerfView 定位的,也就是說,實際上 dotnet trace 是具備此定位的能力的,能收集到足夠的資訊,但上層的分析工具卻是渣的很,無論是 VisualStudio 還是 PerfView 工具,在界面和互動上都渣

不過說 VisualStudio 還是 PerfView 工具渣,我還是需要和 dotTrace 對比一下,和這個本地服務應用的通訊模塊,在我的開發設備上也是相同運行的,和在 Win7 系統上一樣,差別只是我的開發設備上沒有記憶體泄露,但是如上文,其實只是調查某段時間的 CPU 占用,和記憶體泄露沒有關系,我在開發設備上開啟 dotTrace 工具,抓了 Xx 應用,果然迅速就看到了和這個本地服務應用的通訊模塊的執行邏輯,也就是說如果有 dotTrace 工具一開始就能跑起來,應該可以半天內搞定

噴完了 VisualStudio 工具渣,剛好此時 WinDbg 的符號也下載完成了,可以繼續調查更底層的邏輯,依然從記憶體的角度調查,在 VMMap 工具上,通過 Private Data 的資料可以看到堆上有很多大小相同的資料,根據 Win32 記憶體除錯的套路,基本上可以確定這就是某個相同的模塊申請的,而且也沒有釋放

為了確定是哪個模塊申請了某個非托管記憶體,我使用了 gflags 工具的輔助,這個工具就放在 WinDbg 所在的檔案夾里面,在命令列執行下面命令,執行的時候將會提示管理員權限,執行完成之后是不會有任何界面的

gflags.exe /i Xx.exe +ust

使用以上命令,即可讓 gflags 輔助抓取 Xx 應用的記憶體申請的呼叫堆疊,以上命令的 Xx.exe 是不需要也不能使用絕對路徑的,只是一個行程的檔案名即可,因為實際上的抓取邏輯還是在 WinDbg 下執行,詳細請看 官方檔案

接下來是將 Xx 應用跑起來,由于 Xx 應用是在空閑的時候,沒有用戶互動,就出現記憶體泄露,為了減少 WinDbg 的復雜除錯,我在應用跑起來,啟動完成,才使用 WinDbg 附加除錯

盡管知道是某個大小的資料占用了 Private Data 記憶體,但我對 VMMap 工具不夠熟悉,不敢作為結果使用,但是可以作為方向,我重新通過 WinDbg 定位是否某個模塊申請了記憶體沒有釋放,步驟就是先找到哪個記憶體在變更,對應的堆里面的內容,是否某個大小的資料是在不斷泄露的,這些大小的資料的申請的呼叫堆疊是什么

先通過 !heap -s 命令多次執行,了解是那個記憶體在變更

按照慣例是執行至少兩次進行對比,對于大型應用,基本上都推薦是三次以上,不過我通過 VMMap 工具大概了解到方向了,于是就只使用三次,首次執行的命令和輸出如下

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 命令讓應用繼續運行一段時間

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 例外沒有識訓,就沒有繼續定位

讓 Xx 應用跑了一段時間,在 WinDbg 工具按下暫停,繼續執行 !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 的大小從 4876881152 的大小

使用 !heap -stat -h 00420000 了解這個記憶體里面的資料分布情況

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 的資料有 0x71 個,而大小為 269f8 的資料有 0x6f 個,其實這兩個不能說明問題,繼續讓 Xx 應用執行一段時間,再輸入 !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 占用的記憶體更加多了,使用 !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)

可以看到前面兩個變更了,也就是大小為 27994 的資料和大小為 269f8 的資料的數量變更了

原先:
    27994 71 - 117aa54  (37.88)
    269f8 6f - 10bf288  (36.29)
當前:
    27994 b1 - 1b60f54  (39.25)
    269f8 af - 1a67088  (37.85)

也就是說大小 Size 為 27994 的存在很多重復項

接下來就是獲取到這些被分配記憶體的地址,使用命令 !heap -flt s 27994 過濾其它的記憶體塊,只顯示大小為 27994 的記憶體塊資訊

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

輸出的內容太多了,我忽略了一些資訊

剛才開啟了 GFlags 工具,可以通過 !heap -p -a <UserPtr> 了解記憶體塊的申請呼叫堆疊,也就是哪個模塊申請的記憶體,此命令的 <UserPtr> 請替換為 UserPtr 這一列的記憶體地址,需要抓幾個記憶體塊地址來進行統計才能了解是哪個模塊申請而且泄露的

我先抓取了 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 函式申請的,對比剛才的 DUMP 抓到的執行緒呼叫堆疊,似乎 CRYPT32.dll 這個系統組件就是有鍋的,而且 CRYPT32.dll 就是處理證書相關的邏輯, 通過官方檔案了解到 CertGetCertificateChain 就是證書鏈相關邏輯

根據上文使用二分除錯到的,和本地服務應用的通訊模塊的證書鏈在 Win7 系統上損壞導致的記憶體泄露,現在根據 WinDbg 可以看到是 CertGetCertificateChain 處理證書鏈申請的記憶體沒有釋放,那就證明一定是證書鏈的問題

剛才通過 WinDbg 抓到的記憶體變更的記憶體塊大小有兩個,接下來再看 269f8 大小的記憶體塊的地址

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 記憶體地址,通過 !heap -p -a 201ff1a0 了解是哪個模塊申請

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 申請的,這是一個利好訊息,繼續再隨意找了 10e0dbe8 地址,通過 !heap -p -a 10e0dbe8 了解是哪個模塊申請

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 申請的

現在可以完全證明記憶體泄露問題是證書鏈損壞導致 CertGetCertificateChain 記憶體泄露

但是無法確定 CertGetCertificateChain 記憶體泄露的更底層原因,也無法確定這是否是 Win7 這個版本存在的問題,是否安裝了補丁可以修復,還是因為 dotnet 6 呼叫的問題,我嘗試去搜以上的堆疊,找到了 2013 的帖子 IE crashes due to SSL certificate check - Problem with MSVCR80.dll, - Microsoft Community

看起來和上面說的是相同的一個問題,我預計是有補丁可以解決,而且讓 Win7 修復證書預計也能解決此問題

繼續調查是否因為 dotnet 6 呼叫的問題,從 WinDbg 上看到的堆疊只是到 CertGetCertificateChain 函式,這是因為我沒有加載 dotnet 6 的 sos 因此無法拿到 .NET 層的呼叫資訊,如何加載 dotnet 6 的 sos 請看 WinDbg 加載 dotnet core 的 sos.dll 輔助除錯方法

在除錯到 CertGetCertificateChain 申請的記憶體沒有泄露,后續的除錯我也不用 WinDbg 了,也不需要去加載 dotnet 6 的 sos 了,我通過靜態代碼分析,閱讀 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;
                            }
   }
}

根據官方檔案,需要使用 CertFreeCertificateChain 釋放上面代碼的 chain 變數,然而如上面代碼,在 CertGetCertificateChain 方法回傳 false 值,就回傳了,沒有對 chain 呼叫釋放

我不了解是否在 CertGetCertificateChain 方法回傳 false 值,就不需要呼叫 CertFreeCertificateChain 的問題,我反饋給了 dotnet 官方,詳細請看 CertGetCertificateChain memory leak in pure Windows 7 system · Issue #68892 · dotnet/runtime

通過閱讀 mozilla 的代碼,看到了 mozilla 在 CertGetCertificateChain 方法回傳 false 值,也是立刻回傳,沒有呼叫 CertFreeCertificateChain 方法,詳細請看 https://hg.mozilla.org/releases/mozilla-release/rev/d9659c22b3c5#l3.347

但是 Xx 應用的記憶體泄露問題已解決,后續就交給 dotnet 官方

那為什么 .NET Framework 就不存在問題?我繼續閱讀 dotent 代碼和考古 .NET Framework 的代碼,看到了這個邏輯是在 .NET Framework 4.6 變更的,也就是本文開始說的內容,剛好 Xx 應用是從 .NET Framework 4.5 升級到 dotnet 6 的,剛好就踩到這個坑

我回顧了本次的除錯,用了五天,實際上方向錯了,如果開始聽 waterlv 大佬,記憶體不會無緣無故上漲的,一定是有某些業務邏輯在跑,通過除錯 CPU 占用的方法,是能在一天內完成,而如上文的除錯程序,我除錯的方向都是去除錯記憶體,這是不對的,通過 Fiddler 定位是證書問題和定位是 IPC 使用 Https 通訊且證書鏈損壞,也是定位有哪些業務模塊在執行,也就是除錯 CPU 占用,通過任務管理器可以看到,大概每間隔 3 秒就有 CPU 占用,也就是說可以認為在 Xx 應用,所有定時任務小于 10 秒的,都是可能導致本次記憶體泄露的邏輯,我再次閱讀 Xx 應用的代碼,看到了定時任務小于 10 秒的任務,才只有 5 個,通過二分的方法,逐個定時任務干掉,讓這些定時任務一個個都不跑,看哪個定時任務不跑就沒有記憶體泄露,就可以定位到具體的模塊,了解到是哪個模塊就可以快速了解到具體原因,如果開始使用這個方法,可以在一天內完成,而不是花了兩周時間

這就是本次我用 dotnet 6 在 Win7 系統上運行,由于用到了詭異的方式實作的邏輯,導致了觸發了一個系統組件或者是 dotnet 底層的坑,讓應用記憶體泄露了,我記錄了除錯的程序,以及除錯使用的工具,讓大家看的更加無聊

更多請看

ServicePointManager Class (System.Net) Microsoft Docs

無法連接到一臺服務器升級到.NET Framework 4.6 后使用 ServicePointManager 或 SslStream Api

CLR Exception E0434352 Microsoft Docs

EmptyWorkingSet function (psapi.h) - Win32 apps Microsoft Docs

使用 ProcDump 解決 VMM 服務問題 - Virtual Machine Manager Microsoft Docs

ProcDump - Windows Sysinternals Microsoft Docs

GFlags - Windows drivers Microsoft Docs

CertGetCertificateChain function (wincrypt.h) - Win32 apps Microsoft Docs

博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

知識共享許可協議
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可,歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布,如有任何疑問,請與我[聯系](mailto:[email protected]),

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/471714.html

標籤:.NET技术

上一篇:AspNetCore開源中間件-VueRouterHistory

下一篇:AspNetCore開源中間件-VueRouterHistory

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more