1、WebFirst框架描述 WebFirst 是果糖大數據團隊開發的新一代 高性能 代碼生成器&資料庫設計工具,由.net core + sqlsugar 開發 導入1000個表只要1-2秒,用法簡單,功能強大,支持多種資料庫 ,具體功能如下: 一、 建庫、CodeFirst方式線上建表,沒用到C ...
本文記錄我將應用遷移到 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
的大小從 48768
到 81152
的大小
使用 !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])。