解讀 Windows 錯誤碼 ── Win32、HRESULT、NTSTATUS 三層結構

· · Windows, 錯誤碼, HRESULT, NTSTATUS, Win32 API, 故障調查, 偵錯, Windows 開發

更新紀錄(僅初版,2026年08月20日 發布)
初次發布
引用本文(DOI(已登錄存檔): 10.5281/zenodo.22176059)

以下 DOI 指向先前登錄的存檔,內容可能與目前正文不同。引用目前正文時,請使用本頁網址。

Go Komura(2026)。〈解讀 Windows 錯誤碼 ── Win32、HRESULT、NTSTATUS 三層結構〉。小村軟體有限公司。 https://comcomponent.com/zh-TW/blog/windows-error-codes-win32-hresult-ntstatus/

DOI(已登錄存檔)
10.5281/zenodo.22176059
DOI(上次登錄版本)
10.5281/zenodo.22176060

「應用程式畫面上出現 0x80004005 這個錯誤,這是什麼意思?」── 在故障調查裡,這是經典的諮詢。把數字原樣拿去搜尋,排出來的是 Windows Update、共用資料夾、VBA、資料庫連線等互不相干場合的處理方法。

搜尋結果之所以分散,是因為 0x80004005(E_FAIL)是一個只表示「詳情不明的失敗」的通用代碼。相對地,0x80070005 可以分解成「把 Win32 錯誤的第 5 號=拒絕存取重新包成 HRESULT 的結果」。外觀相似,但能從代碼讀出的資訊量並不相同。

Windows 有 Win32 錯誤碼、HRESULT、NTSTATUS 三套體系,代碼跨層傳遞時會被轉換。想讓調查更快,與其死背數字,不如學會分辨「是哪一層的誰回傳的」「重新包裝之前的代碼是什麼」。

本文是寫給中小企業資訊人員與 Windows 應用程式開發者的實務指南。依據 2026 年 8 月時點的 Microsoft Learn 與公開規格書 [MS-ERREF],依序串起三套體系的分辨方法、HRESULT 的分解、與 .NET 例外的關係,以及用 err.exe 和 PowerShell 查詢的做法。

1. 先講結論

首先要掌握的是以下三點。

  1. 對齊記法,分辨代碼體系。Win32 錯誤碼、HRESULT、NTSTATUS 是不同的體系。十進位與十六進位是同一個值的不同記法,HRESULT 也可能以有號負數顯示。先統一成 8 位數的十六進位,再對照回傳它的 API 與出現的位置來讀。123
  2. 0x8007xxxx 要分解,E_FAIL 則轉向脈絡調查。0x80070005 是 FACILITY_WIN32(7)的 HRESULT,低 16 位元是 5=ERROR_ACCESS_DENIED。相對地,0x80004005(E_FAIL)是「未指定的失敗」,光靠代碼縮小不了原因。456
  3. 代碼的意義,要和失敗的操作、對象成組一起查。同樣是錯誤 5,原因可能是 ACL、權限提升、資安軟體等各種情況。錯誤 2 的對象有時是相依 DLL,檔案被占用則會以錯誤 32 的形式出現。查到名稱之後,就用記錄檔或 Process Monitor 走向「哪個 API 對什麼失敗」。1

工具則依情況選用:Windows 內建的 certutil -error 與 net helpmsg、PowerShell 的 Win32Exception、開發機上的 err.exe,以及傾印分析途中 WinDbg 的 !error。789 就算已經變成 .NET 的例外,原本的 HRESULT 仍可從 Exception.HResult 追回。10

依目的決定讀法

想知道的事 該讀的章節
想分辨手上的代碼並立刻查詢 第 2 章對齊記法,再到第 7 章的命令與第 8 章的調查步驟
想把 Win32 API 的失敗正確寫進記錄檔 第 3 章的 GetLastError 與 FormatMessage、6.3 節的 P/Invoke
想理解 0x80004005、0x80070005 與 .NET 例外的關係 第 4 章的 HRESULT 分解、第 6 章的 COM 與 .NET
想查 0xC0000005 這類當機的代碼 第 5 章的 NTSTATUS 與 STOP 代碼、7.4 節的 WinDbg
想避開調查時常見的混淆 第 9 章的誤讀範例

用一句話總結,Windows 錯誤碼調查的套路就是「把記法對齊成十六進位 → 判斷是哪一層的代碼 → 分解取出本質的代碼 → 連同脈絡一起讀」。

圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 18 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle

2. Windows 有三套錯誤碼體系

先看整體樣貌。同樣的數值,依它是以哪一套體系的值傳出來的,讀法就不同。把主要的回傳者與典型外觀並列,會得到下表。

體系 主要回傳者 典型外觀 代表例子
Win32 錯誤碼 Win32 API(GetLastError)、命令的結束碼 較小的十進位數(0~15999) 5 = ERROR_ACCESS_DENIED
HRESULT COM 元件、殼層、安裝程式、許多框架 以 0x8 開頭的 8 位十六進位,或十進位負數 0x80004005 = E_FAIL
NTSTATUS 核心、驅動程式、原生 API(ntdll) 錯誤是以 0xC 開頭的 8 位十六進位 0xC0000005 = STATUS_ACCESS_VIOLATION

這張表的「外觀」是尋找體系的線索。它和第 8 章的判定表一樣,只當第一候選使用,還要和回傳它的 API 或元件資訊對照。

從歷史來看,這三套是一層層疊上來的:承接 MS-DOS 編號的 Win32 錯誤碼、NT 核心內部的 NTSTATUS,以及導入 COM 時為了把成功/失敗與發生來源塞進 32 位元而設計的 HRESULT。

在現今的 Windows 上,核心的 NTSTATUS → Win32 錯誤碼 → COM 層的 HRESULT 這條轉換每天都在發生。從上層看到的代碼往下層回溯,就是調查的基本功。114

三套體系之間的轉換流程Win32 子系統把核心回傳的 NTSTATUS 轉成 Win32 錯誤碼,COM 層再把它包成 HRESULTWin32 子系統轉換COM 層包起來核心與驅動程式NTSTATUS(錯誤是 0xC…)Win32 錯誤碼(5 等)HRESULT(0x8007xxxx)

圖 1: 跨層轉換流程。核心的 NTSTATUS 變成 Win32 錯誤,再被包成 HRESULT。

2.1. 習慣在十進位與十六進位之間互換閱讀

在調查體系之前,先消除記法的搖擺。因為同一個代碼會以十進位、十六進位、有號負數等形式顯示。

顯示出來的值 換成另一種記法 意義
錯誤 5 0x5 ERROR_ACCESS_DENIED
錯誤 1223 0x4C1 ERROR_CANCELLED
0x80070005 -2147024891 同一個 HRESULT

