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

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

「應用程式畫面出現錯誤 0x80004005。這是什麼意思?」── 故障調查諮詢裡,這種問題是經典。把錯誤對話方塊上的數字直接貼進搜尋引擎,換來一堆不相干的文章——Windows Update 失敗、共用資料夾連不上、VBA 執行階段錯誤、資料庫連線失敗——反而更混亂的人很多。

會這樣,是因為 0x80004005(E_FAIL)是只表示「未指定失敗」的通用代碼。同一個代碼用在無數場合,只搜代碼到不了原因。另一方面,像 0x80070005 這種代碼,只要知道結構,搜尋前就能在幾秒內分解成「Win32 錯誤編號 5 = 拒絕存取,再包成 HRESULT」。

Windows 錯誤碼因歷史因素形成 Win32 錯誤碼、HRESULT、NTSTATUS 三層,而且會跨層轉換。把這個結構放進腦子後,就能自己判斷「這是哪一層、誰回傳的」以及「本質代碼是什麼」,調查的開局會快很多。

本文面向中小企業 IT 人員與 Windows 應用開發者,依 2026 年 8 月當下的 Microsoft Learn 與公開規格 [MS-ERREF],整理三種錯誤碼體系的分辨與分解、與 .NET 例外的關係,以及用 err.exe、PowerShell 的實務查詢。

1. 先講結論

  • Windows 錯誤碼主要有三套體系。 Win32 錯誤碼(GetLastError 回傳的較小十進位)、HRESULT(COM 之後的 32 位元代碼,以 0x8 開頭的十六進位或負數十進位)、NTSTATUS(核心層代碼;錯誤以 0xC 開頭)。123
  • 十進位與十六進位是同一代碼的不同記法。 「錯誤 5」、「0x5」、「0x80070005 的低 16 位元」都指向 ERROR_ACCESS_DENIED(拒絕存取)。1
  • 0x8007xxxx 是「包起來的 Win32 錯誤」。 那是放進 HRESULT FACILITY_WIN32(7)的 Win32 錯誤碼;把低 16 位元轉成十進位就是本質代碼。這是讀錯誤碼最重要的模式。45
  • 0x80004005(E_FAIL)不是原因代碼。 意思是「Unspecified failure」,沒有更多資訊。與其深挖這個代碼,不如找來源脈絡與伴隨的記錄。6
  • 負數十進位(-2147467259 等)是 HRESULT。 32 位元的最高位(失敗位元)已設定,有號顯示就是負數。先轉成十六進位再讀。2
  • 以 0xC 開頭的 8 位數是 NTSTATUS。 0xC0000005(存取違規)與 0xC0000135(找不到 DLL)在當機時的事件記錄與傾印裡不斷出現。與 Win32 錯誤編號 5 無關。7
  • 同一代碼會隨脈絡改變意義。 錯誤 5 的原因涵蓋 ACL、提升權限、防毒、被占用的檔案等,錯誤 2 的「找不到檔案」常常是相依 DLL。一定要把代碼意義與「哪個 API 對什麼失敗」一起讀。1
  • 轉換與查詢工具是標準配備。 certutil -errornet helpmsg 內建於 Windows;PowerShell 的 Win32Exception 可取得訊息;開發機用 err.exe(Microsoft Error Lookup Tool);傾印分析用 WinDbg 的 !error8910
  • 在 .NET 裡,HRESULT 會對應到例外型別。 已知的 HRESULT 對應到相應例外型別(E_ACCESSDENIED → UnauthorizedAccessException 等);未知的變成 COMException;原始值留在 Exception.HResult11

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

2. Windows 有三套錯誤碼體系

先看整體地圖。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

歷史上是依這個順序堆疊:繼承 MS-DOS 錯誤編號的 Win32 錯誤碼、NT 核心內部使用的 NTSTATUS,以及導入 COM 時為了「把成敗與來源塞進 32 位元」而設計的 HRESULT。在現今 Windows,日常的轉換流程是:核心回傳 NTSTATUS,Win32 子系統把它轉成 Win32 錯誤碼,COM 層再包成 HRESULT124

三套體系之間的轉換流程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 裡,轉換只要一行。

