本文是我在集團內部上的課程記錄而成的部落格內容。在本次課程裡面將和大家介紹一些在 Windows 上常用的除錯工具,以及調查問題的常見套路。適合於夥伴們入門 Windows 除錯
本文內容的組織方式是按照原本課程課件裡面的一頁頁的內容組裝而來的方式組織的,在過程中補充一些講課時的內容
本次課程裡面核心的內容是除錯工具,除錯工具是我們在除錯軟體的時候的利器,透過除錯工具我們可以找到軟體的問題,解決軟體的問題
本次的課程的開始我來和大家講一個除錯故事,這個故事是從使用者反饋軟體用不了的問題開始的
使用者說軟體用不了,那可能會是什麼問題呢?使用者不是專業的開發人員,他們不知道如何準確的表述問題
學過軟體工程的同學應該有不少,軟體工程裡面應該會有提到,開發的第一步也是非常關鍵的一步就是需求分析。當收到使用者反饋說軟體用不了時,使用者在說什麼呢?是不是可能是軟體崩潰了?還是軟體無法啟動?還是其他的問題
遇到使用者說軟體用不了的時候,咱可以有哪些入手點呢?我的調查思路是分為兩個大的方向。第一個方向是從當下的情況入手。如果當下已經沒有了現場了,則可以考慮第二個方向,復現(重現)問題
從第一個方向入手時,可以先考慮從使用者的裝置上尋找痕跡。接下來我將和大家聊聊如何開始從使用者的裝置上尋找痕跡。當然了,如果這個使用者是咱的測試人員或者是咱的同事,那尋找痕跡這一步就更有價值了
在使用者裝置上尋找痕跡時,別忘了 Windows 是咱的好朋友。Windows 提供了很多工具,可以幫助我們找到問題的原因。接下來我將和大家介紹一些 Windows 上自帶的常用的除錯工具
第一站就是事件檢視器。可以先假設咱可能遇到的是軟體啟動即崩潰的問題。在不遠端使用者的情況下,可以先請使用者傳送系統事件日誌或截圖過來看看。事件檢視器作為第一站的原因是可不發起遠端,直接請使用者截圖或傳送日誌過來。相對來說對開發者的工作成本較低
透過事件檢視器可以進行快速的分析,如看到軟體崩的日誌,那就可以證明確實是軟體崩潰了。後續咱的調查方向就可以向著軟體崩掉的方向進行
也有可能透過事件檢視器直接看到非常有效的資訊,直接就結束戰鬥,定位到了問題
舉個例子
有一次我在除錯一個軟體的時候,使用者反饋說軟體無法啟動。我讓使用者傳送了事件檢視器的日誌過來,透過日誌可以看到如下內容
錯誤應用程式名稱: lindexi.exe,版本: 5.1.12.63002,時間戳: 0xedd2d687
錯誤模組名稱: MSVCR100.dll,版本: 10.0.40219.325,時間戳: 0x4df2be1e
異常程式碼: 0x40000015
錯誤偏移量: 0x0008d6fd
錯誤程序 ID: 0x994
錯誤應用程式啟動時間: 0x01d50ac3bd970061
錯誤應用程式路徑: C:\Program Files\lindexi\lindexi.exe
錯誤模組路徑: C:\Program Files\PowerShadow\App\MSVCR100.dll
報告 ID: a0c5c0b1-76b7-11e9-9d20-94c69123de40
細心的夥伴也許一眼就看出來問題了,出現問題的是 MSVCR100.dll 模組,然而這個模組路徑居然是在一個不認識的,名為 PowerShadow 的軟體的目錄下。這時候就可以大概確定問題了,這是被投毒了
試試用谷歌好幫手,搜搜這個軟體是什麼軟體。剛好搜到了這篇部落格: 影子系統讓 C++ 程式無法執行
於是這就結束戰鬥了,調查到了問題的原因,軟體無法啟動是因為被投毒了,被影子系統投毒了。解決方法就是請使用者解除安裝影子系統,因為影子系統也不維護了,咱軟體層沒啥好掙扎的
可惜的是在很多使用者的裝置上,事件檢視器日常不工作。沒關係,能從事件檢視器找到額外資訊,就是賺到了
如果事件檢視器找不到或不能用?咱還有其他很多工具可以用
尋找痕跡的時候,另一個常用的好工具就是工作管理員。工作管理員是 Windows 自帶的一個工具,可以幫助我們瞭解到非常多的資訊
透過工作管理員尋找痕跡時,可以按照如上圖所示的決策樹瞭解一下情況。如果不能在工作管理員裡面看到程序,那很可能就是程序已經崩掉了。如果能夠看到程序,那可能就是程序卡了。此時關注點可以是 CPU 使用率。如果 CPU 使用率不動,那可以猜猜可能是死鎖問題,如果 CPU 使用率爆高,那可能是死迴圈等問題。同步也看一下記憶體使用率,雖然在工作管理員裡面看記憶體使用率不能真實反映記憶體使用情況,但是可以作為一個參考。詳細關於如何正確檢視程式的記憶體使用情況,後面會有專門的內容介紹
無論是何種情況,都可以試試撈一個 DUMP 回來除錯看看。當然了,對於軟體崩掉的情況,先嚐試一下是不是能啟動起來,拼手的速度快速撈一個 DUMP 回來,如果不能,那後文還會和大家介紹其他工具來輔助撈 DUMP 檔案
先回顧一下,咱的調查思路一開始就是嘗試尋找痕跡。尋找痕跡的時候藉助 Windows 裡面提供的好用的工具,這裡重點介紹的是事件檢視器和工作管理員。透過事件檢視器可以快速的瞭解到軟體崩潰的原因,透過工作管理員可以瞭解到軟體的執行情況
在透過自帶的工具沒有明確收穫的情況下,則嘗試撈一個 DUMP 回來開發機器上進行進一步分析
本課程這裡提到的 DUMP 檔案是指 Windows 下的記憶體轉儲檔案,是一個二進位制檔案,簡單用人話說就是將程序的記憶體內容儲存到檔案裡面。透過 DUMP 檔案可以有效還原出此時的程序的記憶體狀態和記憶體裡面的內容,可以用於進一步的分析。當使用者環境裡面沒有帶開發工具時,撈一個 DUMP 檔案回來,可以幫助我們在開發機器上進行進一步的分析。撈 DUMP 分析的過程,相當於給程序做了一個快照,然後將其放在開發機器上進行進一步的分析
假設程序還在的話,那最簡單的撈 DUMP 方式就是透過工作管理員右鍵選擇建立記憶體轉儲檔案了。對應的英文系統是 Create memory dump file 選單項
這裡需要額外說明的是,如果當前系統是 x64 系統,但是自己的程序是 32 位程序,那此時不建議使用預設開啟的工作管理員撈 DUMP 檔案。因為預設開啟的工作管理員是 x64 的,打出來的是 x64 轉儲檔案,包含 WoW64 子系統的資訊。詳細請看 你生成的轉儲檔案有問題嗎? - 知乎
正確的做法應該是使用 C:\Windows\SysWOW64\Taskmgr.exe
的工作管理員去撈 DUMP 檔案
現在假定撈到了 DUMP 檔案了,那接下來的步驟就是如何分析 DUMP 檔案了。當然了,前置步驟就是如何將 DUMP 檔案傳回到自己的開發機器上,這裡有一個小妙招就是將這個 DUMP 壓縮一下。由於 DUMP 檔案是記憶體轉儲檔案,大部分都是全零的內容,壓縮率非常高。如果需要透過網路等方式傳輸,那壓縮一下再傳輸會快很多
分析 DUMP 的工具有很多,我著重要和大家介紹的是太陽系最強 IDE —— VisualStudio。VisualStudio 已經是一個成熟的 IDE 了,只需將 DUMP 拖進去就可以了,聰明的 VisualStudio 可以自動幫咱進行分析
一般而言,將 DUMP 拖入到 Visual Studio 裡面,接著點選混合除錯按鈕即可。混合除錯是使用 託管 除錯和 本機 除錯的組合。託管除錯是指除錯 .NET 程式,本機除錯是指除錯其他非 .NET 系的程式。混合除錯是指同時除錯託管和本機程式碼,因為一般而言 .NET 系的應用要在託管層崩潰是有點難度的,除非開發者自己比較缺乏處理。然而本機程式碼,如某些使用 C 、彙編、C++ 編寫的程式,那就容易崩潰了。混合除錯可以同時除錯這兩種程式碼。即使程序完全不是 .NET 程式,也可以使用混合除錯來除錯
進入混合除錯之後,需要等待 Visual Studio 自動分析。如果是第一次除錯 DUMP 檔案的,可能會在下載符號這一步卡住一會。大家可以出去喝個茶,等待一下,再回來看看。實在等不急了,那就點選取消符號載入再繼續吧
好的,現在咱的進度就是在使用者側發現了問題,且不能透過事件檢視器等結束戰鬥。將使用者的 DUMP 檔案撈回來,透過 Visual Studio 進行分析。分析的方法就是將 DUMP 檔案拖入 Visual Studio 裡面,然後點選混合除錯按鈕。等待 Visual Studio 自動分析,即可看到分析結果
那聰明的 Visual Studio 會幫咱分析出什麼內容呢?如何看 Visual Studio 的分析結果呢?常見的套路就是關注 Visual Studio 以下三個方面內容
- 呼叫堆疊
- 後文會介紹的 "三板斧" 內容
- 區域性變數
先來和大家介紹一下呼叫堆疊。呼叫堆疊是個好東西,呼叫堆疊是一個非常重要的內容,可以幫助我們瞭解到程式是如何執行的。透過呼叫堆疊可以看到程式是如何執行的,是從哪個函式開始的,是如何呼叫的,是如何返回的。預設的 Visual Studio 除錯佈局裡面,可以快速看到呼叫堆疊窗格
呼叫堆疊可以如何看?呼叫堆疊可以和著之前在使用者端工作管理員所見內容進行一起分析。如在工作管理員看不見程序,即對應程序崩了的問題,可以透過呼叫堆疊嘗試看到是誰帶崩的,崩之前呼叫的是哪個函式。如果是在工作管理員能看到程序,但是 CPU 使用率不動,那可能是死鎖問題,可以透過呼叫堆疊看到是哪個函式卡住了主執行緒或進入鎖。如果是 CPU 使用率爆高,那可能是死迴圈問題,可以透過呼叫堆疊看到是哪個函式跑滿了執行緒
舉個真實的例子,以下就是我從使用者端撈回來的一個 DUMP 檔案。透過 Visual Studio 分析,崩潰之前的呼叫堆疊如下
> 00000000() Unknown
[Frames below may be incorrect and/or missing] Unknown
nvumdshim.dll!710d0745() Unknown
nvd3dum.dll!5989f2e1() Unknown
nvd3dum.dll!595f1716() Unknown
nvd3dum.dll!596b7827() Unknown
nvd3dum.dll!598a6233() Unknown
nvd3dum.dll!5989b95c() Unknown
nvd3dum.dll!5989c33b() Unknown
nvd3dum.dll!598816bc() Unknown
nvumdshim.dll!710ca40e() Unknown
nvumdshim.dll!710cbb78() Unknown
nvumdshim.dll!710ca17f() Unknown
nvumdshim.dll!710ca0d3() Unknown
d3d9.dll!5ab86f81() Unknown
ntdll.dll!_NtWaitForMultipleObjects@20 () Unknown
KERNELBASE.dll!76f69723() Unknown
透過呼叫堆疊可以看到是 nvumdshim.dll 模組帶崩的。這個模組是 NVIDIA 顯示卡驅動的模組。透過這個呼叫堆疊可以看到是 NVIDIA 顯示卡驅動帶崩的。這個問題的解決方法就是更新 NVIDIA 顯示卡驅動。此問題詳細請看 記因為 NVIDIA 顯驅錯誤而讓 WPF 應用啟動閃退問題
驅動問題是客戶端崩的常見問題,表現就是在很多使用者電腦工作好好的,在某些使用者就起不來
修復 DirectX 時,我常用的就是 DirectX 修復工具,此工具下載地址是: https://blog.csdn.net/VBcom/article/details/6962388
講完了誰帶崩的問題,接下來再看另一個案例。對應 CPU 不動的問題,如下圖所示的呼叫堆疊
大家猜猜上面堆疊告訴咱什麼問題
透過以上的堆疊可以知道進入了鎖。此時的常見套路就是從上到下找找,找第一個咱自己程式集的呼叫函式,如這裡就找到了是在 lindexi.dll 裡面的方法。可以知道的是這個方法有邏輯在等待鎖,且這個鎖就不返回。此時配合程式碼食用更佳。咱這裡能夠知道程序卡住的原因是因為等待鎖,且這個鎖不返回,而至於這個鎖在業務上是什麼作用就需要咱進一步配合程式碼進行分析了
再來看看對應 CPU 爆高的一個案例,此時堆疊裡面的資訊可以告訴咱,現在正在跑的方法是哪些。有可能就是當前的呼叫堆疊的頂部的幾個方法有邏輯跑滿了執行緒了。同樣,此時配合程式碼食用更佳
但有可能此時面對的情況是沒有程式碼。如使用的是第三方庫等,此時靠堆疊資訊是不夠的。先讓大家思考這個問題,如果此時沒有程式碼還可以如何進一步分析?我將在後文和大家介紹如何透過三板斧來進一步分析
回顧一下,這就是咱拖入 DUMP 檔案之後,依靠 Visual Studio 裡面的呼叫堆疊進行問題分析的常見三個案例。對應軟體崩潰的問題,可以透過呼叫堆疊看到是誰帶崩的。對應 CPU 不動的問題,可以透過呼叫堆疊看到是誰卡住了主執行緒。對應 CPU 爆高的問題,可以透過呼叫堆疊看到是誰跑滿了執行緒
但是僅靠呼叫堆疊可能還是不夠的,有時候需要更多的資訊。接下來我將和大家介紹如何透過“三板斧”來進一步分析
這裡介紹的“三板斧”分別是暫存器、反彙編、記憶體這三個方面的工具。透過這三個方面的工具可以幫助我們進一步的分析問題
需要說明的是用到這三個工具時僅僅只是在咱有需要了解更多狀態資訊的時候。而且透過這三個工具也不一定能夠準確瞭解到問題的原因。這三個工具的使用本身不難,但是其難點確是這幾個工具所見內容的背後大家關於程式本身的理解以及軟體執行機制的瞭解。如果對於軟體執行機制不瞭解,那這三個工具所見內容可能會讓人難以理解,或者是調查方向跑偏
依然使用剛才的例子,當看到 CPU 爆高的時候,透過呼叫堆疊可以看到是哪個方法跑滿了執行緒。但是這個方法邏輯跑滿了,其原因是什麼呢?呼叫堆疊可無法回答此問題
試試先在 Visual Studio 裡面開啟記憶體、暫存器、反彙編窗格。這三個工具可以幫助我們進一步分析問題
開啟之後的 Visual Studio 的介面佈局大概如上圖所示
拿本課程的 CPU 爆高的例子,先透過反彙編發現了可能存在的問題,如想看看 rcx 暫存器裡面存放了什麼。透過暫存器窗格可以看到 rcx 暫存器裡面存放了什麼內容。透過記憶體窗格可以看到這個地址裡面存放了什麼內容。剛好就看到了對應的記憶體裡面存放了一段逗比程式碼
使用 “三板斧” 本身的難度不大,但是其難點在於其背後的知識。如彙編知識,暫存器的機制,以及軟體本身的執行機制。這部分知識遠遠超過了本課程能介紹的範圍,需要大家自行學習,但由於這部分知識的學習成本較高,所以在實際工作中,這部分知識可能並不是必須的。我只敢推薦大家在有餘力的情況下進行學習,如果平時工作已經很忙了學不過來了,那這部分知識還可以先放著。但是如果能夠掌握這部分知識,那在除錯問題時會有所幫助
繼續和大家介紹 Visual Studio 的另一個除錯工具——區域性變數。區域性變數也是個好東西,可以幫助我們瞭解到程式執行時的狀態。透過區域性變數可以看到程式執行時的變數的值,可以幫助我們瞭解到程式執行時的狀態
如看到了錯誤之前的區域性變數有一個名為 lastErrorCode
的變數,也許可以透過這個變數的值來了解到錯誤的原因。但是這個錯誤碼是什麼意思呢?這個錯誤碼的含義在哪裡找呢?咱可以試試 error 這個工具,這個工具可以自動幫助咱找到可能的錯誤碼的含義。這是工具是微軟整理的,絕大部分呼叫系統層的元件所見的錯誤碼都可以在這裡找到
工具下載地址: https://learn.microsoft.com/en-us/windows/win32/debug/system-error-code-lookup-tool
如在這裡咱可以看到的錯誤資訊是檔案或資料夾名錯誤,根據咱的業務邏輯,可能是檔名錯誤導致的問題。那接下來的調查方向就是看看為什麼出現錯誤的檔名了,這時候也許一看程式碼就理解了
再舉另一個真實的例子,如看到的是如上圖的異常導致的崩潰。根據咱透過搜尋引擎瞭解到的知識,這個 WindowsCodecs.dll 是 Windows 系統的 WIC 多媒體解碼層。可能此時遇到的問題和圖片等多媒體的編解碼有關
剛好在本例子裡面,透過區域性變數看到了出問題的圖片的檔案地址,此時的調查就更加有方向了。除了可能存在的 WIC 層的問題外,還可以是圖片檔案本身的問題。如圖片檔案投毒等問題
延伸一下,如何瞭解圖片、音影片等檔案是否被投毒了?這裡推薦一個工具,透過 MediaInfo 工具可以幫助咱看到檔案的許多資訊
如這個檔案就是一個假裝是 png 的 WebP 檔案,然後投毒將 WIC 層搞崩了
MediaInfo 工具下載地址: https://mediaarea.net/en/MediaInfo/Download
好像… 還是有些問題除錯不出來
太陽系最強 IDE 也頂不住呀
那就試試上接近能除錯一切的 WinDbg 吧
這個工具非常強大,只是有一個問題。那就是有億點點上手門檻
在這裡我告訴大家一個非常簡單的方法,讓大家瞬間就能學會上手使用 WinDbg 工具除錯問題。方法就是請一個熟悉 WinDbg 的夥伴,讓他幫你除錯,找到一個工具人幫你使用 WinDbg 除錯問題是最快能學會使用 WinDbg 的方法
回顧一下,以上咱就聊了在使用者端發現問題,先嚐試使用 Windows 自帶工具快速進行定位問題。以及撈到 DUMP 檔案之後,如何在開發機器上透過 Visual Studio 進行進一步分析。分析的方法就是將 DUMP 檔案拖入 Visual Studio 裡面,然後點選混合除錯按鈕。等待 Visual Studio 自動分析,即可看到分析結果。分析的重點是呼叫堆疊、三板斧、區域性變數。透過這三個方面的工具可以幫助我們進一步的分析問題
如果 Visual Studio 還不能解決問題,那就找個工具人來幫忙使用 WinDbg 繼續調查問題
這就是第一個大方向的內容
第二個大方向就是事後現場的復現問題。什麼時候需要復現問題?比如最簡單來說就是軟體啟動即崩潰,完全來不及開啟工作管理員撈 DUMP 檔案。這時候就需要復現問題了,透過復現問題可以幫助我們更好的定位問題
復現問題時也不是隻是簡單重複跑程式,而是可以透過更多的工具輔助來在復現問題時更好的定位問題
首要介紹的就是 ProcDump 工具
當使用工作管理員撈不到 DUMP 或不好撈 DUMP 時,使用 ProcDump 工具能夠更好的幫助我們撈 DUMP 檔案。ProcDump 工具是 Sysinternals 的工具,下載地址是: https://learn.microsoft.com/zh-cn/sysinternals/downloads/procdump
為什麼說有時候不好使用工作管理員撈 DUMP 呢?因為現實往往很複雜。除了閃崩,軟體啟動即崩潰導致的手速不夠快,撈不到 DUMP 檔案之外,還有其他很多問題。比如軟體就是處於似崩未崩的狀態,期望抓到某個時機的狀態,如軟體一定會在某次 CPU 爆高之後不能符合預期工作,然而 CPU 爆高的時間非常短,靠人類去看去抓是有些廢程式猿的。比如軟體半夜崩潰,只有在午夜12點才會崩潰,這時候人類可能已經睡著了,即使沒睡著,可能錯過了這個時間點就要等明天的午夜12點了。再比如是非必現的問題,需要壓測才能復現,期望自動化收集,否則可能要跑幾千次才能復現一次,靠人類的工作量有些大
透過 ProcDump 可以在程式萬種死法中有效的生成 Dump 檔案,只需使用好 ProcDump 的引數。具體引數作用可以參考 微軟官方文件 和 如何在 NET 程式萬種死法中有效的生成 Dump (上) - 一線碼農 - 部落格園
這是一個小遊戲,讓大家連連線,看看在什麼情況下應該使用什麼方法
在調查思路這裡,復現問題時經常伴隨使用 ProcDump 工具,因為 ProcDump 工具可以在非常多的情況下幫助我們撈 DUMP 檔案
復現問題時,不僅只有 ProcDump 工具。還有可能面對的是事後現場的情況,此時需要使用更多的工具來輔助定位問題。以及當沒有調查思路時,可以試試常見的問題的探索幫助尋找思路
來和大家講講事後現場的調查
什麼是事後現場?事後現場問題在這裡一般說的是當前的現場或能復現所抓取到的現場已經不是問題發生的現場,而是發生問題之後的現場了
比如找到問題了,但問題非本質問題。常見的就是透過 DUMP 分析是如 空 異常的情況,導致崩潰的原因是因為空指標異常。但是空指標異常是如何產生的呢?這時候就需要透過事後現場分析思路調查來進一步分析問題
比如發生問題的地方不是產生問題的地方。如本課程的例子裡面,崩潰原因是一張假裝 png 的 WebP 圖片,那這張圖片是哪裡來的,為什麼會使用這張圖片。如果此時程式碼邏輯沒有幫助的話,那就需要進一步透過復現問題調查事前現場來進一步分析問題
比如系統性的問題。常見的就是團伙作案,不是單個應用導致的問題。這類問題的難度在於其複雜度,可能難以抓到正確的現場。此時也需要透過多次復現問題,抓取更多的資訊,透過事前和事後現場的分析來進一步分析問題
面對事後現場和團伙作案等問題,採用微軟極品工具箱的 Process Monitor 工具,配合 DebugView 工具通常都能有不錯的收穫
Process Monitor 工具下載地址: https://learn.microsoft.com/zh-cn/sysinternals/downloads/portmon
Debugview++ 工具開源地址: https://github.com/CobaltFusion/DebugViewPP
DebugView 工具下載地址: https://learn.microsoft.com/en-us/sysinternals/downloads/debugview
從介面和互動上,DebugView++ 比 DebugView 更好用一些
舉個真實栗子來和大家演示多個工具之間的配合使用來呼叫一個有趣且複雜的問題
這個問題的開始是測試同學和我報告了觸控失效問題,後來經過進一步調查發現其實是 explorer 未響應問題,表現就是 explorer 迷之閃黑
這個問題複雜之處在於 explorer 不是咱的,咱也不熟悉,也不知道是什麼導致的。而且 explorer 太龐大了,撈到 DUMP 分析壓力過大,耗時耗力。需要使用更多的工具輔助進一步分析問題
此時透過 Process Monitor 工具抓取 explorer 程序資訊,發現瞭如上圖的有趣的內容。裡面很受我關注的就是存在了程序退出
透過網上四處搜發現 explorer 是一個多程序軟體,程序的退出和迷之閃黑可能有所影響。既然程序退出了,那就試試上 ProcDump 工具在程序之前之後抓一個 DUMP 檔案回來分析
由於 explorer 十分龐大,且咱也不熟悉 explorer 的程式碼,來回抓了幾次 DUMP 分析都沒有什麼收穫。直到某次抓取到了一個有趣的 DUMP 檔案,透過這個 DUMP 檔案發現了在程序退出之前的呼叫堆疊裡面包含了 Shell32 的一些呼叫
再根據前面的 Process Monitor 工具抓到的在程序退出之前碰的是 Realtek Bluetooth 藍芽模組,於是重心就在 Shell32 和藍芽一起組合上面
既然大概定位到這裡,那就繼續上 ShellView 工具。透過 ShellView 工具進行大量的 Shell32 元件的禁用,我的做法大概就是看哪個不開森就禁用哪個,進行二分法的禁用,最終發現了是 Realtek Bluetooth 藍芽模組導致的問題
二分法的禁用就是先一口氣禁用一半的元件,看看問題是否解決。如果解決了,那就說明問題在這一半里面。如果沒有解決,那就說明問題在另一半里面。然後再在這一半里面繼續二分禁用,直到找到問題所在
經過以上的調查工具可以瞭解到是藍芽相關模組的問題,集中火力找到明確的除錯方向,很快就找到是藍芽驅動的問題
詳細的除錯內容可比這裡介紹的有趣的很,請看 記一次除錯資源管理器未響應經驗
以上的案例裡面 Process Monitor 立了首功,透過 Process Monitor 工具抓取了 explorer 程序的資訊,發現了程序退出的資訊,以及退出之前訪問了藍芽相關模組,最終透過 ShellView 工具禁用了藍芽模組定位到了具體模組導致的問題。在許多事後現場問題中,都會使用 Process Monitor 工具配合復現,嘗試找到故障之前的事前發生了什麼,瞭解故障之前的行為。進而可以縮小定位問題的方向
Process Monitor 工具適用於輔助除錯不熟悉的應用、複雜的情況,透過了解其行為來輔助縮小定位範圍,提高除錯效率。同時 Process Monitor 工具可以一口氣抓多個程序的資訊,也常用於輔助定位程序們團伙作案問題
以上就是調查思路里面的第二大方向的事後現場的內容。其思想核心就是透過復現配合工具抓取現場資訊,透過現場資訊進一步分析問題
有時候調查問題過程中沒有頭緒,沒有思路。此時可以還可以試試一些常見問題的探索,嘗試透過這些常用問題套路和經驗找到入手點
這些常見的工具可以幫助我們進行一些常見問題的排查。有時候開發任務緊張,沒有充足的時間進行全面的調查,透過常用的套路也可以提高定位效率,不一定我們需要來一次全面的調查,有可能只透過現象和猜測,配合常用套路就能找到問題的原因
這些常用工具和套路被我成為更多的除錯工具,分為以下多個方面:
- 系統環境問題-依賴缺失等問題
- 記憶體問題,包括 OOM 系問題
- 登錄檔相關模組
- 視窗相關模組
- 鍵盤等等
這些方面都可以作為常見問題的入手點探索方向
先來聊聊依賴缺失問題
依賴缺失的表現行為很明顯,一般就是程式猿常說的,“在我電腦上明明就是好的,為什麼到你電腦上就不行了”這類的話,那就可以第一時間懷疑是依賴缺失問題。或者是軟體無法啟動,啟動即崩潰,這類問題也可以懷疑是依賴缺失問題。或者是軟體開始能跑,但是跑到某個模組就崩潰了,且很固定的到某個功能模組就崩潰,那此時也能懷疑是依賴缺失問題
有些不熟悉 Windows 的夥伴也許有這樣的問題,明明自己寫的應用似乎就沒有什麼依賴,為什麼還可能遇到依賴缺失的問題呢?這是因為 Windows 系統本身就是一個依賴系統,很多功能都是透過依賴來實現的。比如說一個最簡單的例子,你寫了一個 C# 程式,但是你的 C# 程式裡面呼叫了一個 MessageBox 函式,這個 MessageBox 函式是 Windows 系統的一個函式,你的 C# 程式呼叫了這個函式,那你的 C# 程式就依賴了 Windows 系統的 MessageBox 函式。如果你的 C# 程式在別的電腦上執行,別的電腦上沒有這個 MessageBox 函式,那你的 C# 程式就會崩潰。軟體執行過程中,需要依賴許多元件,包括直接依賴和間接依賴。有可能在程式碼編寫階段沒有引入依賴,但實際上軟體已經帶上了許多隱式依賴內容,如常見的 C++ 程式需要的 VC++ 執行時依賴等等。這些依賴可能會在複雜的使用者環境裡遭遇投毒問題,導致軟體無法正常執行
依賴缺失問題首推是採用 Dependencies 工具,使用這個工具可以幫助我們定位兩種型別的問題:
- 是否在目標機器上依賴缺失問題
- 是否在目標機器上的依賴被投毒
Dependencies 工具開源地址: https://github.com/lucasg/Dependencies
識別依賴缺失問題的方法就是將自己的應用拖入到 Dependencies 工具裡面,然後看看是否有缺失依賴的提示。如上圖所示,可以看到這個應用缺乏了一個名為 Lindexi.dll 的依賴。此時可以看看輸出檔案裡面是否真的包含了這個依賴檔案,如果沒有,那就說明這個依賴確實是缺失的。如果有,那再看看這個依賴檔案的二進位制是否符合預期,如可能被修改或者下載不全等問題
常用的依賴缺失調查方向套路是看看是否有 C++ 執行時系列的丟失。其中 C++ 執行時系列常見的就是 VC++ 的多個版本的分發庫,或者是 VC++ 執行時,即 vcruntime140.dll 檔案。以及 msvcp140.dll 檔案缺失。其中重點需要說明的是,如果能看到有 vcruntime140d.dll 檔案的依賴,那就證明開發側存在問題,這裡的 vcruntime140d.dll 的意思是 VC++ runtime 14.0 debug 版本的 dll 的意思。如果看到有這個 dll 的依賴,證明你的相關方提供給你的 C++ 庫是使用 Debug 版本構建的,這是不能給到使用者端的。最佳方法是重新讓其構建一個 Release 版本的 DLL 給你。只有在實在沒有辦法的時候,才考慮在使用者端配上開發環境
另一個就是看看是否有 ucrt 系列的丟失,這個常見就是軟體能在 Win10 上跑得好好的,在 Win7 上就啟動不了。常見就是缺失了 ucrt 系列的依賴。如果是遇到這種問題,簡單的做法就是將這些檔案複製到軟體執行目錄下,或者是將這些檔案打包到軟體的安裝包裡面即可
最後一個就是看看是否有某些僅在開發機才有的負載,如我就喜歡引用一個名為 Lindexi.dll 的檔案,這個依賴檔案只在我的電腦上存在,使用者裝置上是絕對沒有的,那自然在使用者裝置上跑不起來也就符合預期了
使用 Dependencies 工具除了找依賴缺失之外,還可以用來找到是否被投毒的問題。如上圖所示,看大家是否能夠快速看出來問題
是的,上圖裡麵包含了一個名為 vcruntime140.dll 檔案,包含這個檔案是正常的,但是不正常的地方在於其路徑。為什麼 vcruntime140.dll 載入的居然是在 C:\Program Files (x86)\Foo
資料夾裡,這就證明被投毒了。大家可以在工具裡面看看有沒有存在不熟悉或奇怪的路徑,如果有,那就可能是投毒的問題?這個問題就需要進一步的調查了
如我記錄的 影子系統讓 C++ 程式無法執行 這篇部落格提到的就是非常標準的被投毒的問題,大家可以看到 MSVCR100.dll 載入的路徑是在 C:\Program Files\PowerShadow\App
資料夾裡面
以上就是常用套路里面的調查依賴缺失問題,不僅僅可能是崩潰等問題,有時候軟體執行功能不正常也可以看看是否有依賴缺失或被投毒的問題
繼續看一下記憶體問題
對 Windows 記憶體機制不瞭解的夥伴也許常常會拿工作管理員裡面看到的記憶體數值當成衡量應用軟體執行佔用記憶體的標準。比如我說軟體已經 OOM 了,但是對Windows 記憶體機制不瞭解的夥伴拿出工作管理員一看,發現應用軟體才佔用 300MB 記憶體而已,於是就完全不相信可能存在的 OOM 問題
這裡說的 OOM 問題指的是 OutofMemory 經典問題,直接表現就是記憶體不足了,程式碼裡面想要申請記憶體但是申請記憶體失敗
在開始聊這個話題之前,需要先做一點點科普。如果大家想要看一個符合客觀事實的應用軟體執行過程的記憶體使用情況,應該使用 VMMap 工具檢視,而不是工作管理員。工作管理員只能當成一個快速檢視的工具,而不是一個準確的工具。VMMap 工具可以幫助我們檢視應用軟體的記憶體使用情況,包括了虛擬記憶體、實體記憶體、私有記憶體、共享記憶體等等
VMMap 工具下載地址: https://learn.microsoft.com/zh-cn/sysinternals/downloads/vmmap
應用軟體的記憶體使用是一個相對複雜的話題,不僅僅是看看佔用了多少記憶體就能判斷的。因為事實上一個應用軟體的執行佔用記憶體是需要分至少三個維度去看的。最直接的維度就是私有記憶體和共享記憶體的佔用。一般而言,對於共享記憶體的佔用咱是不做多關注的。另一個維度是記憶體使用種類。其中開發人員在進行應用軟體的記憶體最佳化時,常常直接觸碰的應該就是 Private Data 的 Private 部分。即前面兩個維度裡面的私有記憶體和 Private Data 種類。以及 Stack 的 Private 部分。而至於如 Image 等部分,就常常不是開發人員所關注的。這裡需要對 Image 特別說明的是,這個單詞在這裡的意思不是圖片的意思,至少不是類似於 png 或 jpg 圖片的意思,而應該是記憶體映像之類的意思,常見的就是所載入的庫。有時候可以看到這部分的 Size 大小非常大,這是沒有問題的,非常正常的。比如說軟體載入了顯示卡驅動模組,這個模組的 DLL 檔案一下值就幾百 MB 大小,這部分就直接被統計到 Image 部分裡面,所以說這部分一般情況下不是開發人員所關注的
最後一個維度就是是否在 WorkingSet 裡面。在 VMMap 工具裡面的 WorkingSet 被縮寫為 WS 單詞。簡單但不準確的理解 WorkingSet 工作集就是實體記憶體佔用量。相信大家都知道 Windows 上有可放在硬碟上的虛擬記憶體和放在記憶體條裡面的實體記憶體的概念。可以認為 WorkingSet 就是實體記憶體佔用量。詳細的概念請看官方文件: https://learn.microsoft.com/zh-cn/windows/win32/memory/working-set
在工作管理員裡面常常看到的就是 WorkingSet 的佔用量。但是這個佔用量並不是準確的,至少可能一次 EmptyWorkingSet 就可以將大量的在實體記憶體裡面的記憶體轉換到硬碟上。於是單純靠工作管理員裡面的記憶體佔用來嘗試衡量應用軟體的記憶體使用情況是不準確的。特別是在調查 OOM 問題以及對比多個應用軟體的記憶體使用情況時,工作管理員裡面的記憶體佔用是不具備參考意義的,看工作管理員裡面的記憶體佔用會被誤導
額外聊一個小知識,如果你的使用者半懂不懂,想要在工作管理員裡面看到的記憶體很低。那簡單的做法就是定時呼叫一下 EmptyWorkingSet 函式好了。據說 360 加速球就是這麼做的
常用的調查套路里面,可以使用 VMMap 工具來檢視應用軟體的記憶體使用情況,看看軟體當前現場是否有記憶體使用異常的情況。此時需要關注點可能還需要包括碎片化問題,記憶體洩露問題。碎片化問題指的是儘管記憶體還有空間餘量,但是申請記憶體卻失敗了,因為記憶體雖然夠但是碎片化的存在,無法找到連續的記憶體空間。記憶體洩露問題指的是記憶體申請後沒有釋放,導致記憶體佔用越來越高的問題
額外說明一個知識點,對於 32 位程序來說,使用者態能使用的記憶體就是 2 GB 空間。一旦發現應用軟體已經用了考慮 2GB 的記憶體空間,則可以認為這個軟體可能已經發生了 OOM 問題了。因為配合碎片化問題的存在,接近 2GB 的記憶體空間就有可能存在無法申請足夠的連續的記憶體空間而導致 OOM 問題。一旦發生了 OOM 問題,可能會導致軟體進入非預期邏輯分支,如本該初始化的邏輯沒有初始化,導致後續訪問出現空異常等問題
這些時候,配合 ProcDump 一起食用效果更好。如果透過 VMMap 工具感覺到可能是記憶體問題,那此時透過 ProcDump 抓一個 DUMP 檔案回來分析,可能會有更多的收穫
透過 VMMap 不僅可以檢視應用軟體真實記憶體是怎樣,還可以在使用者裝置上進行簡單的除錯。如追蹤軟體啟動,找到是哪個模組在申請記憶體。因為很有可能只有在某個使用者的裝置上才會存在問題,那在對應的使用者的裝置上跑 VMMap 尋找申請大量記憶體的模組是非常有幫助的
透過 VMMap 也可以看到是否有記憶體碎片化的存在,即記憶體空間一看就發現出現非常多的碎片化的情況
當然了,對於 dotnet 系的應用來說,別忘了還有 dotMemory 這個好用的工具。這個工具既可以在使用者裝置上跑起來,抓取到應用軟體執行記憶體的 dotnet 系資訊,還可以對撈回來的 DUMP 檔案進行分析。這個軟體的互動做的非常好,有可能大家看介面就知道如何使用了
透過 dotMemory 工具可以看到各個模組的記憶體申請情況,各個型別存在記憶體的數量,以及各個型別的引用情況。適合於尋找記憶體洩露問題以及透過記憶體情況反推出軟體的程式碼執行邏輯
在聊到記憶體 OOM 相關問題時,一個會被開發者忽略的點是可能遇到的是爆 GDI 物件的問題。試試開啟工作管理員,在選擇列裡開啟 GDI 物件列,看看程序佔用的 GDI 物件有多少
許多的 GDI 物件所佔用的記憶體都不計入到程序裡面,這就導致了程序看起來沒有使用多少記憶體,但是系統全域性的記憶體卻是不足了。或者是程序使用了大量的 GDI 物件,導致了許多 GDI 相關函式呼叫失敗,而透過 LastErrorCode 獲取到的錯誤被翻譯為 OOM 相關錯誤,進而表現出 OOM 現象
此時可以透過工作管理員快速確定是否是 GDI 物件爆炸問題,然後透過 GDIView 工具檢視 GDI 物件的使用情況。GDIView 工具可以幫助我們檢視系統全域性的 GDI 物件使用情況,以及每個程序的 GDI 物件使用情況
GDIView 工具下載地址: https://www.nirsoft.net/utils/gdi_handles.html
舉一個實際的案例,我的師兄寫了一個程式,這個程式會不斷使用 GDI 獲取螢幕截圖,此時獲取到了大量的 Bitmap 物件,導致了系統資源大量被消耗,反過來導致可用實體記憶體不足,最終導致了 OOM 問題
透過工作管理員看到我的師兄的程式的 GDI 物件十分多,再透過 GDIView 工具看到了大量的 Bitmap 物件,如此即可快速找到入手點。經過和師兄溝通發現可能是截圖相關模組存在 GDI 物件洩露問題,反覆復現和截圖模組相關邏輯,看到工作管理員的 GDI 物件數量不斷新增,就快速定位到了問題
以上就是本課程介紹的 OOM 問題的常用調查套路和工具。使用 VMMap 工具瞭解真實的記憶體是怎樣的,避免被工作管理員誤導。可在使用者裝置上跑 VMMap 工具瞭解到是哪個模組申請了大量記憶體,以及是否存在記憶體碎片化問題。使用 dotMemory 工具瞭解到 dotnet 系的記憶體使用情況,以及可能存在的記憶體洩露問題。如果記憶體沒有問題,那就多看一下 GDI 物件的使用量,使用 GDIView 工具瞭解到 GDI 物件的使用情況
記憶體是一個複雜龐大的話題,本課程這裡只和大家介紹了一些常見的簡單情況,希望能給大家在沒有調查思路的時候提供一些入手點
接下來來看看 Windows 繫上特有的一個問題,登錄檔問題
登錄檔問題也是一個常被 Windows 開發新手忽略的問題。有些開發者有疑惑說,我的應用程式碼裡面明明都沒有碰到登錄檔,為什麼還可能會有登錄檔相關問題。其實跑在 Windows 上的程式,無論如何都會碰到登錄檔的。從軟體程序的啟動開始就在碰登錄檔,後續的載入 COM 元件等,都會涉及到登錄檔。登錄檔問題可能會導致軟體無法啟動,啟動即崩潰,啟動後功能不正常等問題
登錄檔問題可能有些深奧,本課程這裡只介紹一個簡單的真實案例。我發現我的 Notepad 記事本軟體啟動就炸掉了,不知道為什麼。我透過 Process Monitor 工具抓取時,發現了其登錄檔有以下有趣的行為。這裡就是非常靠近軟體程序退出的地方
由於 Process Monitor 工具抓取登錄檔行為時非常刷屏,降低大家的難度,我將有問題的一行日誌選擇了出來,看看大家是否猜到問題
其實這個案例裡面我的調查是靠經驗的,這裡剛好是我上回學習 COM 元件劫持時所投的毒。可以試試將登錄檔的各個訪問項在搜尋引擎裡面搜尋一下,看看是否有人說這個登錄檔項是有問題的。如果有,那就可以嘗試根據網上提供的方法進一步定位問題
相信剛才透過 Process Monitor 工具大家也感受到了被登錄檔刷屏的恐懼。如果應用軟體的表現行為是第一次能跑,第二次就不能跑,且可能是和登錄檔有關的時候。此時可以嘗試一下 RegistryChangesView 和 whatchanged 工具,透過這些工具可以快速幫助大家找到從事前到事後登錄檔的變化情況,減少刷屏的影響,提升調查效率
RegistryChangesView 工具下載地址: https://www.nirsoft.net/utils/registry_changes_view.html
whatchanged 工具下載地址: https://www.majorgeeks.com/files/details/what_changed.html
透過 RegistryChangesView 和 whatchanged 工具配合找到登錄檔變更項,再透過 Process Monitor 工具進一步輔助定位,可以瞭解到是哪個程序改了登錄檔,改了什麼內容以及在什麼時候改的。這樣就可以幫助大家更好找到問題的入手點
登錄檔相關問題如果深入瞭解還是非常有深度的,本課程這裡只介紹了一個簡單的案例。希望大家能夠透過這個案例瞭解到登錄檔問題的存在,以及透過 Process Monitor 工具,RegistryChangesView 和 whatchanged 工具等工具幫助大家更好的調查登錄檔問題
接下來看看視窗相關模組的問題
以上就是本次課程的內容,希望大家能夠透過這次課程瞭解到一些常用 Windows 除錯工具的使用方法,以及瞭解到一些常用的除錯套路。希望能在實際的軟體開發除錯過程中有所幫助
更多 dotnet 系的程式碼除錯方法請參閱: dotnet 程式碼除錯方法
更多技術部落格,請參閱 部落格導航