Windows 应用崩溃时保留日志与转储的设计

· · Windows开发, 异常处理, 日志, WER, 崩溃转储, 故障排查

Windows 应用问题排查中最痛苦的,就是明明知道程序崩溃了,却没有留下崩溃原因的状态。

尤其是以下这类项目,这个问题会格外严重:

  • 只在客户环境中崩溃
  • 只在长时间运行之后才崩溃
  • WPF / WinForms / Windows 服务 / 常驻应用中,复现率很低
  • 涉及 COM、P/Invoke、native DLL、第三方 SDK
  • 只拿到了”异常消息”,却没有崩溃前的上下文

不过,首先要诚实地说一句:仅靠崩溃侧的进程,不可能”百分之百”留下日志。 考虑到栈损坏、内存破坏、fast fail、强制终止乃至断电,进程内最后一条日志本质上只是 best effort(尽力而为)。

实务中真正应该追求的,是打造一套不完全依赖崩溃进程本身的结构。也就是分成以下三层来考虑:

  1. 正常运行时的时间序列日志
  2. 崩溃瞬间的最终崩溃标记
  3. 由操作系统或另一个进程保留的崩溃证据

本文以 Windows 桌面应用、常驻应用、Windows 服务、设备联动工具为前提,整理即使因程序缺陷引发异常而崩溃,也不丢失可调查性的最佳实践

1. 先说结论

先把结论整理出来。

  • 最重要的是不要把”最后一条日志”押在单一的进程内处理程序上
  • 实务中最稳妥的组合是常规日志 + 最终崩溃标记 + WER LocalDumps
  • 如果是长时间运行、设备联动、插件、混用 native SDK 的场景,加入监控进程(watchdog / launcher / service) 会大幅增强可靠性。
  • 崩溃处理程序的铁律是不要做繁重的处理。压缩、HTTP 发送、DI 解析、UI 对话框、复杂 JSON 生成都应排除在外。
  • 崩溃时只在本地简短地留下记录,压缩、上传、通知放到下次启动后或另一个进程中处理。
  • 使用 WinForms 的 ThreadException 或 WPF 的 DispatcherUnhandledException 让程序表面上继续运行的设计,在面对程序缺陷时是危险的
  • 无论是 .NET 还是 native,怀疑状态已损坏的异常,都应以”记录后结束”为基本方针,而不是”恢复”
  • 要采集转储的话,PDB 和发布二进制文件的保存必须同时进行,否则事后无法解读。

归纳来说,最佳实践就是 “不要试图在崩溃的那一瞬间把所有事都做完,而是在崩溃之前、崩溃瞬间、崩溃之后分别承担各自的职责”

2. 为什么仅靠进程内处理无法做到”确保”

如果这一点含糊不清,整体设计就会摇摆不定。

2.1 崩溃线程的上下文本身可能已经损坏

未处理异常的钩子或顶层异常过滤器,有可能运行在已经损坏的线程上下文中。 这个时候,可能出现以下情况:

  • 栈已经很危险
  • 堆已损坏,再分配内存也很危险
  • 因为异常发生时持有的锁而导致等待,进而卡死
  • logger 自身依赖的对象已经损坏

这些情况相当常见。

因此,最后的处理程序不应被当作”什么都能做的地方”,而应视为”能做的事情非常少的地方”,这样更安全。

2.2 fast fail 和状态损坏类异常,前提是”最小限度的进程内动作”

在内存损坏或致命状态下,最好不要指望常规的异常处理能正常工作。 尤其是 native 侧的 __fastfail 系列,以及怀疑状态已损坏的异常,其设计方向是尽可能以最小开销立即终止

也就是说,比较自然的想法是:能写下最后一条进程内日志算是幸运,主要证据应交给 OS / 另一个进程

2.3 .NET 的未处理异常事件也不是”繁重恢复处理”的场所