# Decimal → hex
'0x{0:X8}' -f 1223          # 0x000004C1
'0x{0:X8}' -f -2147024891   # 0x80070005 (negative = HRESULT to hex)

# Hex → decimal
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 的基本行為

CreateFileRegOpenKeyEx 等許多 Win32 API 以傳回值(FALSE、NULL、INVALID_HANDLE_VALUE 等)表示失敗,並把詳細錯誤碼存在 每個執行緒持有的「最後錯誤碼」。呼叫端在確認失敗後立刻用 GetLastError 取出。13

實務上有兩點要注意。13

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

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

要從代碼取得訊息字串,對 FormatMessage 指定 FORMAT_MESSAGE_FROM_SYSTEM 旗標。1

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

void PrintLastError(const wchar_t* apiName)
{
    DWORD code = GetLastError();   // Call immediately after failure (do not insert another 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 使用者取消了操作

其中 998(ERROR_NOACCESS)不是「拒絕存取」,而是 記憶體存取違規的 Win32 表達,也就是稍後會談的 NTSTATUS STATUS_ACCESS_VIOLATION 轉到 Win32 層後的樣子。注意與編號 5 搞混。另外 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 封鎖、服務帳戶權限不足等。
  • 錯誤 2(找不到檔案):不一定是使用者指定的檔案。EXE 隱含要載入的相依 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 位元值的格式。公開規格 [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 值如下。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

實際分解看看。

對 0x80004005:S=1(失敗),Facility=(0x80004005 » 16) & 0x7FF = 0(FACILITY_NULL),Code=0x4005。這是通用的 FACILITY_NULL 代碼,定義為 E_FAIL「Unspecified failure」。6 也就是這個代碼 只帶「無法回報細節的失敗」這個意思。看到 0x80004005,就在那裡停止深挖代碼本身,把調查的重量移到「哪個元件回傳的」以及「事件記錄或應用記錄同時間有沒有細節」。

對 0x80070005:S=1,Facility=7(FACILITY_WIN32),Code=0x0005=5。可以看出是 把 Win32 錯誤編號 5(ERROR_ACCESS_DENIED)包成 HRESULT。別名 E_ACCESSDENIED 實質上就是這個值。6

即使同樣是「拒絕存取」,0x80070005 是 Win32 層發生的具體失敗的包裝,資訊量與 0x80004005 完全不同。

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 的上層(COM 方法或 .NET 執行階段),winerror.h 提供 HRESULT_FROM_WIN32 巨集。4 行為是「把 Win32 錯誤碼放進低 16 位元,Facility 設為 FACILITY_WIN32(7),S 位元設為 1」。

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)

反向讀取時,在 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(FACILITY_ITF)有相反的注意點。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 詳細代碼

因為 severity 是 2 位元,可從前導十六進位數字讀出種類。0xC… 是錯誤(11),0x8… 是警告(10),0x4… 是資訊(01),0x0–0x3… 是成功。中斷點例外 0x80000003(STATUS_BREAKPOINT)是「警告,不是錯誤」的代表例子。37

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

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

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

IT 人員與開發者遇到 NTSTATUS 的情況,主要與當機有關。

  • 應用程式當機的例外代碼:事件記錄「應用程式錯誤(事件識別碼 1000)」裡記錄的「Exception code: 0xc0000005」是 NTSTATUS。代表值如下。7
符號 意義
0xC0000005 STATUS_ACCESS_VIOLATION 存取違規(不合法的記憶體存取)
0xC0000135 STATUS_DLL_NOT_FOUND 找不到必要 DLL,無法啟動
0xC00000FD STATUS_STACK_OVERFLOW 堆疊溢位
0xC0000374 STATUS_HEAP_CORRUPTION 堆積損毀
  • 藍畫面 STOP 代碼:乍看相似,但 STOP 代碼(錯誤檢查代碼)是 與 NTSTATUS 分開的另一套編號,例如 0x0000009F(DRIVER_POWER_STATE_FAILURE),並有專用參考。14 只要記住「0xC0000005 是 NTSTATUS;STOP 0x9F 是錯誤檢查代碼,不可拿到 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 與另外兩層之間的橋有兩條路。

  1. 對應進 HRESULT 空間:設定 HRESULT N 位元(0x10000000)會把 NTSTATUS 值原樣帶進 HRESULT 空間(winerror.h 的 HRESULT_FROM_NT 巨集)。例如把 0xC0000005 對應後變成 0xD0000005。看到以 0xD 開頭的 HRESULT,正確手續是剝掉 N 位元再當 NTSTATUS 讀2
  2. 轉換成 Win32 錯誤碼:ntdll 的 RtlNtStatusToDosError 把 NTSTATUS 轉成對應的 Win32 錯誤碼。沒有定義對應的值會變成 ERROR_MR_MID_NOT_FOUND。12 例如 STATUS_ACCESS_VIOLATION(0xC0000005)轉成 ERROR_NOACCESS(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 對應到相應例外型別;未知的變成 COMException11

從 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 都留在 Exception.HResult 屬性。檔案 I/O 例外處理裡「只在共用違規時重試」這類分支,可以用這個值來寫。

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)
    // Another process is holding the file — wait a little and retry, for example
}