用 PowerShell 的話,一行就能換算。

# 十進位 → 十六進位
'0x{0:X8}' -f 1223          # 0x000004C1
'0x{0:X8}' -f -2147024891   # 0x80070005 (負數=把 HRESULT 轉成十六進位)

# 十六進位 → 十進位
0x4C1                        # 1223

看到以「-214…」開頭的十進位負數,就反射性地轉成十六進位。 光是這樣,就能大幅減少在調查入口迷路的情況。

同一代碼的三種外觀十進位錯誤 5、十六進位 0x5,以及 0x80070005 的低 16 位元,都指向同一個 ERROR_ACCESS_DENIED十進位記法: 錯誤 5ERROR_ACCESS_DENIED十六進位記法: 0x50x80070005 的低 16 位元記法不同,代碼相同

圖 2: 十進位、十六進位與 HRESULT 的低 16 位元,只是同一代碼的不同記法。

3. Win32 錯誤碼 ── GetLastError 與 FORMAT_MESSAGE

3.1. GetLastError 的基本行為

先確認傳回值,再保存最後錯誤

CreateFile 等許多 Win32 API 以傳回值(FALSE、NULL、INVALID_HANDLE_VALUE 等)表示失敗,並把詳細留在每個執行緒各自持有的「最後錯誤碼」裡。這種形式的 API,要在確認失敗之後立刻用 GetLastError 取出。12

不過,取得錯誤的方式要逐一確認每個 API。例如 RegOpenKeyEx 不是用最後錯誤,而是傳回值本身就是錯誤碼。請和使用 GetLastError 的例子分開處理。13

使用 GetLastError 時,要注意以下兩點。12

  1. 在失敗之後立刻讀。中間若插入另一次 API 呼叫(例如寫記錄檔的函式),那次呼叫可能覆寫最後錯誤碼。
  2. 不要依賴成功時的值。有些 API 成功時會把最後錯誤碼清成 0,有些則完全不動它。原則是先用傳回值確認失敗再讀。
失敗後立刻讀 GetLastError從傳回值確認失敗後,立刻用 GetLastError 取出最後錯誤碼,中間不要插入另一次 API 呼叫Win32 APIAppWin32 APIApp中間插入另一次 API 可能覆寫CreateFile 呼叫失敗傳回值GetLastError代碼 5

圖 3: 失敗後立刻讀最後錯誤碼。中間插入另一次 API 呼叫可能覆寫它。

把保存下來的代碼連同訊息一起寫進記錄檔

要從代碼取得訊息字串,就用加上 FORMAT_MESSAGE_FROM_SYSTEM 旗標的 FormatMessage。順序是先把代碼保存下來,之後再轉成字串。1

#include <windows.h>
#include <stdio.h>

void PrintLastError(const wchar_t* apiName)
{
    DWORD code = GetLastError();   // 在失敗之後立刻呼叫(中間不插入其他 API)
    wchar_t message[512] = L"";
    FormatMessageW(
        FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr, code, 0, message, 512, nullptr);
    wprintf(L"%s failed: %lu (0x%08lX) %s", apiName, code, code, message);
}

在自家應用程式的記錄檔裡,像這樣把十進位、十六進位與訊息本文都留下來,日後調查會快上一截。

從代碼查出訊息並寫進記錄對 FormatMessage 指定 FORMAT_MESSAGE_FROM_SYSTEM 旗標以取得錯誤碼的訊息字串,並把十進位、十六進位與訊息本文留在記錄裡錯誤碼(例: 5)用 FormatMessage 取得字串訊息本文寫進記錄十進位、十六進位與本文一起寫

圖 4: 用 FormatMessage 把錯誤碼轉成訊息字串,並把十進位、十六進位與本文一起留在記錄裡。

3.2. 現場不斷出現的代表代碼

Win32 錯誤碼定義在 0~15999 的範圍內,Microsoft Learn 上有完整清單。1 其中在故障調查裡反覆碰面的,是下面這些面孔。

十進位 十六進位 符號 意義
2 0x2 ERROR_FILE_NOT_FOUND 找不到指定的檔案
3 0x3 ERROR_PATH_NOT_FOUND 找不到指定的路徑
5 0x5 ERROR_ACCESS_DENIED 存取被拒絕
32 0x20 ERROR_SHARING_VIOLATION 另一個處理程序正在使用,無法存取
87 0x57 ERROR_INVALID_PARAMETER 參數不正確
122 0x7A ERROR_INSUFFICIENT_BUFFER 傳入的緩衝區太小
998 0x3E6 ERROR_NOACCESS 對記憶體位置的無效存取
1223 0x4C1 ERROR_CANCELLED 使用者取消了操作

這裡最容易混淆的是第 5 號與第 998 號。998(ERROR_NOACCESS)不是「拒絕存取」,而是記憶體存取違規在 Win32 層的表達。它相當於後面會談到的 NTSTATUS STATUS_ACCESS_VIOLATION 被轉換到 Win32 層之後的樣子。

另外,1223(ERROR_CANCELLED)會在使用者於 UAC 提升權限對話方塊選「否」等情況出現。不要單純當成故障,而要讀成表示操作「被中止」的代碼。

錯誤 998 與 5 是不同的事998 是記憶體存取違規,也就是 NTSTATUS 存取違規轉到 Win32 層,與表示拒絕存取的 5 意義不同轉到 Win32 層NTSTATUS 0xC0000005錯誤 998(ERROR_NOACCESS)意義是記憶體存取違規錯誤 5(拒絕存取)權限問題。與 998 不同

圖 5: 錯誤 998 是 NTSTATUS 存取違規轉到 Win32 層,與拒絕存取的 5 是不同的事。

3.3. 同一代碼會隨脈絡改變意義

光是背下代表代碼,還是無法鎖定原因。代碼能告訴你的只到「失敗的種類」,再往下還留著要查的對象。

代碼 會隨脈絡改變的部分 接下來要確認的事
錯誤 5(拒絕存取) NTFS 的 ACL 不足、沒有系統管理員權限就寫入受保護區域、防毒軟體或 AppLocker 封鎖、服務帳戶權限不足等 是哪個 API 對哪個對象的存取被拒絕
錯誤 2(找不到檔案) 不一定是指定的那個檔案,也可能是隱含載入的相依 DLL、因 32 位元/64 位元登錄檔重新導向而看錯位置的設定檔、環境變數展開失敗的路徑等 「哪一個檔案」沒找到
錯誤 32(共用違規) 有另一個處理程序正在使用該對象 是「哪個處理程序」握著它
錯誤 5 的原因由脈絡決定即使同樣是拒絕存取,也有 ACL 不足或缺少系統管理員權限等數個原因候選,需要特定哪個 API 對什麼失敗錯誤 5(拒絕存取)ACL 不足缺少系統管理員權限安全性產品封鎖服務權限偏低Procmon: 失敗對象