.NETAppDomain.UnhandledException 很方便,但 这里能做的最好也只限于简短的记录

  • 会受到异常发生时所持有的锁的影响
  • 并非所有状态损坏类异常都能安全地被捕获
  • 如果在这里强行构建继续运行的方案,很容易在半损坏状态下”延命”

比较现实的理解是:“未处理异常事件 = 最后的通知”,而不是”安全的恢复地点”

3. 推荐架构 - 区分崩溃时(crash-time)与重启后(after-restart)

最容易梳理清楚的方法,是把崩溃时要做的事重启后要做的事分开。

阶段 目的 在哪里运行 要做的事
正常运行时 保留时间序列 应用内 结构化日志、心跳、边界事件
崩溃时 落下最低限度的证据 应用内 + OS 最终崩溃标记、WER 转储
结束后瞬间 检测异常退出 另一个进程 记录 exit code、判断是否重启、通知
下次启动后 处理繁重的后续工作 全新的健康进程 压缩、上传、用户通知、清理旧日志

按这种方式划分,整体设计会相当稳定。

3.1 最小配置

小型业务工具或内部使用的 WPF / WinForms 应用,很多情况下这种程度就够了:

  • 常规日志:本地的 append-only 文件
  • 最终崩溃标记:专用的简短文件
  • 转储:WER LocalDumps
  • 下次启动时:显示”上次异常终止,已保存诊断信息”

3.2 加强版配置

在以下场景中,建议再加强一个层级:

  • 24/7 运行
  • 设备控制、监控、常驻
  • 大量使用 COM / P/Invoke / native SDK
  • 存在子进程、插件、脚本执行
  • 客户环境不允许”一直停摆”

这种情况下,可以拆分为:

  • worker 进程:本体处理
  • launcher / watchdog / service:启动监控、记录退出、重启
  • WER LocalDumps:应用于 worker 侧
  • 下次启动或 watchdog:回收诊断信息

这样划分会相当贴合实务需求。

4. 常规日志的最佳实践

如果只靠崩溃时的最后一行来”战斗”,通常都会输。 真正起作用的是崩溃之前的常规日志

4.1 日志应重视”事后可关联的信息”,而不是”给人看的文章”

常规日志中至少应该包含以下项目:

  • UTC 时间戳
  • 自进程启动以来经过的时间
  • PID / TID
  • 应用名、版本、构建号、commit 标识
  • 会话 ID
  • 操作 ID / 任务 ID / 关联 ID
  • 模块名 / 界面名 / worker 名
  • 之前的外部影响
    • 文件写入
    • 数据库更新
    • 设备命令发送
    • 通信请求
  • 异常类型、HRESULT / Win32 错误码 / 异常代码
  • 主要输入参数的摘要
  • 不含敏感信息范围内的对象 ID

推荐使用每行一个事件的 JSON Lines 或 key=value 格式

与写给人看的长文相比,“事后能对照多个文件”这一点更重要。

4.2 关键事件要同步落地

如果把常规日志全部改成同步写入,会变重。 但如果全部依赖异步缓冲,一旦崩溃,缓冲内容会一并消失。

因此实务中比较现实的做法,是按级别区分处理方式。

  • Information 级别的琐碎事件:可以缓冲
  • Warning 及以上:尽早 flush
  • 重要的边界事件:同步落地
    • ProcessStart
    • ConfigLoaded
    • WorkerStarted
    • ExternalCommandSent
    • TransactionCommitted
    • RecoveryStarted
    • FatalPathEntered

简单说,就是至少把业务上的边界事件,扎扎实实地落到磁盘上

4.3 把”当前正在写的常规日志”和”最后的崩溃标记”分开

这一点相当重要。

如果试图把所有内容都塞进一个 rolling log,就会遇到:

  • 正在轮转(rotation)
  • 还堆在异步队列里
  • 异常发生后 logger 自身立刻挂了
  • 日志行写到一半就被截断

这些情况都会发生。

