Windows 应用崩溃时保留日志与转储的设计
· 小村 豪 · Windows开发, 异常处理, 日志, WER, 崩溃转储, 故障排查
Windows 应用问题排查中最痛苦的,就是明明知道程序崩溃了,却没有留下崩溃原因的状态。
尤其是以下这类项目,这个问题会格外严重:
- 只在客户环境中崩溃
- 只在长时间运行之后才崩溃
- WPF / WinForms / Windows 服务 / 常驻应用中,复现率很低
- 涉及 COM、P/Invoke、native DLL、第三方 SDK
- 只拿到了”异常消息”,却没有崩溃前的上下文
不过,首先要诚实地说一句:仅靠崩溃侧的进程,不可能”百分之百”留下日志。 考虑到栈损坏、内存破坏、fast fail、强制终止乃至断电,进程内最后一条日志本质上只是 best effort(尽力而为)。
实务中真正应该追求的,是打造一套不完全依赖崩溃进程本身的结构。也就是分成以下三层来考虑:
- 正常运行时的时间序列日志
- 崩溃瞬间的最终崩溃标记
- 由操作系统或另一个进程保留的崩溃证据
本文以 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 的未处理异常事件也不是”繁重恢复处理”的场所
.NET 的 AppDomain.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.log或fatal-<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.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- 异常类型或异常代码
- 可能的话,简单的消息
- 之前的操作 ID
- 常规日志的文件名
- 预期的转储文件夹
有这些就足够了。
5.2 崩溃处理程序中不应该做的事
以下每一项都有相当高的概率成为地雷。
- 从 DI 容器解析 logger
- 使用 async / await
- 抛出 Task
- 等待锁
- 组装复杂的 JSON
- 操作 COM 对象
- 弹出 UI 对话框
- 压缩
- 发送 HTTP / SMTP / Slack / Teams 消息
- 解析并汇总 dump
- 吞掉异常继续运行
崩溃处理程序不是普通处理流程的延续。应该定位为”只做最少限度的本地写入然后结束”。
5.3 崩溃处理程序应该做的事
反过来,应该做的事情非常简单。
- 防止重入
- 只写一行
- flush
- 结束
按这个顺序执行。
如果可能的话,最好使用:
- 提前创建好的专用文件夹
- 提前确认存在的路径
- 已确认 ACL 的保存位置
常规日志如果 flush 太频繁会变重,但fatal 标记的写入次数极少,所以这里可以强力 flush。
.NET 可以用 FileStream.Flush(true),native 可以用 FlushFileBuffers,把这一行当作“必须立刻落地”来处理,会更容易设计。
5.4 不要试图继续运行
对于由程序缺陷引起的意外异常,最好把最终处理程序视为记录装置,而不是恢复装置。
尤其应该以”不继续运行”为基本方针的情况,包括:
NullReferenceException或InvalidOperationException,但发生在共享状态更新过程中- UI 线程上的意外异常
- 从监控循环或父循环中漏出的意外异常
AccessViolationExceptionStackOverflowException- 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_handlerset_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 来注册当前日志文件,使其包含在错误报告中。
但这里最好理解为本地保存的补充手段,而不是替代方案。 因为崩溃时真正需要的,首先是尽量确保留在本机上。
顺序上,比较贴合实务的做法是:
- 本地常规日志
- 本地 fatal 标记
- 本地 dump
- 如有需要,再通过 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
ProcessStart和ProcessExit都有记录- 重要边界事件会同步 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
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows应用的crash dump收集入门 - 先搞清楚 WER / ProcDump / WinDbg怎么分工
本文整理在 Windows 应用追查难以复现的 crash 时,要先以 WER LocalDumps 按应用单独配置为起手式,再依现场状况追加 ProcDump,最后才考虑 MiniDumpWriteDump 自研收集的决策顺序。读完能理解 mini 与 full dump...
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
Windows 事件日志・ETW 入门 ── 让业务应用程序的日志接入操作系统标准机制
Windows 业务应用程序的日志,是不是只靠文件日志来解决?事件日志与 ETW,是运维人员和操作系统标准工具所能看到的另一层记录。本文整理三种手段的分工方式、.NET 中的写入方法、通过 EventSource 进行的 ETW 埋点、收集与排查的实务做法,以及来源未注册、...
用 WinDbg + SOS 解读崩溃转储 ── 采集之后的实务分析入门
本文说明如何用 WinDbg 与 SOS 扩展实际读取已采集的 Windows 崩溃转储。内容涵盖符号路径设置、以 !clrstack、!pe、!dumpheap -stat、!gcroot 追踪异常与内存泄漏的方法、原生崩溃的 !analyze -v,以及与 dotnet...
异常处理中,catch 与日志应该放在哪里
为了避免在深层 helper 中大范围 catch、各层重复记录日志,以及隐藏原因的结果化处理,本文整理捕获异常的边界、主日志记录点,以及恢复判断的职责划分。
常见问题
汇总了咨询这一主题时常见的问题。
- 崩溃的应用自身能否确保留下日志?
- 不能。考虑到栈损坏、内存破坏、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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。