更新记录(仅首版,2026年08月20日 发布)
- 首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176057)
以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。
Go Komura(2026)。《解读 Windows 错误码——Win32 错误、HRESULT、NTSTATUS 的三层结构》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/windows-error-codes-win32-hresult-ntstatus/
- DOI(已登记存档)
- 10.5281/zenodo.22176057
- DOI(上次登记版本)
- 10.5281/zenodo.22176058
“应用程序界面上出现了 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. 先说结论
首先要掌握的是以下三点。
- 统一记法,分辨代码体系。Win32 错误码、HRESULT、NTSTATUS 是彼此独立的体系。十进制和十六进制是同一个值的不同记法,HRESULT 还可能以带符号的负数显示。先统一成 8 位十六进制,再结合返回它的 API 和它出现的位置来解读。123
- 0x8007xxxx 要拆解,E_FAIL 则转向上下文排查。0x80070005 是 FACILITY_WIN32(7)的 HRESULT,低 16 位为 5=ERROR_ACCESS_DENIED。而 0x80004005(E_FAIL)表示“未指定的失败”,仅凭代码无法缩小原因范围。456
- 代码的含义要和失败的操作、对象成对排查。同样是错误 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 与停止代码、7.4 节的 WinDbg |
| 想避开排查中常见的混淆 | 第 9 章的误读示例 |
一句话概括,Windows 错误码排查的套路就是“把记法统一成十六进制→判定属于哪一层的代码→拆解出本质代码→结合上下文解读”。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 18 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. Windows 有三套错误码体系
先看整体。即使数值相同,作为哪套体系的值传过来,读法也不一样。把主要的返回方和典型外观排在一起,就是下面这张表。
| 体系 | 主要返回方 | 典型外观 | 代表示例 |
|---|---|---|---|
| Win32 错误码 | Win32 API(GetLastError)、命令的退出码 |
较小的十进制数(0~15999) | 5 = ERROR_ACCESS_DENIED |
| HRESULT | COM 组件、Shell、安装程序、许多框架 | 以 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
flowchart TB
accTitle: 跨三套体系的转换流程
accDescr: 内核返回的 NTSTATUS 由 Win32 子系统转换为 Win32 错误码,COM 层再把它重新封装为 HRESULT
kernel["内核与驱动程序"] --> nt["NTSTATUS(错误以 0xC… 开头)"]
nt -->|Win32 子系统转换| win["Win32 错误码(例如 5)"]
win -->|COM 层重新封装| hr["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…”开头的十进制负数,就条件反射地转成十六进制。仅此一条,就能大幅减少排查开局阶段的迷路。
flowchart TB
accTitle: 同一个代码的三种外观
accDescr: 十进制的错误 5、十六进制的 0x5 以及 0x80070005 的低 16 位,都指向同一个 ERROR_ACCESS_DENIED
d["十进制记法“错误 5”"] --> same["ERROR_ACCESS_DENIED"]
h["十六进制记法“0x5”"] --> same
l["0x80070005 的低 16 位"] --> same
same -.-> memo["只是记法不同,代码相同"]
图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
- 要在失败之后立刻读取。中间若插入别的 API 调用(例如日志输出函数),那次调用可能会覆盖最后错误代码。
- 不要指望成功时的值。有的 API 在成功时会把最后错误代码清零,有的则根本不碰它。原则是先用返回值确认失败,再读取。
sequenceDiagram
accTitle: GetLastError 要在失败之后立刻读取
accDescr: 用返回值确认失败后不要插入其他 API 调用,立刻用 GetLastError 取得最后错误代码
participant app as 应用程序
participant api as Win32 API
app->>api: 调用 CreateFile
api-->>app: 表示失败的返回值
app->>api: GetLastError
api-->>app: 代码 5
Note over app: 中间插入别的 API 就可能被覆盖
图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);
}
在自研应用的日志里,像这样同时留下十进制、十六进制和消息正文,日后排查会快上一个档次。
flowchart TB
accTitle: 由代码查出消息并写进日志
accDescr: 给 FormatMessage 指定 FORMAT_MESSAGE_FROM_SYSTEM 标志取得错误码的消息字符串,日志里同时留下十进制、十六进制和消息正文
code["错误码(例如 5)"] --> fm["用 FormatMessage 取得字符串"]
fm --> msg["消息正文"]
msg --> log["记入日志"]
log -.-> both["十进制、十六进制与正文并列"]
图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 提升对话框中选择“否”等场合出现。不要把它单纯当成故障,而应读作表示操作“被中止”的代码。
flowchart TB
accTitle: 错误 998 和 5 是两回事
accDescr: 998 是 NTSTATUS 的访问冲突被转换到 Win32 层后的内存访问冲突,与表示拒绝访问的 5 含义不同
nt["NTSTATUS 0xC0000005"] -->|转换到 Win32 层| e998["错误 998(ERROR_NOACCESS)"]
e998 -.-> m1["含义是内存访问冲突"]
e5["错误 5(拒绝访问)"] -.-> m2["权限问题。与 998 是两回事"]
图5:错误 998 是 NTSTATUS 的访问冲突转换到 Win32 层后的样子,与表示拒绝访问的 5 是两回事。
3.3. 同一个代码在不同上下文里含义不同
只记住代表性代码,是定位不了原因的。代码能告诉你的只到“失败的种类”为止,再往前还留着需要排查的对象。
| 代码 | 随上下文变化的部分 | 接下来要确认的事 |
|---|---|---|
| 错误 5(拒绝访问) | NTFS 的 ACL 不足、没有管理员权限却写入受保护区域、杀毒软件或 AppLocker 的拦截、服务账户权限不足等 | 是哪个 API 对哪个对象的访问被拒绝 |
| 错误 2(找不到文件) | 未必是指定的文件,也可能是隐式加载的依赖 DLL、因 32 位/64 位注册表重定向而看向别处的配置文件、环境变量展开失败的路径等 | 究竟是“哪个文件”没找到 |
| 错误 32(共享冲突) | 另一个进程正在使用目标对象 | 是“哪个进程”占着它 |
flowchart TB
accTitle: 错误 5 的原因由上下文决定
accDescr: 同样是拒绝访问,候选原因也有 ACL 不足、没有管理员权限等多种,必须确定是哪个 API 对什么失败
e5["错误 5(拒绝访问)"] --> c1["ACL 不足"]
e5 --> c2["没有管理员权限"]
e5 --> c3["安全软件拦截"]
e5 --> c4["服务权限不足"]
c1 --> next["用 Procmon 确定失败的对象"]
c2 --> next
c3 --> next
c4 --> next
图6:代码只能告诉你“失败的种类”。错误 5 的候选原因有多个,必须确定具体对象。
能实测“哪个 API、对哪个对象名、返回了什么结果”的工具是 Process Monitor。用法在“Process Monitor(ProcMon)实战指南”里有详细介绍。查错误码含义的工作,和确定失败对象的工作,请当作车的两个轮子。
4. HRESULT——读懂塞进 32 位里的结构
4.1. 位布局
HRESULT 是把成功/失败、发生源、详细代码塞进一个 32 位值的格式。先按S 位看成功还是失败、Facility 看发生源、Code 看详情的顺序读,后面的拆解示例就容易跟上。公开规范 [MS-ERREF] 给出的布局如下。2
| 位位置 | 名称 | 含义 |
|---|---|---|
| 31 | S | 严重性。0=成功,1=失败 |
| 30 | R | 保留(映射 NTSTATUS 时属于严重性的一部分) |
| 29 | C | Customer 位。为 1 表示由 Microsoft 以外的一方定义的代码 |
| 28 | N | 为 1 表示该值是把 NTSTATUS 映射到 HRESULT 空间的结果 |
| 27 | X | 保留(0) |
| 26–16 | Facility | 表示发生源的设施码(11 位) |
| 15–0 | Code | 设施内部的详细代码(16 位) |
最高位的 S 位为 1,也就是说十六进制以 0x8 及以上开头的 HRESULT 都是失败。把它按带符号 32 位整数显示就会变成负数,这正是前面那个“-214…”的真身。
flowchart TB
accTitle: S 位与负数显示的关系
accDescr: 失败的 HRESULT 最高位的 S 位为 1,因此十六进制以 0x8 及以上开头,按带符号 32 位整数显示时会变成负数
s["S 位=1(失败)"] --> hex["十六进制以 0x8 及以上开头"]
hex --> neg["带符号显示时变成负数"]
neg --> back["看到负数就转成十六进制再读"]
图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 就是拒绝访问,而要根据返回的值选择下一步排查。
flowchart TB
accTitle: 0x80004005 与 0x80070005 的拆解
accDescr: 0x80004005 是 FACILITY_NULL 的通用代码 E_FAIL,不带细节,应转向上下文排查,而 0x80070005 属于 FACILITY_WIN32,可知是 Win32 错误第 5 号拒绝访问的重新封装
a["0x80004005"] --> af["Facility=0(FACILITY_NULL)"]
af --> ac["Code=0x4005 → E_FAIL"]
ac --> ax["未指定的失败。转向上下文排查"]
b["0x80070005"] --> bf["Facility=7(FACILITY_WIN32)"]
bf --> bc["Code=0x0005 → 5"]
bc --> bx["ERROR_ACCESS_DENIED"]
图8:同样是“失败”,拆解后的信息量却不同。0x80070005 能一直追到 Win32 错误第 5 号。
4.3. 最重要的模式:0x8007xxxx = HRESULT_FROM_WIN32
把 Win32 的失败重新封装成 HRESULT
为了把下层的 Win32 错误传给返回 HRESULT 的 COM 方法和 .NET 运行时,winerror.h 中准备了 HRESULT_FROM_WIN32。在下面这些失败代码的例子里,它把 Win32 错误放进低 16 位、Facility 置为 7、S 位置为 1。4
flowchart TB
accTitle: HRESULT_FROM_WIN32 的行为
accDescr: 把 Win32 错误码存进低 16 位,并把 Facility 设为 7、S 位设为 1,组装出 0x8007xxxx 形式的 HRESULT
win["Win32 错误码(例如 5)"] --> low["存进低 16 位"]
low --> fac["Facility 设为 7"]
fac --> sbit["S 位设为 1"]
sbit --> hr["0x80070005"]
图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、服务器产品)的文档里去查。
flowchart TB
accTitle: 0x8007 与 0x8004 的查法不同
accDescr: FACILITY_WIN32 的 0x8007xxxx 可以靠机械地拆出低 16 位来读,而 FACILITY_ITF 的 0x8004xxxx 因定义含义的主体因接口而异,要到返回它的组件资料里查
hr{"Facility 是多少?"} -->|7 的 WIN32| w["把低 16 位转成十进制"]
hr -->|4 的 ITF| i["含义因返回方而异"]
w --> ww["按 Win32 错误解读"]
i --> ii["查返回方的资料"]
图10:0x8007xxxx 可以机械地拆解,0x8004xxxx 则要去查返回它的组件的资料。
5. NTSTATUS——内核层的代码与崩溃的世界
5.1. 布局与 Severity
NTSTATUS 是内核、设备驱动程序和 ntdll 的本机 API 使用的 32 位代码,布局与 HRESULT 貌似而实不同。3
| 位位置 | 名称 | 含义 |
|---|---|---|
| 31–30 | Sev | 严重性。00=成功,01=信息,10=警告,11=错误 |
| 29 | C | Customer 位 |
| 28 | N | 保留(为了能映射到 HRESULT 而置 0) |
| 27–16 | Facility | 设施码(12 位) |
| 15–0 | Code | 详细代码 |
与 HRESULT 最大的区别在于,严重性占 2 位,分成功、信息、警告、错误四种。从十六进制的首位看,0xC… 对应错误(11),0x8… 对应警告(10),0x4… 对应信息(01),0x0~0x3… 对应成功。
因此,不能仅凭以 0x8 开头这一外观就断定它是失败的 HRESULT。NTSTATUS 的 0x80000003(STATUS_BREAKPOINT)是断点异常,严重性不是“错误”而是“警告”。314
flowchart TB
accTitle: NTSTATUS 看首位就能分辨类别
accDescr: 由于严重性占 2 位,NTSTATUS 可以按十六进制首位分辨,0xC 为错误,0x8 为警告,0x4 为信息,0x0 到 0x3 为成功
head{"十六进制首位是几?"} -->|0xC| e["错误"]
head -->|0x8| w["警告"]
head -->|0x4| i["信息"]
head -->|0x0~0x3| s["成功"]
w -.-> ex["例如 0x80000003 是警告"]
图11:NTSTATUS 看十六进制首位就能分辨类别。0x80000003 是“警告而非错误”。
5.2. 会在哪里遇到——异常代码、停止代码与事件日志
下面分异常代码、停止代码和 Process Monitor 三处,来看会在哪里遇到 NTSTATUS。
应用程序崩溃的异常代码
事件日志的“Application Error(事件 ID 1000)”里记录的“异常代码: 0xc0000005”就是 NTSTATUS。在崩溃和启动失败时常见的代表值有下面这些。14
| 值 | 符号名 | 含义 |
|---|---|---|
| 0xC0000005 | STATUS_ACCESS_VIOLATION | 访问冲突(非法的内存访问) |
| 0xC0000135 | STATUS_DLL_NOT_FOUND | 找不到所需的 DLL,无法启动 |
| 0xC00000FD | STATUS_STACK_OVERFLOW | 堆栈溢出 |
| 0xC0000374 | STATUS_HEAP_CORRUPTION | 堆损坏 |
蓝屏的停止代码属于另一套体系
停止代码(错误检查代码)乍看相似,但像 0x0000009F(DRIVER_POWER_STATE_FAILURE)那样,属于与 NTSTATUS 不同的独立编号体系。要查专门的参考资料。15
关键是把“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 错误后送达应用程序这一层与层之间的对应关系。
flowchart TB
accTitle: 异常代码与停止代码的分辨
accDescr: 事件日志里的异常代码按 NTSTATUS 解读,蓝屏的停止代码则属于另一套体系,要查错误检查代码的专用参考资料
q{"代码出现在哪里?"} -->|异常代码| nt["按 NTSTATUS 解读"]
q -->|停止代码| bc["查错误检查代码的表"]
nt -.-> n1["例如 0xC0000005"]
bc -.-> b1["例如 0x0000009F"]
图12:事件日志的异常代码是 NTSTATUS,蓝屏的停止代码是另一套体系。不要查错表。
异常代码之后的排查,也就是崩溃转储的采集与分析,请参考“Windows 崩溃转储收集入门”和“用 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 层有时会被归并到更粗的分类里,所以要区分转换前后来解读。
flowchart TB
accTitle: NTSTATUS 通向其他层的两座桥
accDescr: NTSTATUS 通过置起 N 位映射到 HRESULT 空间,或通过 RtlNtStatusToDosError 转换成 Win32 错误码,以这两条路径传给其他层
nt["NTSTATUS(0xC0000005)"] -->|置起 N 位| hr["HRESULT(0xD0000005)"]
nt -->|RtlNtStatusToDosError| win["Win32 错误 998(ERROR_NOACCESS)"]
win -.-> memo["没有定义对应关系时返回 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 一并处理。对话框里显示“代码+说明文字”的应用程序,多半就是靠这个机制来搬运说明文字的。
flowchart TB
accTitle: 补足 HRESULT 的 IErrorInfo
accDescr: 32 位的 HRESULT 能装下的信息有限,因此错误的说明字符串和发生源由 IErrorInfo 另行传递,在 C++ 中由 _com_error 类把两者一并处理
hr["HRESULT(仅 32 位)"] --> lim["能装下的信息有限"]
lim --> ei["IErrorInfo 搬运说明文字"]
ei --> ce["_com_error 一并处理"]
ce -.-> dlg["对话框里的代码+说明文字"]
图14:装不进 32 位 HRESULT 的说明字符串,由 IErrorInfo 另行搬运。
6.2. .NET 的做法——从 HRESULT 到异常类型
.NET 运行时在 COM 互操作中收到失败的 HRESULT 时,会把它转换成异常。已知的 HRESULT 会映射到对应的异常类型,未知的则变成 COMException。10
flowchart TB
accTitle: 从 HRESULT 到 .NET 异常的映射
accDescr: COM 互操作中收到的失败 HRESULT,已知的转换成对应的异常类型,未知的转换成 COMException,两种情况下原始值都保留在 Exception.HResult 中
hr["失败的 HRESULT"] --> known{"有已知的映射吗?"}
known -->|有| typed["转换成对应的异常类型"]
known -->|没有| comex["转换成 COMException"]
typed --> keep["原始值保留在 Exception.HResult"]
comex --> keep
图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 时,要把声明和取值方式配成一套。
- 在
DllImport(或LibraryImport)上指定SetLastError = true。 - 确认失败之后立刻用
Marshal.GetLastWin32Error取值。.NET 6 及以后可以使用等效的GetLastPInvokeError。16
不要采用把 GetLastError 本身声明为 P/Invoke 再调用的做法。因为运行时内部的 API 调用可能覆盖这个值,导致取不到正确的失败原因。16
flowchart TB
accTitle: P/Invoke 中取得最后错误
accDescr: 把 SetLastError 设为 true 并用 Marshal.GetLastWin32Error 取值才是正确做法,直接把 GetLastError 声明为 P/Invoke 会因运行时覆盖而不准确
pi["用 P/Invoke 调用 Win32 API"] --> ok["指定 SetLastError=true"]
ok --> get["用 GetLastWin32Error 取值"]
pi --> ng["直接声明并调用 GetLastError"]
ng --> bad["运行时会覆盖,结果不准确"]
图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);
// 返回值不用 IntPtr 而用 SafeFileHandle 接收,并用 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
flowchart TB
accTitle: err.exe 的检索结果要结合上下文挑选
accDescr: err.exe 横跨大量头文件列出相符的定义,因此同一个数值出现多个候选时,要结合上下文选出合适的那个
in["输入 err 5"] --> scan["横跨大量头文件检索"]
scan --> hits["命中多个定义"]
hits --> pick["结合上下文选出合适的候选"]
图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。
flowchart TB
accTitle: 自带命令的分工
accDescr: 十进制的 Win32 错误码可以用 net helpmsg 查,包含十六进制的代码则用 certutil 的 -error 选项查
q{"手上的代码是什么形式?"} -->|十进制的 Win32| net["net helpmsg"]
q -->|含十六进制| cert["certutil -error"]
net -.-> jp["返回中文消息"]
cert -.-> any["十六进制和十进制都接受"]
图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 确认含义。
flowchart TB
accTitle: 在 WinDbg 中确认异常代码的流程
accDescr: 崩溃转储中 analyze 命令会自动显示异常代码,把该代码传给 error 扩展并加上第二个参数 1,按 NTSTATUS 确认含义
dump["打开崩溃转储"] --> an["执行 !analyze -v"]
an --> exc["显示出异常代码"]
exc --> chk["用 !error 代码 1 确认含义"]
图19:转储分析中把 !analyze -v 显示出的异常代码,加上标志 1 交给 !error 查询。
8. 排查步骤——从判定层次到与上下文比对
实际排查要按以下五个阶段依次推进。要点是不要查到名称就收手,最后一定要确认失败的操作和对象。
- 统一记法。十进制负数要转换成 8 位十六进制。不足 8 位的十六进制要补零再读。
- 判定属于哪一层的代码。用下面的表从开头几位缩小出首选假设,再与返回它的 API 和它出现的位置比对。
- 拆解出本质代码。0x8007xxxx 形式的 HRESULT 取低 16 位,0xDxxxxxxx 形式的 HRESULT 则去掉 N 位再读。
- 用工具查名称和定义。用 err.exe、certutil、
!error等确认符号名和消息。 - 与上下文比对。用应用日志、事件日志和 Procmon 确定是哪个应用、在哪个操作中、哪个 API 对什么失败了。代码给出的是“失败的种类”,上下文给出的才是“原因所在”。
flowchart TB
accTitle: 错误码排查的步骤
accDescr: 把记法统一成十六进制,用开头几位判定层次,拆解出本质代码,用工具查出名称和定义,最后与上下文比对,这就是排查的套路
fix["统一记法(负数转成 8 位十六进制)"] --> judge{"开头几位是什么?"}
judge -->|十进制记法| d1["按 Win32 错误解读"]
judge -->|0x8007| d2["把低 16 位转成十进制"]
judge -->|0xC| d3["按 NTSTATUS 解读"]
judge -->|0xD| d4["去掉 N 位再读"]
d1 --> tool["用工具查名称和定义"]
d2 --> tool
d3 --> tool
d4 --> tool
tool --> ctx["与上下文比对(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、停止代码与 NTSTATUS 混为一谈
因为都带“5”就把 ERROR_ACCESS_DENIED 和 STATUS_ACCESS_VIOLATION 等同起来,排查就会偏向权限问题与程序缺陷这两个完全不同的方向。另外,蓝屏的停止代码与 NTSTATUS 是两套体系,拿 0x9F 去查 NTSTATUS 的表得不到有意义的答案。15
flowchart TB
accTitle: 错误 5 与 0xC0000005 的排查方向不同
accDescr: Win32 的错误 5 要按权限问题来查,NTSTATUS 的 0xC0000005 要按程序缺陷来查,把两者等同起来排查就会跑偏
a["Win32 的错误 5"] --> ad["排查权限问题"]
b["NTSTATUS 0xC0000005"] --> bd["排查程序缺陷"]
a -.-> memo["属于不同体系、互不相干的代码"]
b -.-> memo
图21:不要因为都带“5”就等同看待。错误 5 指向权限问题,0xC0000005 指向程序缺陷。
10. 总结
先分辨体系。Windows 的主要错误码分为 Win32 错误码、HRESULT、NTSTATUS 三套体系。先理顺十进制、十六进制、负数这些记法上的差异,再确认是哪一层的谁返回的。NTSTATUS 会在崩溃的异常代码和 Procmon 的 Result 列里遇到,但 Win32 的错误 5 与 0xC0000005 是两回事,停止代码更是另一套体系。
接着读结构,取出需要的信息。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。
排查的套路就是“统一记法→判定层次→拆解→查名称→与上下文比对”。代码能告诉你的只到失败的种类为止。下次遇到陌生的数字时,在把它粘进搜索框之前,先确认开头几位和它的出处。这第一步拆解,决定了之后要去哪里查。
相关文章
- 用 WinDbg + SOS 解读崩溃转储——采集之后的实务分析入门
- Windows 崩溃转储收集入门 - WER/ProcDump/WinDbg
- 异常处理中,catch 与日志应该放在哪里
- Process Monitor(ProcMon)实战指南——10 分钟定位“配置未生效”“ACCESS DENIED”
- Windows 事件日志、ETW 入门——让业务应用的日志接入操作系统标准机制
相关咨询领域
小村软件有限公司承接以错误码为起点的故障排查(“看不懂这个错误码的含义”“只有特定环境才出现 0x80070005”)、Win32 API 与 COM、.NET 混用的应用程序的错误处理设计,以及使用崩溃转储和 Process Monitor 定位原因的工作。哪怕只有一张错误对话框的截图,也欢迎来咨询。
参考链接
-
Microsoft Learn, Debug system error codes. 关于 Win32 系统错误码(0~15999)清单的索引,用带 FORMAT_MESSAGE_FROM_SYSTEM 标志的 FormatMessage 取得 GetLastError 返回代码的消息,WinINet/WinHTTP 错误(12000 号段)也定义在这个空间里,以及用 Microsoft Error Lookup Tool 和 !err 命令排查的方法。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Open Specifications, [MS-ERREF]: HRESULT. 关于 HRESULT 的位布局(S、R、C、N、X 位,11 位的 Facility,16 位的 Code),N 位表示该值是把 NTSTATUS 映射到 HRESULT 空间的结果,以及包含 FACILITY_WINDOWS_UPDATE(36) 在内的设施码清单。 ↩ ↩2 ↩3 ↩4
-
Microsoft Open Specifications, [MS-ERREF]: NTSTATUS. 关于 NTSTATUS 的位布局(2 位的 Sev、C 位、N 位、12 位的 Facility、16 位的 Code),以及严重性分为成功(00)、信息(01)、警告(10)、错误(11) 四种。 ↩ ↩2 ↩3
-
Microsoft Learn, HRESULT_FROM_WIN32 macro. 关于把 Win32 系统错误码映射为 HRESULT 值的 winerror.h 宏的定义。 ↩ ↩2 ↩3
-
Microsoft Learn, Structure of COM Error Codes. 关于 HRESULT 的严重性位与设施字段的作用,FACILITY_NULL、FACILITY_RPC、FACILITY_ITF、FACILITY_WIN32、FACILITY_WINDOWS 的各个值,以及 FACILITY_ITF 的代码含义由各个接口定义、同一个值也可能含义不同。 ↩ ↩2 ↩3
-
Microsoft Learn, Common HRESULT values. 关于 E_FAIL(0x80004005) 表示“Unspecified failure(未指定的失败)”,以及 E_ACCESSDENIED(0x80070005)、E_INVALIDARG(0x80070057)、E_OUTOFMEMORY(0x8007000E) 等高频 HRESULT 值的定义。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, The Microsoft Error Lookup Tool. 关于它是横跨 Winerror.h 等各种头文件显示十六进制状态码所关联消息文本的单体工具,下载文件名为 Err_6.4.5.exe,以及需要注意其收录的定义来自编译时点。 ↩ ↩2 ↩3
-
Microsoft Learn, certutil. 关于 certutil 的 -error 选项会显示错误码所关联的消息文本,以及采用类似 0x80070002 (WIN32: 2 ERROR_FILE_NOT_FOUND) 这种包含符号名的错误表示形式。 ↩ ↩2
-
Microsoft Learn, !error. 关于 WinDbg 的 !error 扩展会解码并显示 Win32、Winsock、NTSTATUS、NetAPI 的错误值,以及把标志指定为 1 时按 NTSTATUS 解释。 ↩ ↩2
-
Microsoft Learn, How to: Map HRESULTs and exceptions. 关于 COM 的 HRESULT 与 .NET 异常相互映射的机制,E_NOTIMPL→NotImplementedException 等对应表,没有明确映射的 HRESULT 会转换成 COMException,以及异常的 Message、Source 等会由 IErrorInfo 的信息初始化。 ↩ ↩2
-
Microsoft Learn, RtlNtStatusToDosError function (winternl.h). 关于该函数把 NTSTATUS 代码转换为对应的 Win32 系统错误码,未定义对应关系时返回 ERROR_MR_MID_NOT_FOUND,以及不存在做逆向转换的函数。 ↩ ↩2
-
Microsoft Learn, Last-Error Code. 关于最后错误代码按线程各自保存,应在失败之后立刻用 GetLastError 取得,成功时把代码覆盖为 0 的 API 与不这么做的 API 混杂存在,以及第 29 位保留给应用程序定义的代码。 ↩ ↩2
-
Microsoft Learn, RegOpenKeyExW function. 关于成功时返回 ERROR_SUCCESS,失败时把 Winerror.h 中定义的非零错误码作为返回值返回。 ↩
-
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
-
Microsoft Learn, Bug check code reference. 关于蓝屏上显示的错误检查代码(停止代码)清单,以及用 WinDbg 的 !analyze 扩展显示代码信息的方法。从清单中可以确认它与 NTSTATUS 是不同的独立编号体系。 ↩ ↩2
-
Microsoft Learn, Marshal.GetLastWin32Error Method. 关于它是取得设置了 SetLastError 标志的 P/Invoke 调用的最后错误代码的方法,直接把 GetLastError 声明为 P/Invoke 会因运行时内部的 API 调用覆盖而不可靠,以及 .NET 6 及以后推荐使用 GetLastPInvokeError。 ↩ ↩2
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
睡眠恢复后就出故障的应用——电源事件机制与扛得住恢复的业务应用设计
打开笔记本电脑时业务应用的通信已经断开——原因是设计没有考虑睡眠。本文依据一手资料讲解 WM_POWERBROADCAST 的通知流程、Modern Standby 的行为、断开与重连的设计、睡眠抑制以及调查命令。
DllMain 与加载器锁——“DLL 初始化里什么都别做”的真正原因
为什么不能在 DllMain 里调用 LoadLibrary 或与线程同步。本文依据一手资料,从串行化全部 DLL 通知的加载器锁机制,讲到死锁成立的典型场景、延迟初始化等正确设计,以及挂起的调查步骤。
“无响应”的真相——Windows 如何判定应用卡死,以及不会卡死的设计
Windows 的“无响应”是操作系统在窗口 5 秒未取出消息时作出判定、并换成幽灵窗口的机制。本文讲解判定的内部动作、卡死的常见原因、把繁重处理移出 UI 线程的设计,以及挂起的调查步骤。
Time Travel Debugging ── 把长期运行中不复现的缺陷“录下来”再倒回去
一个月才出一次的缺陷,崩溃转储只拍得到结果。本文讲解如何用 WinDbg 的 Time Travel Debugging(TTD) 录制执行并倒回,涵盖 TTD.exe 的录制设计、环形缓冲区、TTD.Calls 查询,以及与转储的分工。
参数为什么会坏掉 ── Windows 命令行参数的规则
在 Windows 上并不存在参数的数组,传给 CreateProcess 的是一条字符串,切分由接收方完成。本文讲解 CommandLineToArgvW・CRT・.NET 的切分规则,以及 .NET 的 ArgumentList 与 C++ 中正确的组装方法。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
故障调查 & 长期运行故障
整理间歇性故障、通信诊断、长期运行崩溃、失败路径测试基础的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
故障调查 & 根本原因分析
调查难以复现的故障、长时间运行后的问题、内存泄漏、通信停滞等棘手的生产环境问题。
常见问题
汇总了咨询这一主题时常见的问题。
- 错误 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 对哪个资源失败,才是定位原因的捷径。