因此,建议至少拆成两个文件:

  • app-<session>.jsonl 常规时间序列日志
  • fatal-last.logfatal-<session>.log 专用于最终崩溃标记

明确”最后一行应该写在哪里”,光是这一点,现场排查就能轻松很多。

4.4 日志保存路径固定在本地,不使用网络路径

崩溃时依赖 UNC 路径、NAS、HTTP、云 API 是很危险的。

原因包括:

  • 网络瞬断
  • DNS 延迟
  • 凭据失效
  • 在 UI 线程上等待
  • 服务账户权限不足

崩溃时首先落到本地固定路径。 发送则放到下次启动后或另一个进程中处理。

4.5 文件名中要包含 session

只有日期是不够的,因为同一天可能会多次重启。

例如可以是这样的形式:

Logs\
  MyApp_20260318_101530_pid1234_session-4f1c.jsonl
  MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
  MyApp_watchdog_20260318.jsonl

只要“这是哪一次启动实例的记录”足够明确,分析速度就会有很大差别。

5. 最终崩溃标记的最佳实践

这里不是打造功能齐全的 logger 的地方。 而是只写一次、写得简短、尽量确保能落地的地方。

5.1 目的是”固定入口”,而不是”原因的细节”

最终崩溃标记中应该包含的信息,越精简越有力。

  • 发生时刻(UTC)
  • PID / TID
  • 会话 ID
  • 版本 / 构建号
  • 来自哪个钩子
    • AppDomain.UnhandledException
    • Application.ThreadException
    • DispatcherUnhandledException
    • SetUnhandledExceptionFilter
    • _set_invalid_parameter_handler
    • set_terminate
  • 异常类型或异常代码
  • 可能的话,简单的消息
  • 之前的操作 ID
  • 常规日志的文件名
  • 预期的转储文件夹

有这些就足够了。

5.2 崩溃处理程序中不应该做的事

以下每一项都有相当高的概率成为地雷。

  • 从 DI 容器解析 logger
  • 使用 async / await
  • 抛出 Task
  • 等待锁
  • 组装复杂的 JSON
  • 操作 COM 对象
  • 弹出 UI 对话框
  • 压缩
  • 发送 HTTP / SMTP / Slack / Teams 消息
  • 解析并汇总 dump
  • 吞掉异常继续运行

崩溃处理程序不是普通处理流程的延续。应该定位为”只做最少限度的本地写入然后结束”。

5.3 崩溃处理程序应该做的事

反过来,应该做的事情非常简单。

  1. 防止重入
  2. 只写一行
  3. flush
  4. 结束

按这个顺序执行。

如果可能的话,最好使用:

  • 提前创建好的专用文件夹
  • 提前确认存在的路径
  • 已确认 ACL 的保存位置

常规日志如果 flush 太频繁会变重,但fatal 标记的写入次数极少,所以这里可以强力 flush。 .NET 可以用 FileStream.Flush(true),native 可以用 FlushFileBuffers,把这一行当作“必须立刻落地”来处理,会更容易设计。

5.4 不要试图继续运行

对于由程序缺陷引起的意外异常,最好把最终处理程序视为记录装置,而不是恢复装置

尤其应该以”不继续运行”为基本方针的情况,包括:

  • NullReferenceExceptionInvalidOperationException,但发生在共享状态更新过程中
  • UI 线程上的意外异常
  • 从监控循环或父循环中漏出的意外异常
  • AccessViolationException
  • StackOverflowException
  • native 边界的异常
  • CRT 的 invalid parameter / purecall / terminate

“不想让程序崩掉”的心情可以理解,但在半损坏状态下硬撑,往往会让排查和运维更痛苦

结束进程时,.NET 可以考虑 Environment.FailFast,native 可以考虑 RaiseFailFastException__fastfail 这类立即终止的 API,不要指望 finally 或常规的收尾处理。

6. 各框架的注意事项

6.1 .NET 通用:AppDomain.CurrentDomain.UnhandledException

这是很有用的最后通知。 但这里应该避免繁重的恢复处理。