圖 6: 代碼只告訴你「失敗的種類」。錯誤 5 有數個原因候選,必須特定對象。

用來實測「哪個 API、對哪個物件名稱、傳回哪個結果」的工具就是 Process Monitor。用法在「Process Monitor(ProcMon)實戰指南」裡有詳細說明。查錯誤碼意義的工作,和鎖定失敗對象的工作,請當成同一輛車的兩個輪子。

4. HRESULT ── 讀懂塞進 32 位元的結構

4.1. 位元配置

HRESULT 是把成功/失敗、發生來源與詳細代碼塞進單一 32 位元值的格式。先掌握用 S 位元看成功/失敗、用 Facility 看發生來源、用 Code 看細節,後面的分解範例就容易跟上。公開規格書 [MS-ERREF] 的配置如下。2

位元位置 名稱 意義
31 S Severity。0 = 成功,1 = 失敗
30 R 保留(對應 NTSTATUS 時是 severity 的一部分)
29 C Customer 位元。1 表示 Microsoft 以外的人定義的代碼
28 N 1 表示對應進 HRESULT 空間的 NTSTATUS 值
27 X 保留(0)
26–16 Facility 表示來源的 facility 代碼(11 位元)
15–0 Code facility 內的詳細代碼(16 位元)

最高位的 S 位元為 1,也就是十六進位記法從 0x8 以上開頭的 HRESULT 是失敗。把它當成有號 32 位元整數顯示會變成負數——這就是前面說的「-214…」的真身。

S 位元與負數顯示的關係失敗 HRESULT 的最高位 S 位元為 1,因此十六進位從 0x8 以上開始,以有號 32 位元整數顯示則為負數S 位元 = 1(失敗)十六進位從 0x8 以上開始有號顯示為負數看到負數就轉十六進位再讀

圖 7: 失敗 HRESULT 因 S 位元為 1 而從 0x8 以上開始,有號顯示為負數。

Facility 是下一步該查哪份資料的線索

Facility 的代表值如下。7 就是包起來的 Win32 錯誤,4 就是各介面自行定義,像這樣分辨著讀。5

Facility 值 十六進位外觀 意義
FACILITY_NULL 0 0x8000xxxx 廣泛共通的代碼(E_FAIL、E_UNEXPECTED 等)
FACILITY_RPC 1 0x8001xxxx RPC 來源
FACILITY_ITF 4 0x8004xxxx 介面定義的錯誤(意義依介面而定)
FACILITY_WIN32 7 0x8007xxxx 包起來的 Win32 錯誤碼
FACILITY_WINDOWS 8 0x8008xxxx 額外的 Microsoft 定義介面

4.2. 分解 0x80004005 與 0x80070005

把這兩個看起來相似的代碼,拆成 S、Facility、Code 來比較。

讀取的項目 0x80004005 0x80070005
S 1(失敗) 1(失敗)
Facility 0(FACILITY_NULL) 7(FACILITY_WIN32)
Code 0x4005 0x0005=5
定義 E_FAIL:未指定的失敗 E_ACCESSDENIED:拒絕存取
下一步調查 查回傳它的元件與伴隨的記錄檔 當成 Win32 錯誤 5,查被拒絕的操作與對象

0x80004005 光靠代碼縮小不了原因。Facility 是 (0x80004005 >> 16) & 0x7FF = 0,屬於 FACILITY_NULL 的通用代碼 E_FAIL。它不帶有「未指定的失敗(Unspecified failure)」以上的細節,所以分解完就把調查的重心移到「是哪個元件回傳的」「同一時刻的事件記錄檔或應用程式記錄檔裡有沒有細節」。6

0x80070005 可以一路追到 Win32 錯誤 5。因為 Facility=7、Code=5,可知它是把 ERROR_ACCESS_DENIED 重新包成 HRESULT 的值。別名 E_ACCESSDENIED 指的也是同一個值。6

同樣是「失敗」,分解後得到的資訊量並不相同。不要把 E_FAIL 認定成拒絕存取,而要依回傳的值來選下一步的調查。

0x80004005 與 0x80070005 的分解0x80004005 是通用 FACILITY_NULL 代碼 E_FAIL,沒有細節,應轉入脈絡調查;0x80070005 是 FACILITY_WIN32,可看成 Win32 錯誤編號 5 拒絕存取的包裝0x80004005Facility=0(FACILITY_NULL)Code=0x4005 → E_FAIL未指定失敗。轉入脈絡調查0x80070005Facility=7(FACILITY_WIN32)Code=0x0005 → 5ERROR_ACCESS_DENIED

圖 8: 同樣是「失敗」,分解後資訊量不同。0x80070005 可以走到 Win32 錯誤編號 5。

4.3. 最重要的模式:0x8007xxxx = HRESULT_FROM_WIN32

把 Win32 的失敗重新包成 HRESULT

為了把下層的 Win32 錯誤,傳給回傳 HRESULT 的 COM 方法或 .NET 執行階段,winerror.h 準備了 HRESULT_FROM_WIN32。在下面這些失敗代碼的例子裡,它會把低 16 位元放 Win32 錯誤、Facility 放 7、S 位元放 1。4

HRESULT_FROM_WIN32 如何運作把 Win32 錯誤碼存進低 16 位元,Facility 設為 7、S 位元設為 1,組出 0x8007xxxx HRESULTWin32 錯誤碼(例: 5)存進低 16 位元Facility 設為 7S 位元設為 10x80070005

圖 9: HRESULT_FROM_WIN32 把 Win32 錯誤存進低 16 位元,並設定 Facility=7 與 S 位元。

ERROR_ACCESS_DENIED (5)        --HRESULT_FROM_WIN32-->  0x80070005
ERROR_SHARING_VIOLATION (32)   --HRESULT_FROM_WIN32-->  0x80070020
ERROR_INVALID_PARAMETER (87)   --HRESULT_FROM_WIN32-->  0x80070057 (= E_INVALIDARG)
ERROR_OUTOFMEMORY (14)         --HRESULT_FROM_WIN32-->  0x8007000E (= E_OUTOFMEMORY)

反向解讀時,取出低 16 位元

要從 0x8007xxxx 讀出原本的 Win32 錯誤,就用 PowerShell 取出低 16 位元。

0x80070005 -band 0xFFFF   # 5 → ERROR_ACCESS_DENIED
0x80072EE7 -band 0xFFFF   # 12007 → ERROR_INTERNET_NAME_NOT_RESOLVED (WinINet)

