用 WinDbg + SOS 解讀當機傾印檔 ── 收集之後的實務分析入門

· · WinDbg, SOS, 當機傾印, .NET, CSharp, 偵錯, PDB, 缺陷調查, 技術諮詢

之前的文章「Windows 當機傾印收集入門」整理了用 WER LocalDumps、ProcDump、MiniDumpWriteDump 把傾印檔「收集起來」的做法。不過,傾印檔光是收集到手邊並不會告訴你任何事。要實際動手解讀「是哪個執行緒」「為什麼」當機,或者「是什麼」一直握著記憶體不放,才能成為調查的材料。

這篇文章接續收集篇,把焦點放在如何用 WinDbg 與 SOS 擴充功能實際讀取已收集的傾印檔。內容涵蓋安裝與符號設定、.NET 應用程式中不可或缺的 SOS 擴充功能載入、!clrstack!dumpheap -stat 等代表性指令「該看什麼、怎麼判斷」、原生當機的 !analyze -v,以及不使用 WinDbg 的 dotnet-dump analyze 該如何區分使用。

1. 先講結論

  • 傾印分析的主角是 WinDbg(目前版本,舊稱 WinDbg Preview)。可用 winget install Microsoft.WinDbg 或透過 Microsoft Store 取得,在 Windows 10 週年更新(1607)以後 / Windows 11 的 x64、ARM64 上皆可運作。1
  • 即使符號(PDB)沒有正確載入,!clrstack!dumpheap -stat!gcroot 等指令仍會直接從 CLR 的中繼資料與堆積資料運作。失去的只是受管理程式碼的原始檔名、行號,以及原生堆疊框架的符號名稱。話雖如此,若想追到原始碼行數,那就是另一回事了,所以慣例是在 _NT_SYMBOL_PATH 同時放入 Microsoft 的公開符號伺服器與自家 PDB 的位置。2
  • 在 .NET(Framework / Core / 5+)應用程式的傾印檔中,要載入 SOS 擴充功能後才能看到受管理端的資訊。光靠原生的 k(顯示堆疊)指令是追不到 C# 程式碼的。3
  • 代表性的調查型態有 3 種。因例外而當機時走 !clrstack!pe記憶體持續增加時走 !dumpheap -stat!gcroot原生當機時從 !analyze -v 入手。
  • 不使用 WinDbg 的選項是 dotnet-dump analyze。多數 SOS 指令都能直接沿用,但無法處理原生的堆疊框架。若手邊要調查的是純受管理、不涉及原生 DLL 或 COM 的情境,這個工具導入起來更輕巧。4
  • 這裡寫的是「怎麼讀」而不是「怎麼取」。傾印檔的取得方式(WER / ProcDump / MiniDumpWriteDump)請參考收集篇,而當機時與日誌互相對照的設計,請參考「Windows 應用程式當機時保留日誌與傾印檔的設計」。

2. 安裝 WinDbg 並設定符號

2.1 安裝

目前版本的 WinDbg 可透過以下任一方式安裝。1

winget install Microsoft.WinDbg

透過 Microsoft Store 安裝也會得到相同的引擎,指令、擴充功能、工作流程都是共通的。安裝後會自動更新(透過 Store 或直接安裝時是在背景進行,透過 winget 安裝時可用 winget upgrade Microsoft.WinDbg),因此不太需要煩惱版本差異造成的行為不同。1

2.2 開啟傾印檔

windbg -z C:\CrashDumps\MyApp\MyApp_20260702_101500.dmp

-z 是指定傾印檔並啟動的選項。透過 GUI 選單「File > Open Dump File」也能達到同樣效果。

2.3 設定符號路徑

Windows 偵錯器尋找符號檔(PDB)的位置,是透過 _NT_SYMBOL_PATH 環境變數,或在偵錯階段內用 .sympath 指令指定。2 實務上的基本形式是同時放入 Microsoft 的公開符號伺服器與自家 PDB 的位置