使用方式的基本原则很简单:

  • 写最终崩溃标记
  • 如有需要,向 Windows 事件日志写入最简消息
  • 不继续运行
  • 不在这里等待或重试

UnhandledException 很方便,但不应假设可以在这里把应用恢复到健康状态,这样更安全。

6.2 WinForms:Application.ThreadException

这个的难点在于,捕获 UI 线程的未处理异常后,可以表面上继续运行

如果用来把业务录入的可预期错误做成对话框倒还说得过去,但 不适合用于由程序缺陷引发的意外异常场景下的”继续运行”

如果真的要优先保障原因排查,建议:

  • ThreadException 中只做最小限度的记录
  • 或者转为 UnhandledExceptionMode.ThrowException
  • 之后结束进程,保留转储和日志

这样会更安全。

6.3 WPF:Application.DispatcherUnhandledException

WPF 也类似。

  • 主要针对 UI 线程上的异常
  • Handled 设为 true 可以表面上继续运行
  • 但面对程序缺陷这么做,界面状态和内部状态很容易出现偏差

因此在 WPF 中,最好不要把它当作用于延命的继续运行装置,而是当作记录的入口

6.4 不要把 TaskScheduler.UnobservedTaskException 当作主路径

不是”崩溃前的最后一道防线”

它可以用来辅助检测 Task 异常的遗漏,但 作为崩溃时确保记录的路径来说,力度不足

因此,它适合用于:

  • 早期发现异常观测遗漏
  • 开发过程中揭示 Task 设计上的疏漏

这类用途,但不应把它当作最终崩溃处理程序的主角

6.5 native Win32 / C++:不要过度信任 SetUnhandledExceptionFilter

在 native 侧,很容易想依赖 SetUnhandledExceptionFilter

但这运行在发生故障的线程上下文中,会受到:

  • 无效的栈
  • 深层递归
  • 已损坏的堆
  • 异常发生时持有的锁

这些因素的影响。

因此,SetUnhandledExceptionFilter 最好理解为 接收最后通知的、best effort 的入口

6.6 native C++ 也要捕获 CRT 的终止路径

在 native C++ 中,如果只关注未处理的 SEH,就会有遗漏。

具体来说,应该关注:

  • _set_invalid_parameter_handler
  • _set_purecall_handler
  • set_terminate

这一系列,是用来捕获C 运行时或 C++ 运行时引发的”终止路径”的。

实务中比较稳妥的做法是:

  • 在这些处理程序中也写最终崩溃标记
  • 但不做繁重的恢复处理
  • 确保结束进程
  • 主要证据交给 WER / dump

7. 以 WER LocalDumps 为基础

这一点在实务中相当有效。

7.1 首先推荐 WER LocalDumps

“崩溃后尽量确保留下最低限度证据”这个角度来说, 首选还是最容易上手的 WER LocalDumps。

理由很简单:

  • 可以由 OS 侧保留转储
  • 无需额外工具就能接入
  • 可按应用单独设置
  • 能把崩溃时的主要证据保存到进程之外

仅靠日志无法看到:

  • 哪个线程崩溃了
  • 在哪个调用栈崩溃的
  • 是哪个模块边界
  • managed / native / COM / SDK 哪一部分可疑

事后能看到这些信息是它的优势所在。

7.2 典型配置

例如,对于 MyApp.exe,如果想把转储保存到 C:\CrashDumps\MyApp,可以这样设置:

reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps\MyApp" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f

一开始按这个程度取舍就足够了。

首选建议
DumpFolder 专用文件夹
DumpCount 5~10
DumpType 开发机用 2,现场根据容量和保密要求选择 1 或 2

7.3 一定要确认转储保存路径的 ACL

日志和转储都是同样的道理,如果设置了写不进去的文件夹,就毫无意义

尤其是:

  • Windows 服务
  • 权限隔离的子进程
  • 现场机的受限账户
  • 涉及 UAC 的情况