如第二個例子所示,WinINet 與 WinHTTP 的錯誤(12000 號段)也定義在 Win32 錯誤碼空間裡1,所以網路相關的 0x8007xxxx 也能用同一套步驟分解。把「看到 0x8007 就把低 4 位轉成十進位」練成身體記憶,是本文最希望你帶走的一項實務技能。

0x8004xxxx 要去查回傳它的元件的資料

同樣的讀法,不能原封不動套到 0x8004xxxx(FACILITY_ITF)上。因為這邊定義意義的主體是各個介面,同一個 32 位元值只要回傳者不同,意思就可能不同。5

遇到不熟悉的 0x8004xxxx,不要只憑通用搜尋就下判斷,請查回傳它的元件(程式庫、驅動程式 SDK、伺服器產品)的文件。

0x8007 與 0x8004 的查法不同FACILITY_WIN32 的 0x8007xxxx 可機械地分解低 16 位元來讀,但 FACILITY_ITF 的 0x8004xxxx 每個介面定義意義的單位不同,所以要查回傳元件的資料7, WIN324, ITFFacility 是?把低 16 位元轉成十進位意義依回傳者而異當 Win32 錯誤讀查回傳者的資料

圖 10: 0x8007xxxx 可機械分解;0x8004xxxx 要查回傳元件的資料。

5. NTSTATUS ── 核心層代碼與當機的世界

5.1. 配置與 Severity

NTSTATUS 是核心、裝置驅動程式與 ntdll 原生 API 使用的 32 位元代碼,配置和 HRESULT 看似相同其實不同。3

位元位置 名稱 意義
31–30 Sev Severity。00 = 成功,01 = 資訊,10 = 警告,11 = 錯誤
29 C Customer 位元
28 N 保留(0,以便對應到 HRESULT)
27–16 Facility Facility(12 位元)
15–0 Code 詳細代碼

與 HRESULT 最大的差別在於,嚴重性是 2 位元,分成成功、資訊、警告、錯誤四種。從十六進位開頭的第一位來看,0xC… 是錯誤(11)、0x8… 是警告(10)、0x4… 是資訊(01)、0x0~0x3… 則是成功。

因此,不能只憑「以 0x8 開頭」的外觀就斷定是失敗的 HRESULT。NTSTATUS 的 0x80000003(STATUS_BREAKPOINT)是中斷點例外,嚴重性不是「錯誤」而是「警告」。314

NTSTATUS 可從前導數字讀出種類因為 severity 是 2 位元,NTSTATUS 可依前導十六進位數字讀成:0xC 為錯誤、0x8 為警告、0x4 為資訊、0x0 到 0x3 為成功0xC0x80x40x0–0x3前導十六進位是?錯誤警告資訊成功例: 0x80000003 是警告

圖 11: NTSTATUS 可從前導十六進位數字讀出種類。0x80000003 是「警告,不是錯誤」。

5.2. 會遇到的地方 ── 例外代碼、STOP 代碼與事件記錄

以下分成例外代碼、STOP 代碼與 Process Monitor,看看會在哪裡遇到 NTSTATUS。

應用程式當機的例外代碼

事件記錄檔的「Application Error(事件識別碼 1000)」裡記錄的「例外代碼: 0xc0000005」就是 NTSTATUS。當機或啟動失敗時常見的代表值有下面這些。14

值 符號 意義
0xC0000005 STATUS_ACCESS_VIOLATION 存取違規(不合法的記憶體存取)
0xC0000135 STATUS_DLL_NOT_FOUND 找不到必要 DLL,無法啟動
0xC00000FD STATUS_STACK_OVERFLOW 堆疊溢位
0xC0000374 STATUS_HEAP_CORRUPTION 堆積損毀

藍畫面的 STOP 代碼屬於另一套體系

STOP 代碼(錯誤檢查代碼)乍看相似,但像 0x0000009F(DRIVER_POWER_STATE_FAILURE)那樣,是與 NTSTATUS 不同的獨立編號體系,要查專用的參考資料。15

重點是分清楚「0xC0000005 是 NTSTATUS,STOP 0x9F 是錯誤檢查代碼」,不要拿 STOP 代碼去查 NTSTATUS 的表。

在 Process Monitor 裡出現在 Result 欄

Procmon 的 Result 欄出現的 NAME NOT FOUND 或 ACCESS DENIED,是核心回傳的 NTSTATUS(STATUS_OBJECT_NAME_NOT_FOUND 或 STATUS_ACCESS_DENIED)的顯示名稱。在這裡可以用 NTSTATUS 的詞彙觀察檔案 I/O 的失敗,並確認它再被轉成 Win32 錯誤送到應用程式這種層與層之間的對應。

分辨例外代碼與 STOP 代碼把事件記錄的例外代碼當 NTSTATUS 讀;藍畫面 STOP 代碼則查專用的錯誤檢查代碼參考,那是另一套體系例外代碼STOP 代碼代碼出現在哪?當 NTSTATUS 讀查錯誤檢查代碼表例: 0xC0000005例: 0x0000009F

圖 12: 事件記錄的例外代碼是 NTSTATUS;藍畫面 STOP 代碼是另一套體系。不要查錯表。

例外代碼之後的調查,也就是當機傾印的擷取與分析,請參考「Windows 應用的 crash dump 收集入門」與「用 WinDbg + SOS 解讀當機傾印檔」。

5.3. 與 HRESULT 的關係 ── N 位元與 RtlNtStatusToDosError

從 NTSTATUS 交給其他體系的路徑有兩條。對應到 HRESULT 和轉換成 Win32 錯誤,要分開來想。

路徑 要做的事 例子
對應到 HRESULT 空間 用 HRESULT_FROM_NT 設定 N 位元(0x10000000) 0xC0000005 → 0xD0000005
轉換成 Win32 錯誤 用 RtlNtStatusToDosError 轉成對應的編號 STATUS_ACCESS_VIOLATION → ERROR_NOACCESS(998)

對應到 HRESULT 時,會設定 N 位元把 NTSTATUS 值帶進 HRESULT 空間。因此,看到以 0xD 開頭的 HRESULT,步驟是剝掉 N 位元,當成 NTSTATUS 讀。2

轉換成 Win32 錯誤 時,用的是 ntdll 的 RtlNtStatusToDosError。沒有對應的值會變成 ERROR_MR_MID_NOT_FOUND。11 0xC0000005 會轉成 998,STATUS_OBJECT_NAME_NOT_FOUND(0xC0000034)會轉成 ERROR_FILE_NOT_FOUND(2)。核心端豐富的詞彙,到了 Win32 層有時會被收斂成較粗的分類,因此要區分轉換前後來讀。

