更新履歴(4件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- 3層構造の全体関係図を追加しました。C#の最小実装(多重突入の防止、`Flush(true)`、`FailFast`を含み、呼び出し側も併記)を新設し、`UnhandledException`内で`FailFast`を呼ぶとダンプの原因がすり替わる点を注記しました。用語表、ダンプ取得後のWinDbgでの最初の手順、JSON Linesの実データ例を追加しました。
- 本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589698)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「Windowsアプリのクラッシュ時にログとダンプを残す設計」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589698 https://staging.comcomponent.com/blog/2026/03/19/000-windows-app-crash-logging-best-practices/
- DOI(最新版)
- 10.5281/zenodo.21589698
- DOI(この版)
- 10.5281/zenodo.21732721
Windows アプリの不具合調査で一番つらいのは、落ちたことだけは分かるのに、なぜ落ちたかが残っていない 状態です。
特に次のような案件では、この問題がかなり重くなります。
- 顧客環境でしか落ちない
- 長時間運転の末にだけ落ちる
- WPF / WinForms / Windows サービス / 常駐アプリで、再現率が低い
- COM、P/Invoke、native DLL、vendor SDK が絡む
- 「例外メッセージだけ」は取れているが、直前の文脈がない
ただし、最初に正直に言うと、落ちる側のプロセスだけで「必ず」ログを残すことはできません。 スタック破損、メモリ破壊、fast fail、強制終了、電源断まで含めると、in-process の最後のログは本質的に best effort です。
実務で目指すべきなのは、落ちるプロセスの中だけに期待しない構成 にすることです。 つまり、
- 通常時の時系列ログ
- 落ちる瞬間の最終クラッシュマーカー
- OS または別プロセス側で残すクラッシュ証跡
の 3 層で考えます。
この記事では、Windows デスクトップアプリ、常駐アプリ、Windows サービス、装置連携ツールを前提に、プログラムミスによる例外で落ちても、調査可能性を失わないためのベストプラクティス を整理します。
1. まず結論
先に結論だけ並べます。
- 「最後のログ」を 1 本の in-process ハンドラに賭けない のが最重要です。
- 実務で一番無難なのは、通常ログ + 最終クラッシュマーカー + WER LocalDumps の組み合わせです。
- 長時間運転、装置連携、プラグイン、native SDK 混在なら、監視プロセス(watchdog / launcher / service)を足す とかなり強くなります。
- クラッシュハンドラでは、重い処理をしない のが鉄則です。圧縮、HTTP 送信、DI 解決、UI ダイアログ、複雑な JSON 生成は外します。
- クラッシュ時はローカルへ短く残すだけ にして、圧縮・アップロード・通知は 次回起動後か別プロセス に回します。
- WinForms の
ThreadExceptionや WPF のDispatcherUnhandledExceptionを使って 見かけ上延命する設計は、プログラムミス相手では危険 です。 - .NET でも native でも、破損状態を疑う例外は「回復」より「記録して終了」 を基本にしたほうが安全です。
- ダンプを取るなら、PDB と配布バイナリの保管 を同時にやらないと後で読めません。
要するに、ベストプラクティスは 「落ちる瞬間に全部やろうとしない。落ちる前・落ちる瞬間・落ちた後で役割分担する」 です。
1.1 この記事で使う用語
以降で断りなく使う言葉を、先に並べておきます。
| 用語 | 展開・読み | 意味 |
|---|---|---|
| WER | Windows Error Reporting、Windows エラー報告 | アプリの異常終了を OS 側で捕まえて記録する Windows の仕組み。ローカルにダンプを残す設定が LocalDumps です |
| ダンプ / minidump | crash dump | 落ちた瞬間のプロセスのメモリ内容を保存したファイル。スレッド、スタック、モジュールを後から見られます |
| PDB | Program Database | ビルド時に生成されるシンボル ファイル。これがないと、ダンプを開いても関数名や行番号が出ません |
| in-process | プロセス内 | 落ちているそのプロセス自身の中で処理すること。反対語は別プロセス |
| best effort | 最善努力 | 「うまくいけば残る、保証はしない」という性質。落ちる瞬間の in-process ログはこれです |
fast fail / __fastfail |
高速障害終了 | 状態が壊れていると判断したとき、後始末をせず最小手数で即終了させる仕組み。native では __fastfail、.NET では Environment.FailFast が相当します |
| watchdog | 監視プロセス | 本体プロセスの起動・終了・生存を外から見張る別プロセス。launcher や親サービスとして作ることもあります |
| heartbeat | 生存信号 | 「まだ動いている」を watchdog へ定期的に知らせる合図 |
| UNC パス | Universal Naming Convention | \\server\share\... という形式のネットワーク共有パス。クラッシュ時に使うと、瞬断や資格情報で待たされます |
| ACL | Access Control List、アクセス制御リスト | そのフォルダーに誰が読み書きできるかの設定。ダンプやログが「空振り」する原因の常連です |
| SEH | Structured Exception Handling、構造化例外処理 | Windows のネイティブ例外機構。SetUnhandledExceptionFilter はここに載ります |
| CRT | C Runtime、C ランタイム | C / C++ の標準ライブラリ実装。SEH とは別に、独自の終了経路を持っています |
| session | セッション ID | 「どの起動インスタンスの話か」を識別するための値。ログ、ダンプ、watchdog 記録を突き合わせる鍵になります |
この記事の知識マップ
Windowsアプリがプログラムミスによる例外で落ちても原因を追えるようにするには、落ちるプロセス自身のログだけに頼らず、通常時系列ログ・落ちる瞬間の最終クラッシュマーカー・WER LocalDumpsによるダンプという3層で証跡を分担する設計が有効である。AppDomain.UnhandledExceptionやWinFormsのThreadException、WPFのDispatcherUnhandledExceptionは見かけ上の延命に使うと危険で、記録の入口として使ったうえでEnvironment.FailFastのような即時終了APIで終了させる方が安全だが、UnhandledExceptionの中でFailFastを呼ぶとダンプの原因がすり替わってしまう。native C++ではSEHに加えてCRTの終了経路も押さえる必要があり、24時間稼働や装置制御が絡む場合は監視プロセスを足すとexit codeや再起動回数まで外部から検知できるようになる。
flowchart LR
accTitle: Windowsアプリのクラッシュ時ログ・ダンプ設計の知識マップ
accDescr: 落ちるプロセスの中だけに証跡を期待しない設計が、通常ログ・最終クラッシュマーカー・WER LocalDumpsという3層の役割分担と、AppDomain.UnhandledExceptionやFailFastなど各種例外ハンドラの使い分け、監視プロセスによる外部からの検知で成り立つことを示す図
crash_time_logging_design["クラッシュ時のログ・証跡設計"]
wer_localdumps["WER LocalDumps"]
application_log["通常ログ(時系列ログ)"]
fatal_crash_marker["最終クラッシュマーカー"]
seh["SEH(構造化例外処理)"]
crt_termination_handler["CRT/C++ランタイムの終了経路"]
watchdog_process["監視プロセス(watchdog)"]
high_reliability_operation_requirement["24/7運転・装置制御などの強めの要件"]
post_restart_processing["次回起動後の後処理"]
session_id_correlation["session IDによる証跡の突き合わせ"]
dotnet_unhandledexception["AppDomain.UnhandledException"]
winforms_threadexception["Application.ThreadException(WinForms)"]
unexpected_exception_continuation["プログラムミス起点の想定外例外での処理継続"]
wpf_dispatcherunhandledexception["Application.DispatcherUnhandledException(WPF)"]
unobserved_task_exception["TaskScheduler.UnobservedTaskException"]
environment_failfast["Environment.FailFast"]
windows_application_event_log["Windowsアプリケーションイベントログ"]
dump_folder_acl["ダンプ保存先フォルダのACL"]
crash_dump["クラッシュダンプ"]
pdb["PDB(プログラムデータベース)"]
windbg["WinDbg"]
minidumpwritedump["MiniDumpWriteDump"]
wer_file_registration["WerRegisterFileによるログ添付登録"]
crash_time_logging_design -->|"前提とする"| application_log
crash_time_logging_design -->|"前提とする"| fatal_crash_marker
crash_time_logging_design -->|"前提とする"| wer_localdumps
crash_time_logging_design -.->|"前提とする"| seh
crash_time_logging_design -.->|"前提とする"| crt_termination_handler
watchdog_process -->|"推奨される対応"| high_reliability_operation_requirement
fatal_crash_marker -->|"より先に行うべき"| post_restart_processing
fatal_crash_marker -.->|"前提とする"| session_id_correlation
fatal_crash_marker -->|"推奨される対応"| dotnet_unhandledexception
winforms_threadexception -->|"用いるのは非推奨"| unexpected_exception_continuation
wpf_dispatcherunhandledexception -->|"用いるのは非推奨"| unexpected_exception_continuation
unobserved_task_exception -->|"用いるのは非推奨"| fatal_crash_marker
environment_failfast -->|"利用する"| windows_application_event_log
fatal_crash_marker -->|"推奨される対応"| seh
fatal_crash_marker -->|"推奨される対応"| crt_termination_handler
wer_localdumps -.->|"前提とする"| dump_folder_acl
crash_dump -.->|"前提とする"| pdb
crash_dump -->|"で確認できる"| windbg
minidumpwritedump -->|"実装を担う"| crash_dump
watchdog_process -.->|"利用する"| minidumpwritedump
wer_file_registration -->|"利用する"| application_log
wer_localdumps -->|"より先に行うべき"| wer_file_registration
wer_localdumps -->|"実装を担う"| crash_dump
fatal_crash_marker -->|"推奨される対応"| winforms_threadexception
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全24件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. なぜ in-process だけでは「確実」にできないのか
ここを曖昧にすると、設計がぶれます。
2.1 落ちたスレッドの文脈そのものが壊れていることがある
未処理例外のフックやトップレベル例外フィルタは、壊れた側のスレッド文脈で動く ことがあります。 この時点で、
- スタックがもう危ない
- ヒープ破壊で追加確保が危ない
- 例外発生時に取っていたロックのせいで待機すると止まる
- logger 自体が依存しているオブジェクトが既に壊れている
ということが普通にあります。
なので、最後のハンドラは「何でもできる場所」ではなく、「やれることがかなり少ない場所」 と見たほうが安全です。
2.2 fast fail や破損状態例外は「最小限の in-process 動作」前提
メモリ破壊や致命的な状態では、通常の例外処理に期待しないほうがよいです。
特に native 側の __fastfail 系や、破損状態を疑う異常は、「できるだけ少ないオーバーヘッドで即終了する」 方向で設計されています。
つまり、最後の in-process ログは書けたらラッキー、主証跡は OS / 別プロセス側 という考え方が自然です。
2.3 .NET の未処理例外イベントも「重い回復処理」の場ではない
.NET の AppDomain.UnhandledException は便利ですが、
ここでやってよいのは 短い記録 までと考えたほうがよいです。
- 例外発生時に保持していたロックの影響を受けうる
- 破損状態例外まで何でも安全に取れるわけではない
- ここで継続方針を無理に作ると、半壊れ状態で延命しやすい
「未処理例外イベント = 最後の通知」 であって、「安全な回復地点」ではない と捉えておくのが現実的です。
3. 推奨アーキテクチャ - crash-time と after-restart を分ける
一番整理しやすいのは、クラッシュ時にやること と 再起動後にやること を分ける方法です。
先に、3 層がそれぞれ どのプロセスの責任で、どこへ落ちるのか を 1 枚にしておきます。
flowchart TD
subgraph APP["アプリ プロセス"]
L1["通常ログ<br/>append-only の時系列"]
L2["最終クラッシュマーカー<br/>1 行だけ書いて終了"]
end
subgraph WIN["Windows 側"]
WER["WER LocalDumps<br/>プロセス外でダンプを保存"]
end
subgraph WD["watchdog プロセス"]
EX["exit code と終了時刻を記録<br/>再起動判断"]
end
subgraph NEXT["次回起動した健全なプロセス"]
POST["圧縮 / アップロード / 通知<br/>前回異常終了の検知"]
end
DISK[("ローカルの固定フォルダー")]
L1 --> DISK
L2 --> DISK
WER --> DISK
EX --> DISK
DISK --> POST
L1 -. 例外発生 .-> L2
L2 -. プロセス終了 .-> WER
L2 -. プロセス終了 .-> EX
ポイントは 3 つです。
- 落ちるプロセスが自分で書くのは、「アプリ プロセス」の枠の中にある 2 つだけ です。しかも最終クラッシュマーカーは「1 行書いて終わる」までです。
- 主証跡はプロセスの外 にあります。WER のダンプと watchdog の記録は、アプリが壊れていても残ります。
- 突き合わせの鍵は、共通の session ID と PID です。ここが揃っていないと、3 つの証跡が別々の事件に見えます。
| フェーズ | 目的 | どこで動かすか | やること |
|---|---|---|---|
| 通常時 | 時系列を残す | アプリ内 | 構造化ログ、heartbeat、境界イベント |
| クラッシュ時 | 最低限の証跡を落とす | アプリ内 + OS | 最終クラッシュマーカー、WER ダンプ |
| 終了直後 | unexpected exit を検知する | 別プロセス | exit code 記録、再起動判断、通知 |
| 次回起動後 | 重い後処理をする | 新しい健全なプロセス | 圧縮、アップロード、ユーザー通知、古いログ整理 |
この分け方にすると、設計がかなり安定します。
3.1 最小構成
小さめの業務ツールや社内向け WPF / WinForms なら、まずはこのくらいで足りることが多いです。
- 通常ログ: ローカルの append-only ファイル
- 最終クラッシュマーカー: 専用の短いファイル
- ダンプ: WER LocalDumps
- 次回起動時: 「前回異常終了しました。診断情報があります」を出す
3.2 強めの構成
1 段強くしたほうがよいのは、こんな要件のときです。
- 24/7 運転
- 装置制御、監視、常駐
- COM / P/Invoke / native SDK が多い
- 子プロセス、プラグイン、スクリプト実行がある
- 顧客環境で「止まりっぱなし」が許されない
この場合は、
- worker プロセス: 本体処理
- launcher / watchdog / service: 起動監視、exit 記録、再起動
- WER LocalDumps: worker 側
- 次回起動または watchdog: 診断情報回収
に分けると、かなり実務向きになります。
4. 通常ログのベストプラクティス
クラッシュ時の最後の 1 行だけで戦おうとすると、だいたい負けます。 本当に効くのは、直前までの通常ログ です。
4.1 ログは「人間向け文章」より「あとで相関できる情報」
通常ログに最低限入れておきたい項目を挙げます。
- UTC タイムスタンプ
- プロセス開始からの経過時間
- PID / TID
- アプリ名、バージョン、ビルド番号、コミット識別子
- セッション ID
- 操作 ID / ジョブ ID / 相関 ID
- モジュール名 / 画面名 / ワーカー名
- 直前の外部作用
- ファイル書き込み
- DB 更新
- 装置コマンド送信
- 通信要求
- 例外型、HRESULT / Win32 エラー / 例外コード
- 主要な入力パラメータの要約
- 機密を含まない範囲での対象 ID
おすすめは、1 行 1 イベントの JSON Lines か key=value 形式 です。
JSON Lines なら、1 イベントがこれくらいの粒度になります。
{"ts":"2026-03-18T01:15:33.412Z","up_ms":184213,"pid":1234,"tid":9,"app":"MyApp","ver":"3.2.1.884","commit":"9f1c2ab","session":"4f1c","level":"Warning","module":"DeviceWorker","op":"JOB-20260318-0042","event":"ExternalCommandSent","target":"COM3","cmd":"SEQ_START","timeout_ms":3000}
長いですが、これで 1 行だけ見れば「いつ・どの起動インスタンスの・どのバージョンが・どの操作の途中で・何をした」が分かります。ダンプ側とは pid と session、ビルド側とは ver と commit でつながります。
key=value 形式にするなら、同じ内容がこうなります。
ts=2026-03-18T01:15:33.412Z up_ms=184213 pid=1234 tid=9 app=MyApp ver=3.2.1.884 commit=9f1c2ab session=4f1c level=Warning module=DeviceWorker op=JOB-20260318-0042 event=ExternalCommandSent target=COM3 cmd=SEQ_START timeout_ms=3000
人間向けに長文を残すより、 「あとで 3 ファイルを突き合わせられる」 ことのほうが重要です。
4.2 クリティカルイベントは同期的に残す
通常ログを全部同期書き込みにすると重くなります。 ただし、全部を非同期バッファ任せにすると、落ちた瞬間にまとめて消えます。
なので、実務ではレベルに応じて扱いを変えるのが現実的です。
Informationの細かいイベント: バッファしてよいWarning以上: 早めに flush- 重要な境界イベント: 同期的に残す
- ProcessStart
- ConfigLoaded
- WorkerStarted
- ExternalCommandSent
- TransactionCommitted
- RecoveryStarted
- FatalPathEntered
要は、業務上の境界だけはちゃんと地面に落とす ということです。
4.3 「今書いている通常ログ」と「最後のクラッシュマーカー」は分ける
これはかなり大事です。
1 本の rolling log だけに全部入れようとすると、
- ローテーション中だった
- 非同期キューに残っていた
- 例外発生直後に logger 自体が死んだ
- ログ行の途中で切れた
ということが起きます。
そこで、最低でも 2 本に分けておくのがおすすめです。
app-<session>.jsonl通常時系列ログfatal-last.logまたはfatal-<session>.log最終クラッシュマーカー専用
「最後の 1 行をどこに残すか」が明確 になっているだけで、現場でかなり助かります。
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 を作る場所ではありません。 1 回だけ、短く、確実寄りに残す 場所です。
5.1 目的は「原因の詳細」ではなく「入口の固定」
最終クラッシュマーカーに入れるべき情報は、絞ったほうが強いです。
- 発生 UTC
- PID / TID
- セッション ID
- バージョン / ビルド番号
- どのフックから来たか
AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- 例外型または例外コード
- 可能なら簡単なメッセージ
- 直前の操作 ID
- 通常ログのファイル名
- dump 想定フォルダ
これだけで十分です。
5.2 クラッシュハンドラでやってはいけないこと
どれも、かなり高い確率で地雷になります。
- DI コンテナから logger を解決する
- async / await を使う
- Task を投げる
- ロック待ちをする
- 複雑な JSON を組み立てる
- COM オブジェクトを触る
- UI ダイアログを出す
- 圧縮する
- HTTP / SMTP / Slack / Teams 送信
- dump を解析して要約する
- 例外を握りつぶして継続する
クラッシュハンドラは、普通の処理フローの続きではない です。 「最小限のローカル書き込みだけして終わる」に寄せます。
5.3 クラッシュハンドラでやること
逆に、やることはかなり単純です。
- 多重突入を防ぐ
- 1 行だけ書く
- flush する
- 終了する
この順です。
できれば、
- 事前に作っておいた専用フォルダ
- 事前に存在確認済みのパス
- ACL を確認済みの保存先
を使います。
通常ログでは flush をやりすぎると重いですが、fatal マーカーは件数が極小 なので、ここだけは強めに flush してよいです。
.NET なら FileStream.Flush(true)、native なら FlushFileBuffers のように、「この 1 行だけは今すぐ地面に落とす」 扱いに寄せると設計しやすくなります。
5.4 継続させようとしない
プログラムミス起点の unexpected な例外なら、最終ハンドラは 回復装置ではなく記録装置 と考えたほうが安全です。
特に「継続しない」を基本にしたいのは、このあたりです。
NullReferenceExceptionやInvalidOperationExceptionでも、共有状態更新の途中だった- UI スレッドで unexpected な例外
- 監視ループや親ループから漏れた unexpected 例外
AccessViolationExceptionStackOverflowException- native 境界の異常
- CRT の invalid parameter / purecall / terminate
「落としたくない」気持ちは分かりますが、半壊れで生き延びるほうが診断も運用もつらい ことが多いです。
終了させるときは、.NET なら Environment.FailFast、native なら RaiseFailFastException や __fastfail のような 即時終了系 API を検討し、finally や通常の後始末に期待しない設計のほうが安全です。
5.5 C# での最小実装
ここまでの方針を、そのまま .NET 6 以降の C# に落とすと、これくらいの分量になります。DI も logger も使いません。
using System;
using System.IO;
using System.Text;
using System.Threading;
internal static class FatalMarker
{
// 事前に作り、書き込みテストまで済ませたローカル固定パスにする
private static readonly string LogDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp", "Logs");
// 通常ログ・ダンプ・watchdog 記録と突き合わせるための鍵
private static readonly string SessionId = Guid.NewGuid().ToString("N").Substring(0, 8);
// 多重突入を防ぐフラグ。0 なら未書き込み
private static int _written;
/// <summary>アプリ起動直後に 1 回だけ呼ぶ。</summary>
public static void Install()
{
// クラッシュ時に CreateDirectory を呼びたくないので、ここで作っておく
Directory.CreateDirectory(LogDirectory);
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
}
/// <summary>「これ以上続けてはいけない」と判断した場所から呼ぶ。</summary>
public static void FailNow(string reason)
{
Write("FailNow", null, reason);
Environment.FailFast(reason);
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Write("AppDomain.UnhandledException", e.ExceptionObject as Exception, null);
// ここでは終了させない。未処理例外なので、この後 CLR が既定の終了処理へ進む。
// ここで FailFast を呼ぶと、WER に残るダンプの原因が
// 元の例外ではなく FailFast にすり替わってしまう。
}
private static void Write(string hook, Exception ex, string note)
{
// 2 回目以降は何もしない
if (Interlocked.Exchange(ref _written, 1) != 0)
{
return;
}
try
{
string fileName = string.Format(
"MyApp_fatal_{0:yyyyMMdd_HHmmss}_pid{1}_session-{2}.log",
DateTime.UtcNow, Environment.ProcessId, SessionId);
string path = Path.Combine(LogDirectory, fileName);
// シリアライザを呼ばず、文字列連結だけで 1 行を組み立てる
string line = string.Join("\t",
"ts=" + DateTime.UtcNow.ToString("O"),
"pid=" + Environment.ProcessId,
"tid=" + Environment.CurrentManagedThreadId,
"session=" + SessionId,
"hook=" + hook,
"type=" + (ex != null ? ex.GetType().FullName : "none"),
"message=" + Flatten(ex != null ? ex.Message : note),
"log=" + LogDirectory);
using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
byte[] bytes = new UTF8Encoding(false).GetBytes(line + Environment.NewLine);
stream.Write(bytes, 0, bytes.Length);
// true を渡すと、OS のキャッシュではなくディスクまで落とす
stream.Flush(true);
}
}
catch
{
// ここで失敗したら、もうできることはない。握って終わる
}
}
private static string Flatten(string s)
{
return string.IsNullOrEmpty(s) ? string.Empty : s.Replace("\r", " ").Replace("\n", " ");
}
}
呼び出し側は、起動直後に Install() を呼ぶだけ です。ここを忘れると、上のコードは 1 バイトも効きません。
internal static class Program
{
private static void Main(string[] args)
{
FatalMarker.Install();
// 以降、通常のアプリ起動処理
RunApplication(args);
}
private static void RunApplication(string[] args)
{
// 例: 共有状態が壊れたと判断したら、継続せずに落とす
// FatalMarker.FailNow("device state inconsistent");
}
}
この 60 行が守っているのは、5.3 で挙げた 4 つだけです。
Interlocked.Exchangeで 多重突入を防ぐ- 文字列連結だけで 1 行書く
Flush(true)で ディスクまで落とすUnhandledExceptionからは 継続させない
Environment.FailFast は、メッセージを Windows のアプリケーション イベント ログに書いてからプロセスを即時終了させ、その内容をエラー報告にも含めます。つまり FailFast 自体も証跡を 1 本増やします。ただし前述のとおり、未処理例外の経路でこれを呼ぶとダンプの見え方が変わるので、呼ぶ場所は選んでください。
6. フレームワーク別の注意点
6.1 .NET 共通: AppDomain.CurrentDomain.UnhandledException
これは 最後の通知 として有用です。 ただし、ここでの重い回復処理は避けます。
使い方の基本はシンプルです。
- 最終クラッシュマーカーを書く
- 必要なら Windows Event Log に最小メッセージを残す
- 継続しない
- ここで待機や再試行をしない
UnhandledException は便利ですが、ここでアプリを健康な状態へ戻せる前提にはしない ほうが安全です。
6.2 WinForms: Application.ThreadException
これは、UI スレッドの未処理例外を拾って見かけ上継続できてしまう のが難しいところです。
業務入力の想定内エラーをダイアログ化する用途ならまだしも、 プログラムミス起点の unexpected 例外で継続する用途には向きません。
本当に原因調査を優先するなら、
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 に期待したくなります。
ただし、これは faulting thread の文脈で動く ので、
- 無効スタック
- 深い再帰
- 既に壊れたヒープ
- 例外発生時のロック保持
の影響を受けます。
したがって、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 側でダンプを残せる
- 追加ツールなしで入れやすい
- アプリ単位で設定できる
- クラッシュ時の主証跡を in-process 以外へ逃がせる
ログだけでは分からない
- どのスレッドが落ちたか
- どのスタックで落ちたか
- どのモジュール境界だったか
- 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 or 2 |
7.3 dump 保存先の ACL を必ず確認する
ログでもダンプでも同じですが、書けないフォルダに設定しても意味がありません。
特に、
- Windows サービス
- 権限分離した子プロセス
- 現場機の制限アカウント
- UAC 絡み
では、保存先 ACL が空振りの主因 になります。
保存先は、
- 事前作成
- 書き込みテスト
- 保持数制限
- 運用担当が見に行ける場所か
まで確認します。
7.4 WER レポートへ現在ログを添付したいとき
Microsoft への WER レポートや独自の WER 運用を使う場合は、WerRegisterFile で 現在のログファイルをエラーレポートに含めるための登録 を行う方法もあります。
ただし、ここは ローカル保存の代替ではなく追加導線 と考えたほうが安全です。 クラッシュ時に本当に欲しいのは、まず 手元の端末に確実寄りに残ること だからです。
順番としては、
- ローカル通常ログ
- ローカル fatal マーカー
- ローカル dump
- 必要なら WER 送信経路でも関連ファイルを登録
のほうが実務向きです。
7.5 ダンプだけでなく版管理を残す
ダンプを取っても、後で
- その時の EXE / DLL がない
- PDB がない
- どのコミットのビルドか分からない
となると、かなり弱くなります。
最低でも、これだけは残しておきます。
- 配布したバイナリ
- 対応する PDB
- バージョン
- ビルド日時
- コミット識別子
- インストーラ版
ダンプ収集と PDB 保管は セット です。
7.6 取れたダンプを最初に開くところまで
「ダンプは溜まっているが、誰も開いたことがない」は本当によくあります。深追いは専門記事に譲るとして、最初の 10 分ぶんだけ ここに置いておきます。
準備は 2 つです。
- その dump と 同じビルドの PDB と配布バイナリ を 1 つのフォルダーへ集める
- Windows SDK の Debugging Tools for Windows に入っている WinDbg を用意する
あとは、WinDbg で .dmp を開いてから順に打ちます。
.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols;C:\myapp\pdb
.reload /f
.ecxr
!analyze -v
~*k
lm v
それぞれの役割はこうです。
| コマンド | 何をするか |
|---|---|
.sympath |
シンボルの探索先を設定する。Microsoft のシンボル サーバーと、自分の PDB 置き場を両方指定する |
.reload /f |
シンボルを強制的に読み直す |
.ecxr |
例外発生時のレジスタ コンテキストへ移動する。これを忘れると、落ちた場所ではなく WER 側の待機スタックを見てしまう |
!analyze -v |
現在の例外を自動解析し、詳細表示する。まずここを読む |
~*k |
全スレッドのコール スタックを出す。落ちたスレッド以外が何をしていたかが見える |
lm v |
読み込まれているモジュールとバージョンを出す。配布物とビルド番号の突き合わせに使う |
ここまで来て「関数名が出ない」「行番号が出ない」なら、原因はだいたい PDB が違うビルドのもの です。7.5 の版管理へ戻ってください。
より踏み込んだ収集と解析の話は、末尾の関連記事にまとめてあります。
8. MiniDumpWriteDump や独自クラッシュレポータを使うときの考え方
独自実装が必要になる場面もあります。
- UI から「診断情報を保存」ボタンを出したい
- ログや設定ファイルも束ねたい
- 子プロセス群をまとめて扱いたい
- 自動アップロード前に独自マスキングを入れたい
ただし、ここで一番大事なのは、dump を取る処理も落ちる側に背負わせすぎない ことです。
8.1 self-dump より別プロセス
MiniDumpWriteDump は強力ですが、
クラッシュしたそのプロセスの中から呼ぶより、別プロセスから呼ぶほうが安全 です。
典型的にはこんな構成になります。
- worker 本体が異常を検知
- 可能ならイベントや名前付きパイプで helper に通知
- helper が worker の dump を取る
- helper が
tailログや設定ファイルを束ねる - helper が終了後にアップロードキューへ置く
これなら、worker が壊れていても helper 側はまだ健全です。
8.2 どうしても in-process なら専用スレッドに寄せる
別プロセス化できない場合でも、 専用スレッドを dump 専用にしておく とマシです。
ただし、それでも本質は best effort です。 「独自 dump 実装を入れたから 100% 安心」にはなりません。
8.3 重いことは次回起動後に回す
独自レポータでやりたくなりがちなことがあります。
- zip 圧縮
- symbol 情報との突き合わせ
- サーバーアップロード
- 画面キャプチャ
- DB から追加情報取得
これらは クラッシュ時ではなく、再起動後や helper 側 に回します。
9. 監視プロセスを入れると何が変わるか
長時間運転系では、監視プロセスがかなり効きます。
9.1 監視プロセスが残すもの
watchdog / launcher / 親サービス側では、こうした情報を残せます。
- 子プロセス開始時刻
- 起動引数
- PID
- 監視対象バージョン
- heartbeat の最終受信時刻
- 終了時刻
- exit code
- restart 回数
- dump の有無
- 再起動したかどうか
これがあるだけで、
- 本当にクラッシュしたのか
- OS シャットダウンだったのか
- ユーザーが閉じたのか
- hang から kill されたのか
- 何回再起動ループしたのか
がかなり見えます。
9.2 特に向いているケース
分離を前向きに検討してよいのは、こんなケースです。
- vendor SDK を抱えた worker
- 画像処理 / 動画処理 / device I/O
- 監視やポーリングの親ループ
- スクリプトやプラグイン実行
- COM / ActiveX 既存資産のホスト
- 64bit / 32bit ブリッジや相互運用
危ない処理を 1 個の worker に閉じ込める と、ログ設計も復旧設計も楽になります。
10. よくある NG
ここは 設計レベルの落とし穴 を集めています。「クラッシュハンドラの中で何をしてはいけないか」という手続きレベルの話は 5.2 にまとめてあるので、実装中の方はそちらを見てください。重なって見える項目 (10.3 の HTTP 送信など) は、5.2 が「なぜハンドラ内でやるとまずいか」、こちらが「その結果、運用で何が起きるか」を書いています。
10.1 catch (Exception) でログだけ出して続ける
一番ありがちで、一番危ないです。
- 途中変更が残る
- 共有状態が壊れる
- 後続障害が増える
- 真の原因地点がぼける
ログが 1 本増える代わりに、事故が長引く ことが多いです。
10.2 async logger のキューだけを信じる
非同期ログ自体は悪くありません。 問題は、fatal path でも同じキューへ積んで終わる ことです。
落ちた瞬間にワーカーが止まれば、そのキューごと消えます。
fatal path だけは直接書く 逃げ道を持ったほうが安全です。
10.3 クラッシュハンドラで HTTP 送信する
実装したくなるのですが、かなり危険です。
- DNS
- TLS
- proxy
- 認証
- タイムアウト
- 再送待ち
全部が落ちた文脈に乗ります。
送るのは 再起動後 にします。
10.4 dump はあるが、通常ログと結びつかない
これは多いです。
- ダンプファイル名に session がない
- ログ側に PID / session がない
- watchdog 側に PID がない
- build 番号が一致しない
結果として、3 つの証跡が別々の話に見える ようになります。
10.5 WinForms / WPF の未処理例外イベントで延命する
見かけ上「落ちなくなる」ので、最初は喜ばれます。 でも、実際には
- 画面だけ残る
- ワーカーは死んでいる
- ボタン活性だけ残る
- 保存できたかどうか分からない
というゾンビ状態を作りがちです。
10.6 native 側の終了経路を見ていない
SetUnhandledExceptionFilter だけで安心すると、
- invalid parameter
- purecall
- terminate
- fast fail
側を取りこぼします。
native C++ では、SEH だけでなく CRT / C++ ランタイム側の終了経路も意識 したほうがよいです。
11. 最低限の導入チェックリスト
以下を満たしていれば、かなり実戦的です。
- 通常ログが 1 行 1 イベントで残る
- すべてのログに UTC、PID、TID、version、session がある
ProcessStartとProcessExitが残る- 重要境界イベントは同期的に flush される
- 最終クラッシュマーカー専用ファイルがある
- fatal path では async logger を経由しない
- WER LocalDumps がアプリ単位で設定されている
- dump 保存先の ACL を検証済み
- PDB と配布バイナリを保管している
- 次回起動時に前回異常終了を検知できる
- 圧縮 / アップロード / 通知は再起動後または別プロセスで行う
- native C++ では invalid parameter / purecall / terminate も整理した
- 検証機で意図的に落として、本当に残るか を確認した
最後の 1 行が特に重要です。 設計しただけでは意味がなく、必ず「取り切る試験」をする 必要があります。
12. どこまで試験するか
確認しておきたい項目を表にまとめました。
| 試験 | 何を確認するか |
|---|---|
| managed の未処理例外 | 通常ログ、fatal マーカー、dump が全部そろうか |
| UI スレッド例外 | WinForms / WPF のイベント経路が想定通りか |
| worker スレッド例外 | AppDomain.UnhandledException まで来るか、watchdog が検知できるか |
| native 例外 | WER dump が本当に取れるか |
| invalid parameter / terminate | CRT / C++ ランタイム経路でも最小記録が残るか |
| 強制 kill | in-process では無理でも watchdog 側が unexpected exit を記録できるか |
| 再起動 | 次回起動後の通知、回収、アップロードが動くか |
「例外が飛んだらログが出るはず」ではなく、「この条件でこのファイルが残る」と確認する のが大事です。
13. まとめ
Windows アプリがプログラムミスによる例外で落ちても、調査に必要な情報を残したいなら、考え方の軸はかなり単純です。
- 落ちる側のプロセスだけに期待しない
- 通常ログ、最終クラッシュマーカー、OS / 別プロセス側の証跡に分ける
- クラッシュ時はローカルへ短く残すだけ
- 重い処理は再起動後か別プロセスへ回す
- WER LocalDumps を土台にする
- 継続より、記録して終了を基本にする
結局のところ、 「最後の 1 行を頑張る」より、「最後の 1 行がなくても追える構成を作る」 ほうが強いです。
それでも最後の 1 行は欲しいので、 最終クラッシュマーカーは別ファイルに短く残す。 そして、本当の主証跡は 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
- Microsoft Learn: FileStream.Flush(Boolean) https://learn.microsoft.com/en-us/dotnet/api/system.io.filestream.flush
- Microsoft Learn: !analyze (WinDbg) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-analyze
- Microsoft Learn: .ecxr (Display Exception Context Record) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-ecxr–display-exception-context-record-
- Microsoft Learn: Symbol path for Windows debuggers https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/symbol-path
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Windowsクラッシュダンプ収集入門 - WER/ProcDump/WinDbg
再現しにくいWindowsアプリのクラッシュを追うために、WER LocalDumps、ProcDump、MiniDumpWriteDump、WinDbgの使い分けと運用上の注意点を整理します。
障害対応は復旧で終わらない ── 小さな開発チームのためのポストモーテム(再発防止)の型
障害を「直して謝って終わり」にすると同じ障害を繰り返します。blameless postmortemを小規模チーム向けに翻訳し、1時間で書けるテンプレート、再発防止策の強度判断表、実施のトリアージまでをまとめます。
スリープ・休止・Modern Standbyと長時間稼働アプリ ── 「夜中に止まっていた」を設計で防ぐ
長時間動き続けるWindowsアプリが「朝見たら止まっていた」となる原因を、S3スリープ/休止/Modern Standbyの違いから整理します。スリープ中のタイマーやTCP接続の挙動、SetThreadExecutionStateによる抑止まで解説します。
ソースコードも仕様書もないシステムを引き継いだら ── 止めずに運用・保守するための実務手順
ソースコードも仕様書もない業務システムの運用・保守を始める実務手順を整理します。動いている環境の保全とバックアップ、実行ファイル・DBの棚卸し、挙動からの仕様復元、延命・ラップ・再構築の判断まで解説します。
MAX_PATHとWindowsのパス・ファイル名の落とし穴 ── 260文字制限、予約名、末尾ドット、大文字小文字
「ファイルが見つかりません」の定番原因であるパス・ファイル名の制限を整理します。MAX_PATH=260文字の内訳、LongPathsEnabledによる長パス有効化、CON等の予約名、末尾ドットの正規化、Path.Combineの罠まで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
不具合調査 / 長期稼働テーマ
再現しにくい不具合、通信停止、長期稼働障害、失敗パス検証を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
不具合調査・原因解析
顧客環境でしか出ないクラッシュ、低再現率の異常終了、ダンプとログの突き合わせによる原因解析は、不具合調査・原因解析と相性がよいテーマです。
Windowsアプリ開発
WPF、WinForms、常駐アプリ、Windows サービスで、通常ログ、WER、watchdog をどう設計するかは Windows アプリ開発そのものに直結します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- クラッシュするアプリ自身で確実にログを残すことはできますか?
- できません。スタック破損、メモリ破壊、fast fail、強制終了、電源断まで含めると、in-processの最後のログは本質的にbest effortです。実務では、通常時の時系列ログ、落ちる瞬間の最終クラッシュマーカー、OSまたは別プロセス側で残すクラッシュ証跡の3層に分け、落ちるプロセスの中だけに期待しない構成にします。一番無難なのは、通常ログ+最終クラッシュマーカー+WER LocalDumpsの組み合わせです。
- クラッシュハンドラの中でやってはいけないことは何ですか?
- 重い処理全般です。DIコンテナからのlogger解決、async/await、ロック待ち、複雑なJSON生成、COMオブジェクト操作、UIダイアログ、圧縮、HTTP/SMTP/Slack送信などは高い確率で地雷になります。やることは、多重突入を防ぐ、1行だけ書く、flushする、終了する、の4つだけに寄せます。圧縮・アップロード・通知といった重い後処理は、次回起動後か別プロセスに回します。
- WER LocalDumpsはどう設定すればよいですか?
- レジストリのHKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\アプリ名.exe配下に、DumpFolder(専用フォルダ)、DumpCount(5〜10程度)、DumpType(開発機は2、現場は容量と機密要件を見て1か2)を設定します。重要なのは保存先ACLの確認で、Windowsサービスや制限アカウントでは書けないフォルダに設定して空振りするのが典型的な失敗です。またダンプ収集とPDB・配布バイナリの保管はセットで、どちらが欠けても後で読めなくなります。
- WPFのDispatcherUnhandledExceptionで例外を握って継続してよいですか?
- プログラムミス起点のunexpected例外相手では危険です。Handled=trueで見かけ上継続できますが、画面だけ残ってワーカーは死んでいる、ボタン活性だけ残って保存できたか分からない、というゾンビ状態を作りがちです。未処理例外イベントは回復装置ではなく記録の入口として使い、最終クラッシュマーカーを書いたら継続せずに終了させ、主証跡はWERのダンプと直前までの通常ログに持たせるほうが安全です。終了にはEnvironment.FailFastのような即時終了系APIを検討します。