在这些场景中,保存路径的 ACL 是空转的主要原因

保存路径最好确认:

  • 是否已提前创建
  • 是否做过写入测试
  • 是否有保留数量限制
  • 运维人员能否访问到

7.4 想在 WER 报告中附带当前日志时

如果使用面向 Microsoft 的 WER 报告或自建的 WER 机制,也可以用 WerRegisterFile注册当前日志文件,使其包含在错误报告中

但这里最好理解为本地保存的补充手段,而不是替代方案。 因为崩溃时真正需要的,首先是尽量确保留在本机上

顺序上,比较贴合实务的做法是:

  1. 本地常规日志
  2. 本地 fatal 标记
  3. 本地 dump
  4. 如有需要,再通过 WER 发送路径注册相关文件

7.5 不只保留转储,也要保留版本管理

即使采集了转储,如果事后发现:

  • 找不到当时的 EXE / DLL
  • 没有 PDB
  • 不知道对应哪个 commit 构建

那这份转储的价值会大幅下降。

至少应该保留:

  • 发布的二进制文件
  • 对应的 PDB
  • 版本号
  • 构建时间
  • commit 标识
  • 安装包版本

转储采集和 PDB 保存是一套的

8. 使用 MiniDumpWriteDump 或自建崩溃报告工具时的思路

有些场景需要自己实现相关功能。

  • 想在 UI 上提供”保存诊断信息”按钮
  • 想把日志和配置文件一起打包
  • 想统一处理一组子进程
  • 想在自动上传前加入自定义的信息脱敏

不过,这里最重要的是,不要把采集 dump 这项工作也全部压在崩溃侧的进程上

8.1 self-dump 不如另一个进程

MiniDumpWriteDump 很强大,但 从另一个进程调用,比从崩溃的那个进程内部调用更安全

典型的结构大致如下:

  • worker 本体检测到异常
  • 尽可能通过事件或命名管道通知 helper
  • helper 采集 worker 的 dump
  • helper 汇总 tail 日志或配置文件
  • helper 结束后将其放入上传队列

这样即使 worker 已经损坏,helper 侧依然是健康的。

8.2 如果无论如何都要放在进程内,就集中到专用线程

如果无法拆分到另一个进程, 把 dump 处理集中到专用线程会稍微好一些。

但即便如此,本质上仍是 best effort。 “加入了自定义 dump 实现”并不代表可以百分之百放心。

8.3 繁重的工作放到下次启动之后

在自建报告工具中,容易想加入以下功能:

  • zip 压缩
  • 与 symbol 信息比对
  • 上传到服务器
  • 截屏
  • 从数据库获取补充信息

这些都应该放到重启之后或 helper 侧,而不是崩溃当时。

9. 引入监控进程会带来什么变化

在长时间运行的系统中,监控进程会相当有效。

9.1 监控进程能保留的信息

在 watchdog / launcher / 父服务侧,可以保留以下信息:

  • 子进程启动时间
  • 启动参数
  • PID
  • 被监控对象的版本
  • 心跳最后接收时间
  • 结束时间
  • exit code
  • 重启次数
  • 是否存在 dump
  • 是否发生了重启

有了这些信息,就能大致看清:

  • 是否真的崩溃了
  • 是否是操作系统关机造成的
  • 是否是用户主动关闭的
  • 是否因为挂起而被 kill
  • 重启循环发生了几次

9.2 特别适合的场景

值得考虑进行进程拆分的场景包括:

  • 承载 vendor SDK 的 worker
  • 图像处理 / 视频处理 / 设备 I/O
  • 监控或轮询的父循环
  • 脚本或插件执行
  • 承载 COM / ActiveX 遗留资产的宿主
  • 64 位 / 32 位桥接或互操作

危险的处理封闭到单个 worker 里,日志设计和恢复设计都会轻松很多。

10. 常见的 NG 做法

10.1 用 catch (Exception) 只记录日志然后继续运行

