解读 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 的情况,主要与崩溃有关。

  • 应用程序崩溃的异常代码:事件记录「应用程序错误(事件ID 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表