6.3. P/Invoke 與 GetLastError

經由 P/Invoke 直接呼叫 Win32 API 時,DllImport(或 LibraryImport)指定 SetLastError = true,再用 Marshal.GetLastWin32Error 取出(.NET 6 起可用同等的 GetLastPInvokeError。把 GetLastError 本身定義成 P/Invoke 再呼叫並不準確,因為執行階段內部的 API 呼叫可能覆寫這個值。15

在 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);

// Receive the return value as SafeFileHandle, not IntPtr, and close it reliably with using
// (leaving it as IntPtr leaks a kernel handle)
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();              // Example: 5
    var message = new Win32Exception(code).Message;       // Example: Access is denied.
    logger.LogError("CreateFileW failed: {Code} (0x{Code:X8}) {Message}",
        code, code, message);
}

Win32Exception 會從 Win32 錯誤碼查出作業系統訊息字串,因此可直接用來把代碼與訊息一起留在記錄。在哪一層捕捉例外、如何寫進記錄這個設計問題,見「應該在哪裡 catch 例外並輸出日誌、進行錯誤處理」。

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

7.1. err.exe(Microsoft Error Lookup Tool)

Microsoft 發行的獨立錯誤查詢工具。它會走過 winerror.h、ntstatus.h 等大量標頭檔,列出與指定代碼相符的定義與訊息。8

err 0x80070005
err 5
err 0xC0000005

同一個數字可能命中多個標頭(例如「5」除了 Win32 ERROR_ACCESS_DENIED 還會對上各處定義),因此 哪個候選合理必須依脈絡選擇。下載檔名帶版本(撰寫時為 Err_6.4.5.exe),也要注意代碼定義以打包當時的標頭為準。8

err.exe 搜尋結果依脈絡選擇err.exe 走過大量標頭檔並列出相符定義,因此同一數字出現數個候選時,依脈絡選擇合理的那個輸入 err 5走過大量標頭命中數個定義依脈絡選合理候選

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

7.2. Windows 內建命令

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

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

net helpmsg 只適用十進位 Win32 錯誤碼,但在中文環境會回傳中文訊息,可直接用來向使用者說明。

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

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

7.3. PowerShell 一行指令集

# Win32 error code → OS message string
[System.ComponentModel.Win32Exception]::new(5).Message
# → Access is denied.

# Negative decimal → hex notation (confirm the identity of an HRESULT)
'0x{0:X8}' -f -2147467259     # 0x80004005

# 0x8007xxxx → the Win32 error code in the low 16 bits
0x80070005 -band 0xFFFF        # 5

# HRESULT → confirm the exception .NET maps
[System.Runtime.InteropServices.Marshal]::GetExceptionForHR(-2147024891)
# → UnauthorizedAccessException (0x80070005)

# Win32 error code → HRESULT (reproduce the wrap)
'0x{0:X8}' -f (0x80070000 -bor 32)   # 0x80070020

7.4. WinDbg 的 !error

在傾印分析中查代碼,WinDbg 的 !error 擴充很快。預設當成 Win32 錯誤碼解讀;第二個引數傳 1 則當成 NTSTATUS。10