從 NTSTATUS 到其他層的兩座橋NTSTATUS 以兩種方式傳到其他層:設定 N 位元對應進 HRESULT 空間,以及由 RtlNtStatusToDosError 轉成 Win32 錯誤碼設定 N 位元RtlNtStatusToDosErrorNTSTATUS(0xC0000005)HRESULT(0xD0000005)Win32 錯誤 998(ERROR_NOACCESS)沒有對應時為 ERROR_MR_MID_NOT_FOUND

圖 13: NTSTATUS 的橋有兩座。以 0xD 開頭的,剝掉 N 位元後當 NTSTATUS 讀。

6. COM 與 .NET ── 錯誤碼如何對應到例外

6.1. COM 風格 ── HRESULT + IErrorInfo

COM 的方法基本上都回傳 HRESULT。不過,只靠 32 位元的代碼能傳達的資訊有限。

補足這一點的就是 IErrorInfo。它可以另外傳達錯誤的說明字串與發生來源;在 C++ 裡,編譯器支援的 _com_error 類別會把 HRESULT 與 IErrorInfo 一起處理。對話方塊上顯示「代碼+說明文」的應用程式,多半是透過這個機制帶出說明文。

補充 HRESULT 的 IErrorInfo32 位元 HRESULT 能塞的東西有限,因此錯誤描述字串與來源由 IErrorInfo 另外傳達,C++ 裡由 _com_error 類別一併處理HRESULT(只有 32 位元)能塞的東西有限IErrorInfo 帶描述_com_error 一併處理對話方塊的代碼 + 描述

圖 14: 塞不進 32 位元 HRESULT 的描述字串,由 IErrorInfo 另外帶。

6.2. .NET 風格 ── 從 HRESULT 到例外型別

.NET 執行階段在 COM 互通中收到失敗的 HRESULT 時,會把它轉成例外。已知的 HRESULT 會對應到相應的例外型別,未知的則變成 COMException。10

從 HRESULT 到 .NET 例外的對應COM 互通收到的失敗 HRESULT,若有已知對應就轉成相應例外型別,否則轉成 COMException,兩種情況原始值都留在 Exception.HResultYesNo失敗 HRESULT有已知對應?轉成相應例外型別轉成 COMException原始值留在 Exception.HResult

圖 15: .NET 把 HRESULT 對應到例外型別,每個例外的原始值都留在 Exception.HResult。

HRESULT .NET 例外型別
E_ACCESSDENIED (0x80070005) UnauthorizedAccessException
E_OUTOFMEMORY (0x8007000E) OutOfMemoryException
E_INVALIDARG (0x80070057) ArgumentException
E_NOTIMPL (0x80004001) NotImplementedException
沒有定義對應的值 COMException(原始值在 ErrorCode 屬性)

用原本的 HRESULT 為例外處理分支

不論是哪一種例外,原本的 HRESULT 都保存在 Exception.HResult 裡。例如在檔案 I/O 想做到「只有共用違規時才重試」,就能拿這個值當條件。下面這段例子只捕捉代表共用違規的 0x80070020。

try
{
    using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None);
}
catch (IOException ex) when (ex.HResult == unchecked((int)0x80070020))
{
    // 0x80070020 = HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION)
    // 另一個處理程序握著這個檔案 ── 例如稍等一下再重試
}

6.3. P/Invoke 與 GetLastError

透過 P/Invoke 直接呼叫 Win32 API 時,宣告方式與取得方式要成組搭配。

  1. 在 DllImport(或 LibraryImport)指定 SetLastError = true。
  2. 確認失敗之後立刻用 Marshal.GetLastWin32Error 取出。.NET 6 以後可以用同等的 GetLastPInvokeError。16

要避免把 GetLastError 本身定義成 P/Invoke 再呼叫的做法。因為執行階段內部的 API 呼叫可能覆寫這個值,導致取不到正確的失敗原因。16

在 P/Invoke 取出最後錯誤指定 SetLastError 為 true 再用 Marshal.GetLastWin32Error 取出才正確;直接 P/Invoke GetLastError 會因執行階段覆寫而不準經 P/Invoke 呼叫 Win32 API指定 SetLastError=true用 GetLastWin32Error 取出直接呼叫 GetLastError 的定義執行階段覆寫,不準確

圖 16: 在 P/Invoke 把 SetLastError=true 與 Marshal.GetLastWin32Error 當一組用。直接呼叫 GetLastError 不準確。

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern SafeFileHandle CreateFileW(string fileName, uint access, uint share,
    IntPtr security, uint disposition, uint flags, IntPtr template);

// 傳回值用 SafeFileHandle 接而不是 IntPtr,並以 using 確實關閉
//(放著 IntPtr 不管會洩漏核心控制代碼)
using var handle = CreateFileW(@"C:\ProgramData\MyApp\config.dat",
    0x80000000 /*GENERIC_READ*/, 0, IntPtr.Zero, 3 /*OPEN_EXISTING*/, 0, IntPtr.Zero);
if (handle.IsInvalid)
{
    int code = Marshal.GetLastWin32Error();              // 例: 5
    var message = new Win32Exception(code).Message;       // 例: 存取被拒。
    logger.LogError("CreateFileW failed: {Code} (0x{Code:X8}) {Message}",
        code, code, message);
}

Win32Exception 會依 Win32 錯誤碼查出作業系統的訊息字串,因此可以直接用在把代碼與訊息一起留在記錄檔的用途上。至於該在哪一層 catch 例外、又該怎麼寫進記錄檔這種設計問題,在「應該在哪裡 catch 例外並輸出日誌、進行錯誤處理」裡有談到。

7. 實務上的轉換與調查工具 ── 可複製的速查

首先依手邊的環境與目的挑工具。以下各節都放了可以直接使用的例子。

場合 工具 能查到的東西
想不額外安裝就確認 certutil -error、net helpmsg 代碼對應的名稱與訊息(7.2 節)
想橫跨多套體系的定義查詢 err.exe 標頭檔裡的定義候選(7.1 節)
想轉換或分解數值 PowerShell 十進位/十六進位的轉換、低 16 位元、對應到 .NET 例外(7.3 節)
想在傾印分析途中確認意義 WinDbg 的 !error 當成 Win32 或 NTSTATUS 的解讀(7.4 節)

7.1. err.exe(Microsoft Error Lookup Tool)

這是 Microsoft 發行的單一執行檔錯誤查詢工具。它會橫跨 winerror.h、ntstatus.h 等大量標頭檔,列出與指定代碼相符的定義與訊息。7