.symfix C:\Symbols\Microsoft
.sympath+ C:\Symbols\MyApp
.reload
  • .symfix 是設定通往 Microsoft 公開符號伺服器(https://msdl.microsoft.com/download/symbols)路徑的捷徑指令,並附帶指定的本機快取。OS 標準 DLL 的符號會從這裡自動下載。5
  • .sympath+ 會把自家 PDB 的存放位置附加到現有路徑之後。自家程式碼的 PDB 必須自行準備,不會出現在 Microsoft 的符號伺服器上。
  • .reload 重新載入,確認模組清單的符號狀態。

若要用環境變數永久設定,格式如下。這種方式適合 CI 或建置伺服器上的自動分析。

set _NT_SYMBOL_PATH=srv*C:\Symbols\Microsoft*https://msdl.microsoft.com/download/symbols;C:\Symbols\MyApp

符號是否正確讀取,可用 lm(loaded modules)指令列出模組清單,確認目標模組是否顯示為 pdb symbols。若仍顯示 deferred,表示符號還沒被解析。

3. 載入 SOS 擴充功能

.NET 應用程式的傾印檔,光靠原生的 WinDbg 指令是看不到「受管理堆積的內容」「C# 的堆疊框架」「例外物件的內容」的。填補這個缺口的正是 SOS(Son of Strike)擴充功能。堆積的調查、堆積損壞的偵測、執行環境內部資料型別的顯示,一直到執行中受管理程式碼狀態的掌握,都是透過 SOS 指令完成的。3

3.1 依執行環境而異

目標應用程式是 .NET Framework,還是 .NET (Core) / .NET 5+,會影響該載入的執行環境本體與 SOS 的來源。

目標 執行環境本體 載入指令
.NET Framework clr.dll .loadby sos clr
.NET Core / .NET 5+ coreclr.dll .loadby sos coreclr

.loadby 是從指定模組(clrcoreclr)所在的目錄,尋找同一位置的擴充功能 DLL(sos.dll)並載入的指令。優點是不需要打完整路徑,就能確實取得對應傾印檔採集環境的 SOS 版本。6

10.0.18317.1001 以後版本的 WinDbg、cdb,只要偵測到目標行程載入了 coreclr.dll(或 Linux/macOS 上的 libcoreclr.so),就會自動從 Microsoft Extension Gallery 載入對應 .NET 的擴充功能。6 上述的 .loadby 手動指令,適用於自動載入沒有生效,或使用較舊版本偵錯器的情況。

3.2 找不到 SOS 時

在自動載入不生效的環境中,可用 dotnet-sos 工具在本機安裝。

dotnet tool install --global dotnet-sos
dotnet-sos install

安裝完成後,也能在 WinDbg 中如下手動載入(較舊的偵錯器可能需要這個步驟)。7

.load %USERPROFILE%\.dotnet\sos\sos.dll

3.3 確認是否載入成功

!sos.help

或者,目標若是 Core 系列可嘗試 !Threads,Framework 系列可嘗試 !sosstatus,只要沒有出錯並回傳資訊,就代表載入成功。若指令在此以類似 Unable to find module 的錯誤失敗,幾乎都是符號路徑或執行環境不一致所致(例如傾印檔採集環境與手邊執行環境的位元數、版本不同)。在進入下一章的指令之前,卡在這一步的情況在實務上並不少見。

4. 讀取例外與堆疊 ── !clrstack 與 !pe

這是因未處理例外而當機的傾印檔的第一步。

!threads

先用 !Threads(在 lldb 環境中別名為 clrthreads)確認受管理執行緒清單,以及各執行緒的 Exception 欄位。8 若有執行緒持有例外,就切換到那個執行緒。

~5s
!clrstack

!CLRStack 只會顯示受管理程式碼的堆疊追蹤。9 若想連引數與變數都看到,可加上 -a(相當於 -l-p 的組合捷徑)。

!clrstack -a
  • 若看到自家程式碼的方法排列出來,就能直接讀出「在哪裡」「經過什麼呼叫路徑」當機。能顯示到原始檔名、行號,是因為符號正確讀取了(第 2 章)。CLRStack 是直接從 CLR 的中繼資料列舉受管理框架,因此符號有無並不影響框架是否顯示。符號不足時失去的只是原始檔名、行號,框架本身不會因此被省略。9
  • 若一個自家程式碼的框架都看不到,先別懷疑符號不足,該懷疑的是:選到的執行緒其實是沒有例外的另一個執行緒(選錯執行緒)、只有原生端當機而受管理框架本來就不存在,或是傾印檔的種類(如 Mini)沒有包含足夠的堆疊資訊。

接下來看例外物件本身。

!pe

!PrintException(簡稱 !pe)在未指定位址時,會顯示目前執行緒最後一次擲出的例外。可取得型別名稱、訊息、內部例外(用 -nested 顯示),一直到堆疊追蹤字串。10System.NullReferenceException 這種光看型別名稱看不出任何訊息的例外,就需要與 !clrstack -a 看到的區域變數值互相對照。

5. 追蹤堆積與洩漏 ── !dumpheap -stat 與 !gcroot

在「記憶體慢慢增加,數小時到數天後才當機」這類調查中是核心指令。要先分辨是等待 GC 還是真正的洩漏,前置的判斷方式已在「用 .NET 分辨 GC 等待與記憶體洩漏」中詳細寫過。這篇文章接續其後,屬於讀一份傾印檔並深入到「是什麼握著」的部分。

!dumpheap -stat

-stat 選項只顯示受管理堆積的統計摘要。會依型別的件數與合計大小,大致由多到少排列,因此可先鎖定「數量壓倒性多的型別」。11 實務上常見的有以下 2 種型態:

  • 業務類別本身在增加(例如 MyApp.Models.Customer 有數十萬件)── 表示某處持續存在強參考握著它
  • 只有 System.String 或陣列異常地多── 多半是排名靠前的業務類別內部資料所致,先懷疑業務類別,往往比逐一檢視個別執行個體更快

鎖定目標後,取得個別執行個體的位址。

!dumpheap -type MyApp.Models.Customer

然後調查該物件為什麼沒被 GC 回收,一直殘留著。

!gcroot 000001a2b3c4d5e0

!GCRoot 會搜尋整個受管理堆積與控制代碼表,找出所有能到達指定物件的根(堆疊上的變數、靜態欄位、GC 控制代碼等)。12 若輸出中看到快取用的靜態欄位或事件處理器的訂閱,那裡就是解除訂閱漏掉的候選犯人。以下這種「放進快取後就不釋放」的程式碼是典型範例:

public static class CustomerCache
{
    // 沒有解除的途徑,只會不斷增加的靜態字典
    private static readonly Dictionary<int, Customer> _cache = new();

    public static void Add(Customer c) => _cache[c.Id] = c;
}

!gcroot 的輸出中出現像 CustomerCache 這樣的靜態容器,就是考慮加上有效期限、上限件數,或改用 WeakReference 的材料。11

6. 原生當機的自動分析 ── !analyze -v

牽涉 C++ DLL、COM、廠商 SDK 的原生當機(如存取違規),一律先從這個指令入手。

!analyze -v

!analyze 是進行當機・例外自動分析的擴充指令,加上 -v 會顯示詳細內容。13 輸出中特別需要留意的有以下 3 項:

  • EXCEPTION_CODE / BUGCHECK_STR: 是哪一種異常(存取違規、堆疊溢位等)
  • FAULTING_IP / FOLLOWUP_IP: 實際當機的指令位址,以及對應的模組・函式名稱
  • MODULE_NAME / IMAGE_NAME: 當機處是自家模組,還是第三方 DLL

若當機發生在自家模組之外的廠商 DLL 中,要繼續往下追就需要廠商的 PDB(通常拿不到)。實務上比較現實的做法,是回溯到「呼叫端(自家程式碼最後傳遞的引數)」,懷疑傳入的值是否有異常。請用 !analyze -vSTACK_TEXT 欄位確認自家程式碼呼叫了什麼之後才當機。

!analyze 不只能用在例外導致的傾印檔上。懷疑是掛死(hang)時,選定目標執行緒後執行以下指令,會分析執行緒之間的封鎖關係。

!analyze -hang

7. 不使用 WinDbg 的選項 ── dotnet-dump analyze

若只想調查 .NET Core / .NET 5+ 的受管理程式碼(不涉及原生 DLL 或 COM),比 WinDbg 更輕量的 dotnet-dump 也是一個選項。

dotnet tool install --global dotnet-dump
dotnet-dump analyze C:\CrashDumps\MyApp\MyApp_20260702_101500.dmp

analyze 子指令會開啟一個預先安裝 SOS 的互動階段,clrstackdumpheapgcroot 等本文介紹過的多數指令,都能直接使用而不需要 ! 前綴。4

使用區分的大致標準如下:

觀點 WinDbg + SOS dotnet-dump analyze
原生堆疊框架 可見 不可見(僅受管理端)4
!analyze -v 的原生自動分析 可用 不可用
Linux 的傾印檔 可在 Windows 上用 WinDbg 分析(x64 傾印檔用 x64 版、Arm64 傾印檔用 x64 版、x86 傾印檔用 x86 版)14 支援(使用相同平台位元數的工具)14
macOS 的傾印檔 不支援(WinDbg 的 Linux 傾印支援不含 macOS) 支援(.NET 5 以後)4
導入的輕便度 安裝程式或 winget dotnet global tool 一道指令
納入跨平台 CI 較費工 較容易

「可能牽涉 COM、P/Invoke、原生 DLL」就用 WinDbg,「純受管理程式碼的記憶體洩漏調查,且想在 CI 或多平台上執行」就用 dotnet-dump analyze,是實務上的分界線。兩者共用 SOS 的指令體系,因此在一邊學到的指令,在另一邊幾乎都能直接沿用。若要處理在 macOS 上採集的傾印檔,WinDbg 不在選項之內,這一點請留意,只能選 dotnet-dump(或 LLDB)。

8. 符號讀不到就什麼都做不了

到目前為止的部分步驟,即使符號(PDB)沒有正確載入,也還算能運作。如第 4 章所寫,!clrstack!dumpheap -stat!gcroot 是直接讀取 CLR 的中繼資料與堆積資料,因此即使沒有 PDB,框架與型別資訊本身仍會顯示。PDB 缺失時失去的是受管理程式碼的原始檔名、行號,以及原生框架、原生模組的符號名稱(只顯示位址而非函式名稱)。若手邊只有傾印檔與執行檔,在「先想掌握發生了什麼事」的場景下,不必卡在尋找 PDB 上,直接從 !threads!clrstack 著手也無妨。話雖如此,若想追到原始碼行數以確定問題所在,那就是另一回事了。第 2 章在 .sympath+ 加上了自家 PDB 的路徑,但實務上光是「不知道對應已發佈 EXE/DLL 的 PDB 在哪裡」,就足以讓調查卡在無法確定原始碼行數的階段,這種情況比收集篇中寫到的還要頻繁。

PDB 本身持有與不持有什麼、Portable PDB,以及 Source Link(把原始碼控管的中繼資料嵌入組件內,讓偵錯器能直接取得對應建置時間點提交的原始碼的機制),都已整理在「PDB 是什麼」這篇文章中。15 若要把傾印分析納入持續性的運維流程,按建置版本保存 PDB、並啟用 Source Link,這件事的重要性和收集設定本身不相上下。這一步偷懶的話,!clrstack 的輸出就完全不會出現原始碼行數,只能靠位址與型別名稱摸索。

9. 實例 ── 控制代碼洩漏調查中的傾印分析

之前寫過的「工業相機長期運轉當機調查 - 控制代碼洩漏篇」,是長時間運轉後突然當機的工業相機控制應用程式調查案例,主犯不是記憶體洩漏而是控制代碼洩漏。這類調查中傾印檔能發揮作用,是在以下組合成立的時候:

  1. 先用 !dumpheap -stat 確認受管理堆積端正常(各型別的件數、大小沒有持續增加)
  2. 若行程的控制代碼數量仍持續增加,就能區分出洩漏的不是受管理物件,而是 OS 控制代碼(檔案、事件、相機 SDK 內部配置的控制代碼等)
  3. 若還殘留持有 SafeHandle 的受管理包裝物件,就用 !gcroot 追蹤該包裝物件的 GC 根,找出「應該釋放卻仍有參考殘留」的位置

也就是說,!dumpheap -stat 是判斷「是不是受管理堆積在增加」的分岔點,一旦確定不是,調查的主軸就會轉移到像 Application Verifier 這類原生邊界異常偵測工具上。這套異常路徑測試基礎的建置方式,在「用 Application Verifier 建置 Windows 異常路徑測試基礎」中有介紹。傾印分析扮演的是「掌握現在發生的狀態」,Application Verifier 扮演的是「提前重現異常」,兩者並用是長時間運轉系統故障調查的定式。

10. 總結

當機傾印檔「收集」比較容易,「讀懂」則需要更多時間才能熟練,但型態並不算多。

  1. 安裝 WinDbg,並在符號路徑中同時放入 Microsoft 的公開符號伺服器與自家 PDB(第 2 章)
  2. .NET 應用程式要載入 SOS 擴充功能(.loadby sos clr / .loadby sos coreclr,或自動載入。第 3 章)
  3. 因例外而當機就走 !clrstack!pe,記憶體增加就走 !dumpheap -stat!gcroot,原生當機就從 !analyze -v 入手(第 4~6 章)
  4. 若用途純粹是受管理端的調查,也可以考慮輕量的 dotnet-dump analyze(第 7 章)

而支撐這一切的基礎,就是 PDB 與符號的管理。在建置傾印收集設定的同時,一併決定好按建置版本保存 PDB、啟用 Source Link,實際發生故障時的調查時間會有很大差異。若自家難以進行分析,或抽不出時間,只要把傾印檔與日誌一併寄來,我們也能協助分析。

相關文章

相關諮詢領域

合同會社小村軟體提供結合當機傾印檔與日誌的缺陷原因調查、僅在長期運轉後才發生的故障排除,以及傾印檔・PDB 保存與分析體制本身的設計諮詢。

參考連結

  1. Microsoft Learn, Install the Windows debugger. 說明透過 winget / Microsoft Store 安裝 WinDbg 的方式、支援的 OS(Windows 10 1607 以後・Windows 11)與架構(x64・ARM64),以及自動更新的行為。  2 3

  2. Microsoft Learn, Symbol path for Windows debuggers. 說明透過 _NT_SYMBOL_PATH 環境變數設定符號路徑的方式,以及 .symfix 指令可設定通往公開符號伺服器的預設路徑。  2

  3. Microsoft Learn, SOS debugging extension. 說明 SOS 擴充功能可用於受管理堆積資訊的收集、堆積損壞的偵測、執行環境內部資料型別的顯示,以及 WinDbg 上的語法為 ![command]。  2

  4. Microsoft Learn, Dump collection and analysis utility (dotnet-dump). 說明 dotnet-dump analyze 提供可直接使用 SOS 指令的互動階段,但因不是原生偵錯器而無法顯示原生堆疊框架,以及 macOS 支援始於 .NET 5。  2 3 4

  5. Microsoft Learn, Microsoft public symbol server. 說明 srv*DownstreamStore*https://msdl.microsoft.com/download/symbols 格式的符號路徑語法,以及 .symfix 可附帶本機快取進行設定。 

  6. Microsoft Learn, Debugging Managed Code Using the Windows Debugger. 說明 .NET Framework 的執行環境為 clr.dll、.NET Core/.NET 5+ 的執行環境為 coreclr.dll.loadby 可從模組附近載入擴充功能,以及 WinDbg 10.0.18317.1001 以後版本的自動載入機制。  2

  7. Microsoft Learn, SOS installer (dotnet-sos). 說明透過 dotnet-sos install 在本機安裝 SOS 擴充功能,以及在較舊版本偵錯器中手動載入的指令。 

  8. Microsoft Learn, SOS debugging extension - Commands. 說明 Threads(在 lldb 環境中別名為 clrthreads)指令會列出各執行緒的 ID、網域、最後擲出的例外等資訊。 

  9. Microsoft Learn, SOS debugging extension - Commands. 說明 CLRStack 指令只顯示受管理程式碼的堆疊追蹤,-a 選項可同時顯示區域變數與引數,以及符號(SYMOPT_LOAD_LINES)僅影響原始檔名、行號能否顯示,與框架本身是否顯示無關。  2

  10. Microsoft Learn, SOS debugging extension - Commands. 說明 PrintException(pe)指令在省略位址時,會顯示目前執行緒最後一次擲出的例外,並可用 -nested 顯示巢狀例外。 

  11. Microsoft Learn, Debug a memory leak in .NET 以及 Dump collection and analysis utility (dotnet-dump) - Analyze memory leaks and allocations. 說明 dumpheap -stat 依型別顯示件數與合計大小的統計,以及以此為起點推進調查的方式。  2

  12. Microsoft Learn, SOS debugging extension - Commands. 說明 GCRoot 指令會搜尋整個受管理堆積與控制代碼表,找出通往指定物件的參考(根)。 

  13. Microsoft Learn, Using the !analyze Extension 以及 !analyze (WinDbg). 說明 !analyze -v 的當機・例外自動分析,輸出中 FAULTING_IPMODULE_NAME 等欄位的意義,以及用於掛死調查的 !analyze -hang。 

  14. Microsoft Learn, Debug Linux dumps. 說明可在 Windows 上用 WinDbg 或 dotnet-dump 分析 Linux 的傾印檔,且需要依採集環境(x64/Arm64/x86)的位元數使用對應版本的工具。  2

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

.NET 應用程式的當機傾印檔該從哪個指令開始讀?
調查的型態大致分成 3 種。若因未處理例外而當機,先用 !threads 找出持有例外的執行緒,再用 !clrstack 顯示受管理的堆疊,並以 !pe 確認例外物件的型別、訊息與內部例外。若是記憶體持續增加的調查,先用 !dumpheap -stat 找出數量偏多的型別,再用 !gcroot 找出握住該物件的根(靜態欄位或事件處理器的訂閱等)。若是原生當機,則從 !analyze -v 的自動分析入手。
在 WinDbg 中要如何載入 SOS 擴充功能?
.NET Framework 用 .loadby sos clr,.NET Core/.NET 5+ 用 .loadby sos coreclr。10.0.18317.1001 以後版本的 WinDbg,只要偵測到目標行程載入了 coreclr.dll,就會自動載入 SOS。若自動載入不生效,可以用 dotnet-sos 工具在本機安裝後手動 .load。是否成功載入,可以用 !sos.help 或 !Threads 執行後不出錯並回傳資訊來確認。
沒有 PDB(符號)也能做傾印分析嗎?
在一定程度上可以。!clrstack・!dumpheap -stat・!gcroot 是直接讀取 CLR 的中繼資料與堆積資料,因此即使沒有 PDB,堆疊框架與型別資訊仍會顯示。失去的是受管理程式碼的原始檔名、行號,以及原生堆疊框架的符號名稱。若想追到原始碼行數以確定問題所在,就需要在符號路徑同時放入 Microsoft 的公開符號伺服器與自家 PDB 的位置。按建置版本保存 PDB,並啟用 Source Link,在運維上相當重要。
dotnet-dump analyze 與 WinDbg 該如何區分使用?
若可能牽涉 COM、P/Invoke 或原生 DLL,用 WinDbg;若是純受管理程式碼的調查,dotnet-dump analyze 是基本方針。dotnet-dump 只需一道 dotnet global tool 指令即可安裝,clrstack、dumpheap 等多數 SOS 指令都能直接沿用,但無法處理原生堆疊框架,也無法使用 !analyze -v。在 macOS 上採集的傾印檔無法用 WinDbg 分析,因此只能選 dotnet-dump 或 LLDB。兩者共用 SOS 的指令體系,因此學會的指令幾乎可以互通使用。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