0:000> !error 5
Error code: (Win32) 0x5 (5) - Access is denied.

0:000> !error 0xc0000005 1
Error code: (NTSTATUS) 0xc0000005 - <Access violation>

在當機傾印裡,!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 位的十六進位前面補 0 再讀。
  2. 判斷是哪一層的代碼。 如下列表,前幾位幾乎就能決定。
  3. 分解取出本質代碼。 機械操作:0x8007xxxx 取低 16 位元,0xDxxxxxxx 剝掉 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 步「對照脈絡」特別有效的是 Process Monitor 的 Result 欄。即使應用只顯示「0x80070002」,Procmon 也能用一列告訴你「哪個處理程序、對哪條路徑、被回傳 NAME NOT FOUND」。事件記錄側的查法見「Windows 事件記錄・ETW 入門」。

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

最後是實際諮詢裡出現的誤讀模式。

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

E_FAIL 是「Unspecified failure」,同一個值會出現在 Windows Update、網路與資料庫。把搜這個代碼得到的每項補救都試一遍,幾乎一定是繞遠路。不要從代碼縮小,而要從「哪個應用、哪個操作、同時間的其他記錄」縮小。6

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

把寫著「Error -2147467259 occurred」的記錄原樣搜尋,或被「負號錯誤?」搞混的情況。看到負數就轉成十六進位。光這樣就知道是 0x80004005(E_FAIL),並接到誤讀 1 的知識。

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

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

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

一旦經歷過「錯誤 5 是防毒造成的」,下一次錯誤 5 就容易跳到同一補救。即使代碼相同,失敗的 API 與目標資源不同,原因就是另一件事。確認代碼意義,再用 Procmon 等特定對象,每一次都是一組。

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

因為「5」這個連結就把 ERROR_ACCESS_DENIED 與 STATUS_ACCESS_VIOLATION 當成同一件事,會把調查送到完全不同的方向——權限問題對上程式錯誤。另外,藍畫面 STOP 代碼與 NTSTATUS 是另一套體系,把 0x9F 拿到 NTSTATUS 表查不會得到有意義的答案。14

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

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

10. 摘要

  • Windows 錯誤碼是 Win32 錯誤碼、HRESULT、NTSTATUS 的三層結構。先判斷是哪一層、誰回傳的代碼。
  • 記法搖擺(十進位/十六進位/負數)可以機械地對齊。把負數轉成 8 位十六進位再讀。
  • HRESULT 是 S/R/C/N/X 位元 + Facility(11 位元)+ Code(16 位元)的結構,0x8007xxxx 是最重要的模式:包起來的 Win32 錯誤。把低 16 位元轉成十進位,取出本質代碼。
  • 0x80004005(E_FAIL)這類通用代碼不指出原因。停止深挖代碼、改做脈絡調查的判斷,正是因為知道結構才做得到。
  • 會在當機例外代碼或 Procmon 的 Result 欄遇到 NTSTATUS。0xC0000005 是存取違規,與 Win32 錯誤 5 無關。STOP 代碼又是另一套體系。
  • 在 .NET,HRESULT 對應到例外型別,原始值留在 Exception.HResult。在 P/Invoke 把 SetLastError=true 與 Marshal.GetLastWin32Error 當一組用。
  • 查詢工具是 certutil -error 與 net helpmsg(標準)、err.exe(開發機)、PowerShell 一行指令,以及 WinDbg 的 !error。
  • 程序是「正規化記法 → 判斷層 → 分解 → 查名稱 → 對照脈絡」。代碼告訴你的是失敗的種類;原因的位置由脈絡告訴你。

下次遇到不熟悉的錯誤碼,貼進搜尋框之前先看前幾位。0x8007 就看低 4 位,0xC 就是 NTSTATUS,負數就轉十六進位——這 10 秒的分解,很大程度上決定後面的調查時間。

相關文章

相關諮詢領域

小村軟體有限公司承接從錯誤碼開始的故障調查——「不知道這個錯誤碼是什麼意思」、「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 6

  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 5

  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 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 3

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

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

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

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

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

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

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

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

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

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

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

常見問題

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

錯誤 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 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