err 0x80070005
err 5
err 0xC0000005

看結果時,有兩點要注意。

  • 從多個候選裡挑出符合脈絡的那一個。例如「5」除了 Win32 的 ERROR_ACCESS_DENIED 之外,也會命中其他定義。不要把命中的名稱直接當成原因。
  • 確認收錄定義的時點。下載檔名帶有版本(撰稿時為 Err_6.4.5.exe),代碼的定義是以打包當時的標頭檔為準。7
err.exe 搜尋結果依脈絡選擇err.exe 走過大量標頭檔並列出相符定義,因此同一數字出現數個候選時,依脈絡選擇合理的那個輸入 err 5走過大量標頭命中數個定義依脈絡選合理候選

圖 17: err.exe 是跨標頭搜尋,因此可能出現數個候選,合理的那個依脈絡選擇。

7.2. Windows 內建命令

不必額外安裝就能用的是 certutil 與 net helpmsg。certutil 的 -error 選項會顯示錯誤碼對應的訊息文字,十六進位的 HRESULT 或十進位都接受。8

certutil -error 0x80070005
certutil -error 5
net helpmsg 5

net helpmsg 只吃十進位的 Win32 錯誤碼。在中文環境下訊息會以中文傳回,因此也能拿來向使用者說明。要查十六進位的 HRESULT 時,就像上面的例子那樣選 certutil -error。

標準命令怎麼選十進位 Win32 錯誤碼可用 net helpmsg 查;包含十六進位的代碼(含 HRESULT)可用 certutil 的 -error 選項查十進位 Win32含十六進位手上的代碼是?net helpmsgcertutil -error會回傳中文訊息十六進位與十進位都接受

圖 18: 標準命令怎麼選。十進位 Win32 錯誤用 net helpmsg;含十六進位就用 certutil -error。

7.3. PowerShell 一行指令集

這是把記法轉換、取得 Win32 訊息、HRESULT 分解與對應到 .NET 例外整理在一起的速查。就算確認了代碼的意義,失敗的操作與對象仍然需要另外調查。

# Win32 錯誤碼 → 作業系統的訊息字串
[System.ComponentModel.Win32Exception]::new(5).Message
# → 存取被拒。

# 十進位負數 → 十六進位記法(確認 HRESULT 的真身)
'0x{0:X8}' -f -2147467259     # 0x80004005

# 0x8007xxxx → 低 16 位元的 Win32 錯誤碼
0x80070005 -band 0xFFFF        # 5

# HRESULT → 確認 .NET 對應到的例外
[System.Runtime.InteropServices.Marshal]::GetExceptionForHR(-2147024891)
# → UnauthorizedAccessException (0x80070005)

# Win32 錯誤碼 → HRESULT(重現重新包裝的過程)
'0x{0:X8}' -f (0x80070000 -bor 32)   # 0x80070020

7.4. WinDbg 的 !error

要在傾印分析途中查代碼,WinDbg 的 !error 擴充最快。預設會當成 Win32 錯誤碼解讀,第二個引數加上 1 就會當成 NTSTATUS 解讀。9

0:000> !error 5
Error code: (Win32) 0x5 (5) - 存取被拒。

0:000> !error 0xc0000005 1
Error code: (NTSTATUS) 0xc0000005 - <存取違規>

在當機傾印裡,!analyze -v 會自動顯示例外代碼(NTSTATUS),因此流程就是從那裡再用 !error <code> 1 確認意義。

在 WinDbg 確認例外代碼的流程當機傾印裡 analyze 命令會自動顯示例外代碼;把該代碼連同第二個引數 1 傳給 error 擴充,以 NTSTATUS 確認意義開啟當機傾印執行 !analyze -v顯示例外代碼用 !error code 1 確認意義

圖 19: 傾印分析時,用 !error 加上旗標 1 查 !analyze -v 顯示的例外代碼。

8. 調查程序 ── 從判斷層到對照脈絡

實際調查時,依序走完以下五個階段。重點是不要查到名稱就結束,最後要確認到失敗的操作與對象為止。

  1. 對齊記法。十進位負數就轉成 8 位數的十六進位。不足 8 位的十六進位則補零再讀。
  2. 判斷是哪一層的代碼。用下表從開頭幾位縮小出第一候選,再和回傳它的 API 或出現的位置對照。
  3. 分解取出本質的代碼。0x8007xxxx 的 HRESULT 取低 16 位元,0xDxxxxxxx 的 HRESULT 則剝掉 N 位元再讀。
  4. 用工具查名稱與定義。用 err.exe、certutil、!error 等確認符號名稱與訊息。
  5. 對照脈絡。用應用程式記錄檔、事件記錄檔與 Procmon,鎖定是哪個應用程式、在哪個操作、哪個 API 對什麼失敗。代碼是「失敗的種類」,脈絡才是「原因的位置」。
調查錯誤碼的程序把記法對齊到十六進位、從前幾位判斷層、分解取出本質代碼、用工具查名稱與定義,再對照脈絡的調查型十進位0x80070xC0xD正規化成十六進位前導數字?當 Win32 錯誤讀低 16 位元 → 十進位當 NTSTATUS 讀剝掉 N 位元再讀查名稱/定義對照脈絡(Procmon)

圖 20: 調查的型。正規化記法、判斷層並分解、查名稱,再對照脈絡。

外觀 第一候選 如何分解與轉換
1 到 5 位十進位(5、1223 等) Win32 錯誤碼 原樣給 net helpmsg 或 err.exe
負數十進位(-2147024891 等) HRESULT 轉成 8 位十六進位,再依下面各列判斷
0x8007xxxx HRESULT(FACILITY_WIN32) 低 16 位元轉十進位,當 Win32 讀
0x8004xxxx HRESULT(FACILITY_ITF) 查回傳元件的文件
0x8000xxxx HRESULT(FACILITY_NULL) E_FAIL 等通用代碼。把重量移到脈絡調查
0xCxxxxxxx NTSTATUS(錯誤) !error <code> 1;必要時轉成 Win32 再讀
0xDxxxxxxx NTSTATUS 的 HRESULT 對應 剝掉 N 位元(0x10000000)當 NTSTATUS 讀
0x8024xxxx 等自有 facility 功能領域特有的 HRESULT 從 Facility 值鎖定領域再查專用資料(0x8024… 是 Windows Update)2

步驟 5 的具體例子:從 0x80070002 走向找不到的路徑

即使應用程式只顯示「0x80070002」,只要看 Procmon 的 Result 欄,就能知道「哪個處理程序、對哪條路徑、被回傳了 NAME NOT FOUND」。這是從代碼的分解,前進到鎖定實際失敗對象的觀測。