这是最常见、也是最危险的做法。

  • 中途的修改残留下来
  • 共享状态被破坏
  • 后续故障增多
  • 真正的原因位置变得模糊

结果往往是:日志多了一条,但事故反而拖得更久

10.2 只依赖 async logger 的队列

异步日志本身并不坏。 问题在于,fatal path 也走同一个队列然后就结束了

一旦崩溃瞬间 worker 停止,那个队列也会一并消失。

只让 fatal path 直接写入,保留这样一条退路会更安全。

10.3 在崩溃处理程序中发送 HTTP 请求

这是很容易想去实现的功能,但相当危险。

  • DNS
  • TLS
  • 代理
  • 认证
  • 超时
  • 等待重发

这些全部都会叠加在已经崩溃的上下文里。

发送操作应该放到重启之后

10.4 有转储,但无法与常规日志关联

这种情况很常见。

  • 转储文件名里没有 session
  • 日志侧没有 PID / session
  • watchdog 侧没有 PID
  • build 号不一致

结果就是,三份证据看起来像是三件互不相关的事

10.5 用 WinForms / WPF 的未处理异常事件来延命

表面上”不再崩溃”,一开始会让人觉得挺好。 但实际上往往会出现:

  • 只剩界面
  • worker 已经死了
  • 按钮还能点
  • 不知道是否真的保存成功了

这种僵尸状态。

10.6 没有关注 native 侧的终止路径

如果只依赖 SetUnhandledExceptionFilter 就掉以轻心,会漏掉:

  • invalid parameter
  • purecall
  • terminate
  • fast fail

这些路径。

在 native C++ 中,不仅要关注 SEH,也要关注 CRT / C++ 运行时侧的终止路径,这样更稳妥。

11. 最低限度的落地检查清单

如果满足以下条件,说明已经相当实战化了。

  • 常规日志以”一行一事件”的方式保留
  • 所有日志都包含 UTC、PID、TID、version、session
  • ProcessStartProcessExit 都有记录
  • 重要边界事件会同步 flush
  • 存在专用的最终崩溃标记文件
  • fatal path 不经过 async logger
  • WER LocalDumps 已按应用单独配置
  • 已验证转储保存路径的 ACL
  • 保存了 PDB 和发布二进制文件
  • 下次启动时能检测到上次异常终止
  • 压缩 / 上传 / 通知在重启后或另一个进程中进行
  • native C++ 中也整理了 invalid parameter / purecall / terminate
  • 在验证机上故意让程序崩溃,确认是否真的留下了记录

最后一行尤其重要。 光是设计出来没有意义,必须做一次”确保能采集到”的实测

12. 应该测试到什么程度

把需要确认的项目整理成表格。

测试 确认什么
managed 的未处理异常 常规日志、fatal 标记、dump 是否全部齐全
UI 线程异常 WinForms / WPF 的事件路径是否符合预期
worker 线程异常 是否能到达 AppDomain.UnhandledException,watchdog 能否检测到
native 异常 WER dump 是否真的能采集到
invalid parameter / terminate CRT / C++ 运行时路径是否也留下了最小记录
强制 kill 即使进程内无法记录,watchdog 侧能否记录到意外退出
重启 下次启动后的通知、回收、上传是否正常运行

不能只满足于”异常抛出后应该会有日志”,而要确认”在这种条件下,这个文件确实会留下来”,这才是关键。

13. 总结

即便 Windows 应用因程序缺陷引发的异常而崩溃,如果想保留调查所需的信息,思路的主轴其实相当简单。

  • 不要只依赖崩溃侧的那个进程
  • 拆分为常规日志、最终崩溃标记、OS / 另一个进程侧的证据
  • 崩溃时只在本地简短地保留记录
  • 繁重的处理推迟到重启之后或另一个进程
  • 以 WER LocalDumps 为基础
  • 以”记录后结束”为基本方针,而不是”继续运行”