事件記錄檔那一側的查法,也請參考「Windows 事件記錄・ETW 入門」。

9. 常見誤讀 ── 讓調查繞遠路的模式

最後列出實際諮詢裡看到的誤讀模式。

誤讀 1:以為 0x80004005 是「指出特定原因的代碼」

E_FAIL 是「未指定的失敗」,在 Windows Update、網路或資料庫都會出現同一個值。把用這個代碼搜到的處理方法一個個試過去,幾乎肯定是繞遠路。請不要從代碼下手,而要從「哪個應用程式、哪個操作、同一時刻的其他記錄檔」來縮小範圍。6

誤讀 2:沒發現負數十進位是 HRESULT

這是把「發生錯誤 -2147467259」這種記錄原樣拿去搜尋,或被「負的錯誤?」搞混的情況。看到負數就轉成十六進位。光是這樣就知道它是 0x80004005(E_FAIL),也就能接上誤讀 1 的知識。

誤讀 3:查 0x8007xxxx 的整段 8 位,卻不看底下的 Win32 錯誤

0x80070005 的本質是「5 = 拒絕存取」。與其拿整段 8 位去搜尋,不如取出低 16 位元,思考「Win32 的錯誤 5 在這個操作的脈絡裡代表什麼」,這樣更快碰到核心。

誤讀 4:假定「同一代碼 = 同一原因」

一旦有過「錯誤 5 是防毒軟體造成的」經驗,下次遇到錯誤 5 就容易直接跳到同一套處理。就算代碼相同,只要失敗的 API 與目標資源不同,原因就是另一回事。確認代碼的意義,和用 Procmon 等鎖定對象,請每次都成組進行。

誤讀 5:把 Win32 錯誤 5 與 0xC0000005、STOP 代碼與 NTSTATUS 搞混

因為都有「5」就把 ERROR_ACCESS_DENIED 與 STATUS_ACCESS_VIOLATION 視為同一件事,調查就會偏到權限問題與程式錯誤這兩個完全不同的方向。另外,藍畫面的 STOP 代碼和 NTSTATUS 是不同體系,把 0x9F 拿到 NTSTATUS 的表去查,不會得到有意義的答案。15

錯誤 5 與 0xC0000005 的調查方向不同Win32 錯誤 5 應當權限問題調查,NTSTATUS 0xC0000005 應當程式錯誤調查;把它們當成同一件事會把調查送到不同方向Win32 錯誤 5調查權限問題NTSTATUS 0xC0000005調查程式錯誤別套體系、互不相干的代碼

圖 21: 不要因為「5」這個連結就當成同一件事。錯誤 5 走向權限問題;0xC0000005 走向程式錯誤。

10. 總結

先分辨體系。Windows 主要的錯誤碼分成 Win32 錯誤碼、HRESULT、NTSTATUS 三套體系。先整理十進位、十六進位、負數這些記法的搖擺,再確認是哪一層的誰回傳的。NTSTATUS 會在當機的例外代碼或 Procmon 的 Result 欄遇到,但 Win32 的錯誤 5 和 0xC0000005 是不同的東西,STOP 代碼更是另一套體系。

接著讀懂結構,取出需要的資訊。HRESULT 由 S/R/C/N/X 位元、11 位元的 Facility 與 16 位元的 Code 組成。最重要的模式是 0x8007xxxx,可以從低 16 位元讀出 Win32 錯誤。在 COM 互通中,HRESULT 會對應到 .NET 的例外型別,原本的值留在 Exception.HResult。在 P/Invoke 則把 SetLastError=true 與 Marshal.GetLastWin32Error 當成一組使用。

最後與脈絡對照。E_FAIL(0x80004005)不是指出原因的代碼。一旦發現分解也不會增加資訊,就停止深挖代碼,轉往操作、對象與同一時刻的記錄檔。工具則在 certutil、net helpmsg、err.exe、PowerShell 與 WinDbg 的 !error 之間依情況選用。

調查的型就是「對齊記法 → 判斷層 → 分解 → 查名稱 → 對照脈絡」。代碼能告訴你的只到失敗的種類。下次遇到不熟悉的數字,貼進搜尋框之前,請先確認開頭幾位與它的出處。最初的這一次分解,會決定後面要去哪裡查。

相關文章

相關諮詢領域

小村軟體有限公司承接從錯誤碼出發的故障調查——「不知道這個錯誤碼是什麼意思」「0x80070005 只在特定環境出現」——以及混用 Win32 API、COM、.NET 的應用程式錯誤處理設計,還有用當機傾印與 Process Monitor 鎖定原因的工作。從錯誤對話方塊的一張螢幕擷取開始諮詢也可以。

參考連結

  1. Microsoft Learn, Debug system error codes. 關於 Win32 系統錯誤碼(0~15999)清單索引、用 FormatMessage 與 FORMAT_MESSAGE_FROM_SYSTEM 旗標取得 GetLastError 回傳代碼的訊息、WinINet/WinHTTP 錯誤(12000 號段)定義在此空間,以及用 Microsoft Error Lookup Tool 與 !err 命令調查的方法。 ↩ ↩2 ↩3 ↩4 ↩5

  2. Microsoft Open Specifications, [MS-ERREF]: HRESULT. 關於 HRESULT 位元配置(S、R、C、N、X 位元,11 位元 Facility,16 位元 Code)、N 位元表示對應進 HRESULT 空間的 NTSTATUS 值,以及包含 FACILITY_WINDOWS_UPDATE(36)的 facility 代碼清單。 ↩ ↩2 ↩3 ↩4

  3. Microsoft Open Specifications, [MS-ERREF]: NTSTATUS. 關於 NTSTATUS 位元配置(2 位元 Sev、C 位元、N 位元、12 位元 Facility、16 位元 Code),以及 severity 分成成功(00)、資訊(01)、警告(10)、錯誤(11)四種。 ↩ ↩2 ↩3

  4. Microsoft Learn, HRESULT_FROM_WIN32 macro. 關於把 Win32 系統錯誤碼對應到 HRESULT 值的 winerror.h 巨集定義。 ↩ ↩2 ↩3

  5. Microsoft Learn, Structure of COM Error Codes. 關於 HRESULT severity 位元與 facility 欄位的角色、FACILITY_NULL、FACILITY_RPC、FACILITY_ITF、FACILITY_WIN32、FACILITY_WINDOWS 的值,以及 FACILITY_ITF 代碼依介面定義意義、同一值可能意思不同。 ↩ ↩2 ↩3

  6. Microsoft Learn, Common HRESULT values. 關於 E_FAIL(0x80004005)是「Unspecified failure(未指定的失敗)」,以及 E_ACCESSDENIED(0x80070005)、E_INVALIDARG(0x80070057)、E_OUTOFMEMORY(0x8007000E)等常見 HRESULT 值的定義。 ↩ ↩2 ↩3 ↩4

  7. Microsoft Learn, The Microsoft Error Lookup Tool. 關於這是橫跨 Winerror.h 等各種標頭檔、顯示十六進位狀態代碼對應訊息本文的獨立工具、下載檔名為 Err_6.4.5.exe,以及收錄定義以編譯當下為準這點需要注意。 ↩ ↩2 ↩3

  8. Microsoft Learn, certutil. 關於 certutil 的 -error 選項顯示錯誤碼對應的訊息本文,以及使用包含符號名稱的錯誤記法,例如 0x80070002 (WIN32: 2 ERROR_FILE_NOT_FOUND)。 ↩ ↩2

  9. Microsoft Learn, !error. 關於 WinDbg 的 !error 擴充解碼並顯示 Win32、Winsock、NTSTATUS、NetAPI 錯誤值,以及指定旗標 1 時當成 NTSTATUS 解讀。 ↩ ↩2

  10. Microsoft Learn, How to: Map HRESULTs and exceptions. 關於 COM HRESULT 與 .NET 例外的相互對應機制、E_NOTIMPL → NotImplementedException 等對應表、沒有明確對應的 HRESULT 會轉成 COMException,以及例外的 Message、Source 等從 IErrorInfo 資訊初始化。 ↩ ↩2

  11. Microsoft Learn, RtlNtStatusToDosError function (winternl.h). 關於此函式把 NTSTATUS 代碼轉成對應的 Win32 系統錯誤碼、沒有定義對應時回傳 ERROR_MR_MID_NOT_FOUND,以及不存在反向轉換函式。 ↩ ↩2

  12. Microsoft Learn, Last-Error Code. 關於最後錯誤碼依執行緒持有、應在失敗後立刻用 GetLastError 取出、成功時把代碼覆寫成 0 的 API 與不動它的 API 並存,以及位元 29 保留給應用程式定義代碼。 ↩ ↩2

  13. Microsoft Learn, RegOpenKeyExW function. 關於成功時傳回 ERROR_SUCCESS、失敗時以傳回值傳回 Winerror.h 所定義的非零錯誤碼。 ↩

  14. Microsoft Open Specifications, [MS-ERREF]: NTSTATUS values. 關於包含 STATUS_ACCESS_VIOLATION(0xC0000005)、STATUS_DLL_NOT_FOUND(0xC0000135)、STATUS_STACK_OVERFLOW(0xC00000FD)、STATUS_HEAP_CORRUPTION(0xC0000374)、STATUS_BREAKPOINT(0x80000003)的 NTSTATUS 值清單。 ↩ ↩2

  15. Microsoft Learn, Bug check code reference. 關於藍畫面顯示的錯誤檢查代碼(STOP 代碼)清單,以及用 WinDbg 的 !analyze 擴充顯示代碼資訊的方法。從清單可確認它是與 NTSTATUS 分開的另一套編號體系。 ↩ ↩2

  16. Microsoft Learn, Marshal.GetLastWin32Error Method. 關於取出設定了 SetLastError 旗標的 P/Invoke 呼叫最後錯誤碼的方法、直接 P/Invoke GetLastError 會因執行階段內部 API 呼叫覆寫而不可靠,以及從 .NET 6 起建議使用 GetLastPInvokeError。 ↩ ↩2

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

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

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

常見問題

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

錯誤 0x80004005 是什麼意思?
0x80004005 是 HRESULT 的 E_FAIL,意思是「未指定的失敗(Unspecified failure)」。也就是它只表示「發生了無法回報詳細原因的失敗」,並不是指出原因本身的代碼。網路、Windows Update、VBA、資料庫驅動程式等互不相干的場合會出現同一個 0x80004005,正是這個緣故。看到這個代碼時,不要深挖代碼本身的意思,而要從它出現在哪個應用程式的哪個操作這個脈絡,以及事件記錄檔或詳細記錄檔裡留下的其他錯誤資訊來縮小原因範圍。
像 -2147467259 這種負數錯誤碼是什麼?
那是把 32 位元的 HRESULT 以有號十進位顯示的結果。HRESULT 在失敗時會把最高位設為 1,因此以有號整數顯示時一定是負數。在 PowerShell 執行 '0x{0:X8}' -f -2147467259 就能還原成十六進位記法(此例為 0x80004005 = E_FAIL)。在記錄檔或指令碼的錯誤訊息裡看到以 -214… 開頭的負數時,先轉成十六進位再查是標準做法。
查詢錯誤碼意義最簡單的方法是什麼?
不必額外安裝就能用的是命令提示字元的 net helpmsg 5(用於十進位的 Win32 錯誤)和 certutil -error 0x80070005。certutil 也接受十六進位的 HRESULT,會顯示符號名稱與訊息本文。用 PowerShell 的話,[System.ComponentModel.Win32Exception]::new(5).Message 可以取得中文訊息。在開發機上備妥 Microsoft 官方的錯誤查詢工具 err.exe(Microsoft Error Lookup Tool),就能橫跨 Win32、HRESULT、NTSTATUS 一次檢索出相符的定義,非常方便。
0xC0000005 是什麼樣的錯誤?
那是 NTSTATUS 的 STATUS_ACCESS_VIOLATION,也就是存取違規(不合法的記憶體存取)。它是應用程式當機時,在事件記錄檔的「例外代碼」與當機傾印裡最常看到的代碼,表示無效指標解參考或存取已釋放記憶體等程式錯誤。它和 Win32 錯誤的 5(ERROR_ACCESS_DENIED=拒絕存取)名稱相似,卻是另一套體系、互不相干的代碼,請不要混淆。要鎖定原因,可靠的做法是擷取當機傾印並用 WinDbg 分析。
為什麼同一個錯誤碼,每次原因卻不一樣?
因為錯誤碼只表示「哪一種失敗」,「什麼失敗、為什麼失敗」由呼叫的脈絡決定。例如錯誤 5(拒絕存取)可能來自 NTFS 存取權限不足、缺少系統管理員權限、防毒軟體封鎖等完全不同的原因,卻得到同一個代碼。即使是相似的場景,只要別的處理程序還開著那個檔案,就會變成另一個代碼(錯誤 32=共用違規),正確分辨代碼就會改變要查的地方。錯誤 2(找不到檔案)也常常不是主程式本身,而是相依 DLL 或設定檔沒找到。查過代碼意義之後,再用 Process Monitor 等確認是哪個 API 對哪個資源失敗,才是鎖定原因的捷徑。

作者檔案

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

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