归根结底, 与”努力写好最后一行日志”相比,”打造一套即使没有最后一行也能追溯的结构”更强大

尽管如此,最后一行仍然值得争取, 最终崩溃标记应该以简短的形式保存在单独的文件中。 真正的主要证据,则交给 WER 的 dump 和之前的常规日志来承担。 这是 Windows 应用实务中相当稳定的做法。

相关文章

参考资料

  • Microsoft Learn: Collecting User-Mode Dumps https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
  • Microsoft Learn: Using WER https://learn.microsoft.com/en-us/windows/win32/wer/using-wer
  • Microsoft Learn: MiniDumpWriteDump function https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
  • Microsoft Learn: SetUnhandledExceptionFilter function https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
  • Microsoft Learn: System.AppDomain.UnhandledException event https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appdomain-unhandledexception
  • Microsoft Learn: Application.ThreadException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.threadexception
  • Microsoft Learn: Application.DispatcherUnhandledException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception
  • Microsoft Learn: TaskScheduler.UnobservedTaskException Event https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception
  • Microsoft Learn: Environment.FailFast https://learn.microsoft.com/en-us/dotnet/api/system.environment.failfast
  • Microsoft Learn: Registering for Application Recovery https://learn.microsoft.com/en-us/windows/win32/recovery/registering-for-application-recovery
  • Microsoft Learn: RegisterApplicationRecoveryCallback https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrecoverycallback
  • Microsoft Learn: WerRegisterFile https://learn.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterfile
  • Microsoft Learn: _set_invalid_parameter_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler
  • Microsoft Learn: _set_purecall_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler
  • Microsoft Learn: set_terminate https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-terminate-crt
  • Microsoft Learn: __fastfail https://learn.microsoft.com/en-us/cpp/intrinsics/fastfail

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

崩溃的应用自身能否确保留下日志?
不能。考虑到栈损坏、内存破坏、fast fail、强制终止直至断电等情况,进程内最后一条日志本质上只能是尽力而为(best effort)。实务中的做法是分成三层:正常运行时的时间序列日志、崩溃瞬间的最终崩溃标记,以及由操作系统或另一个进程保留的崩溃证据,从而形成一套不完全依赖崩溃进程本身的架构。最稳妥的组合是常规日志 + 最终崩溃标记 + WER LocalDumps。
在崩溃处理程序中不应该做哪些事?
所有偏重的处理都不应该做。从 DI 容器解析 logger、使用 async/await、等待锁、生成复杂 JSON、操作 COM 对象、弹出 UI 对话框、压缩、发送 HTTP/SMTP/Slack 消息等,都有很高概率变成地雷。应该做的只有 4 件事:防止重入、只写一行、flush,然后结束。压缩、上传、通知这类繁重的后续处理,应推迟到下次启动之后或交给另一个进程处理。
WER LocalDumps 应该如何配置?
在注册表 HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\应用名.exe 下,设置 DumpFolder(专用文件夹)、DumpCount(大约 5~10 个)、DumpType(开发机用 2,现场则根据容量和保密要求选择 1 或 2)。关键是要确认保存路径的 ACL:把路径设置成 Windows 服务或受限账户无法写入的文件夹,是最典型的失败案例。此外,转储收集要与 PDB、发布二进制文件的保存配套进行,缺了任何一项,事后都无法解读。
在 WPF 的 DispatcherUnhandledException 中吞掉异常继续运行可以吗?
面对由程序缺陷引发的意外异常,这样做很危险。虽然把 Handled 设为 true 可以让程序表面上继续运行,但很容易造成「界面还在,但工作线程已经死了」「按钮还能点,却不知道是否真的保存成功了」这类僵尸状态。未处理异常事件应当被当作记录的入口,而不是恢复装置:写完最终崩溃标记后就应该结束进程,不要继续运行,主要证据交给 WER 的转储和之前的常规日志会更安全。结束进程时可以考虑使用 Environment.FailFast 这类立即终止的 API。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表