更新履歴(初版のみ・2026年09月02日公開)
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.22249657)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「Time Travel Debugging ── 長期稼働で再現しない不具合を「録画」して巻き戻す」合同会社小村ソフト. https://doi.org/10.5281/zenodo.22249657 https://comcomponent.com/blog/windows-time-travel-debugging-ttd/
- DOI(最新版)
- 10.5281/zenodo.22249657
- DOI(この版)
- 10.5281/zenodo.22249658
「月に一度、深夜にだけ落ちる」「同じ操作をしても手元では二度と出ない」「ダンプは取れたが、落ちた場所を見ても『なぜその値になったか』が分からない」── 長期稼働するWindowsアプリの不具合調査で、いちばん時間を溶かすのはこの種の案件です。以前の記事「WinDbg + SOSでクラッシュダンプを読む」では、クラッシュダンプという1枚の写真をどう読むかを整理しました。この記事はその続きで、写真では足りない場面のための道具、Time Travel Debugging(TTD) を扱います。
TTDは、プロセスの実行を丸ごと録画し、あとから前にも後ろにも再生できるWinDbgの機能です。不具合が出るまで何度も再現を試みる代わりに、デバッガーのセッションを「巻き戻す」ことができます。1 対象読者は、ダンプとログを読む調査はひととおり経験していて、それでも原因に届かない案件を抱えているWindows / .NET / C++アプリの開発・保守担当です。前提環境はWindows 10/11またはWindows Server 2016以降と現行のWinDbgで、録画には管理者権限が要ります。12 難易度は中級です。
この記事の前提
| 項目 | 内容 |
|---|---|
| 対象読者 | ダンプとログでは原因に届かない、長期稼働・断続的な不具合を抱えるWindowsアプリの開発・保守担当 |
| 前提知識 | WinDbgでダンプを開いて !analyze -v や !clrstack を打った経験。SOS解析記事の内容は既知とします |
| 前提環境 | Windows 10/11 または Windows Server 2016/2019/2022/2025、WinDbg(現行版)、TTD.exe、管理者権限2 |
| 扱わないこと | Visual Studio EnterpriseのSnapshot Debugger連携、カーネルモード(TTDはユーザーモード専用3) |
1. まず結論
- ダンプは「状態」、TTDは「経路」を残します。公式ドキュメントも、ダンプは失敗に至った状態と実行経路を取り逃がしがちだと明記しています。1 落ちた瞬間の写真で原因が分からないなら、次に取るべきは写真の枚数ではなく録画です。
- 録画は重い。録画中の対象プロセスは5〜20倍以上遅くなり、トレースファイルは活動中に毎秒5〜50MB成長し、上限はありません。24 長期稼働のアプリに無条件で貼り付ける道具ではありません。
- 長期稼働に使うなら「録る範囲」を設計します。TTD.exeの
-ring/-maxFile(最後のNMBだけ残す)、-module(自社モジュールの実行中だけ録る)、-recordmode Manual(アプリ側から録画区間を指定する)、-monitor(起動のたびに録る)の4つの入口を第5章で整理します。2 - 再生の主役は3つ。位置(
!tt)、イベント(dx @$curprocess.TTD.Events)、クエリ(TTD.Calls/TTD.Memory)です。ba(アクセスブレークポイント)とg-(逆方向実行)を組み合わせると、「その値を最後に書いたのは誰か」をデバッガーに直接答えさせられます。5 - トレースにはメモリの中身が入ります。ファイルパス、レジストリ、メモリやファイルの内容など、個人情報・秘密情報を含み得ます。1 共有と保管は「機密ファイル」の扱いとして設計してください。
- TTDにできないことも先に押さえます。カーネルモードは録れず、保護されたプロセス(PPL)には注入できず、一度アタッチすると自分では外れず、再生中はメモリを書き換えられません。32
この記事の知識マップ
Time Travel Debugging(TTD)は、プロセスの命令実行を丸ごと録画してあとから前後に再生できるWinDbgの機能で、録画は管理者権限を要する操作で、記録した命令実行は.runトレースファイルに保存され、WinDbgが再生用にインデックスファイル(.idx)を作って使います。録画は5〜20倍の速度低下とトレースの肥大化を招くため、TTD.exeのリングバッファ(-ring / -maxFile)・モジュール限定(-module)・手動録画(-recordmode Manual)・監視モード(-monitor)で録る範囲を構成し、リングバッファとモジュール限定は肥大化を、モジュール限定と手動録画はオーバーヘッドを軽減します。いつ起きるか分からない長期稼働の不具合にはリングバッファを、起動時や特定の起動でだけ出る断続的な不具合には監視モードを勧めます。再生ではトレース内の位置を単位に移動し、TTD.Eventsで例外の位置を確認し、TTD.Memoryとアクセスブレークポイント(ba)からの逆方向実行(g-)でデータ破壊を書いた命令まで遡り、TTD.Callsでハンドルリークの取得・解放やハングの待機呼び出しを確認できますが、TTD.Callsで正しい引数と戻り値を読むにはprivate symbolsを用意することを勧めます。64ビットのSOS拡張を組み合わせれば.NETアプリのマネージド状態も読めます。トレースはメモリ内容を含むため情報漏えいの原因になり得て、保護されたプロセス(PPL)は録画できず、Application Verifierを有効にしたままの録画は再生が遅くなるので勧めません。採取コストの低いダンプ解析を先に行い、経路が要ると分かった案件にTTDを出す順序を勧めます。
flowchart LR
accTitle: Time Travel Debuggingの知識マップ
accDescr: Time Travel Debugging(TTD)がWinDbgを前提とし、管理者権限を要する録画で命令実行を.runトレースに記録し、TTD.exeがリングバッファ・モジュール限定・手動録画・監視モードで録画範囲を構成すること、録画のオーバーヘッドとトレースの肥大化をそれらが軽減すること、再生ではTTD.Events・TTD.Calls・TTD.Memoryとアクセスブレークポイント・逆方向実行で例外やデータ破壊・ハンドルリーク・ハングを確認できること、トレースが情報漏えいの原因になり得ること、PPLと両立しないこと、Application Verifier併用が非推奨なこと、ダンプ解析を先に行うべきことを示した図
time_travel_debugging["Time Travel Debugging(TTD)"]
windbg["WinDbg"]
ttd_recording["TTDの録画"]
admin_rights["管理者権限"]
ttd_trace_file["TTDトレースファイル(.run)"]
ttd_index_file["TTDインデックスファイル(.idx)"]
ttd_exe["TTD.exe(コマンドライン録画ツール)"]
ttd_position["TTDの位置(position)"]
ttd_recording_overhead["TTD録画のオーバーヘッド"]
ttd_trace_growth["TTDトレースの肥大化"]
ttd_ring_buffer["TTDのリングバッファ録画(-ring / -maxFile)"]
ttd_module_filter["TTDのモジュール限定録画(-module)"]
ttd_manual_record_mode["TTDの手動録画(-recordmode Manual)"]
ttd_monitor_mode["TTDの監視モード(-monitor)"]
long_run_crash_investigation["長時間運転後クラッシュの切り分け"]
intermittent_failure["断続的・再現しない不具合"]
exception["例外(exception)"]
ttd_events_query["TTD.Events(イベント一覧)"]
data_corruption["データ破壊(不正な値の書き込み)"]
ttd_memory_query["TTD.Memoryクエリ"]
reverse_execution["逆方向実行(g- / p- / t-)"]
break_on_access["アクセスブレークポイント(ba)"]
private_symbols["private symbols"]
ttd_calls_query["TTD.Callsクエリ"]
handle_leak["ハンドルリーク"]
application_hang["アプリケーションのハング(応答なし)"]
information_leakage["情報漏えい"]
protected_process_light["保護されたプロセス(PPL)"]
application_verifier["Application Verifier"]
dump_analysis["ダンプ解析"]
sos_extension["SOS拡張"]
time_travel_debugging -->|"前提とする"| windbg
time_travel_debugging -->|"利用する"| ttd_recording
ttd_recording -->|"前提とする"| admin_rights
ttd_recording -->|"に保存される"| ttd_trace_file
windbg -->|"利用する"| ttd_index_file
ttd_exe -->|"実装を担う"| ttd_recording
time_travel_debugging -->|"利用する"| ttd_position
ttd_recording -->|"原因になり得る"| ttd_recording_overhead
ttd_recording -->|"原因になり得る"| ttd_trace_growth
ttd_exe -->|"で構成できる"| ttd_ring_buffer
ttd_exe -->|"で構成できる"| ttd_module_filter
ttd_exe -->|"で構成できる"| ttd_manual_record_mode
ttd_exe -->|"で構成できる"| ttd_monitor_mode
ttd_ring_buffer -->|"軽減する"| ttd_trace_growth
ttd_module_filter -->|"軽減する"| ttd_recording_overhead
ttd_module_filter -->|"軽減する"| ttd_trace_growth
ttd_manual_record_mode -->|"軽減する"| ttd_recording_overhead
ttd_ring_buffer -.->|"推奨される対応"| long_run_crash_investigation
ttd_monitor_mode -.->|"推奨される対応"| intermittent_failure
time_travel_debugging -.->|"推奨される対応"| intermittent_failure
exception -->|"で確認できる"| ttd_events_query
data_corruption -->|"で確認できる"| ttd_memory_query
data_corruption -.->|"で確認できる"| reverse_execution
break_on_access -->|"より先に行うべき"| reverse_execution
private_symbols -->|"推奨される対応"| ttd_calls_query
handle_leak -.->|"で確認できる"| ttd_calls_query
application_hang -.->|"で確認できる"| ttd_calls_query
ttd_trace_file -.->|"原因になり得る"| information_leakage
ttd_recording -->|"両立しない"| protected_process_light
application_verifier -->|"用いるのは非推奨"| ttd_recording
dump_analysis -->|"より先に行うべき"| time_travel_debugging
time_travel_debugging -.->|"利用する"| sos_extension
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全32件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. ダンプでは足りない場面
クラッシュダンプは、プロセスが落ちた(または止めた)瞬間のメモリとレジスターの写しです。SOS解析記事で書いたとおり、!clrstack でどこで落ちたか、!dumpheap -stat で何がヒープを食っているかは読めます。読めないのは、その状態に至るまでに何が起きたかです。
flowchart TB
accTitle: ダンプが写すものとTTDが写すもの
accDescr: クラッシュダンプは落ちた瞬間の状態だけを写し、それ以前の経路は写らない。TTDは録画開始から終了までの命令実行を丸ごと残すので、状態に加えて経路が残る
dump["クラッシュダンプ:落ちた瞬間の状態"] --> q1["なぜその値になったかは写らない"]
ttd["TTDトレース:録画区間の命令実行"] --> q2["値が書き換わった位置まで戻れる"]
q1 -.-> gap["この隙間が長期稼働の調査を長引かせる"]
図1: ダンプは状態、TTDは経路。長期稼働の不具合で欲しいのは、たいてい後者です。
典型的なのは次のような案件です。
- ヒープ上の構造体の1フィールドがあり得ない値になっている。ダンプには壊れた値は写っていますが、誰がいつ書いたかは残っていません。
- 例外の発生位置は分かるが、渡された引数がなぜ不正なのか分からない。呼び出し元をさらに遡ると、途中で値を作った関数はもうスタックにいません。
- ハンドルやメモリが1か月かけて増える。ダンプ1枚では「増えている」までしか言えず、増やした呼び出し経路は写りません。
flowchart TB
accTitle: 長期稼働の典型案件でダンプに写らないもの
accDescr: 壊れた値は写るが書いた主体は写らず、例外の位置は写るが引数を作った関数はスタックにおらず、資源の増加は写るが増やした経路は写らない、という3種類の案件の共通点
c1["壊れたフィールド"] --> m1["誰がいつ書いたかは無い"]
c2["不正な引数での例外"] --> m2["値を作った関数はもうスタックに無い"]
c3["1か月かけて増える資源"] --> m3["増やした呼び出し経路は無い"]
m1 --> same["共通点:状態はあるが経路が無い"]
m2 --> same
m3 --> same
図2: 3つの案件はどれも「状態はあるが経路が無い」。写真を増やしても経路は埋まらない。
Microsoftのドキュメントは、調査手段の長所と短所を次のように整理しています。1
| 手段 | 長所 | 短所 |
|---|---|---|
| ライブデバッグ | 対話的で、実行の流れが見え、状態を変えられる | 利用者の作業を止める。再現を繰り返す手間。本番では使えないことが多い。失敗地点から原因へ遡るのが難しい |
| ダンプ | 事前のコード変更が不要。侵襲性が低く、トリガーで採れる。使わなければオーバーヘッドはほぼゼロ | 連続スナップショットでも「時間経過」の見え方は粗い |
| テレメトリ・ログ | 軽量。業務シナリオと結びつく | 想定外のコードパスにはログが無い。データの深さが足りず、コードに静的に埋め込まれる |
| TTD | 複雑なバグに強い。事前のコード変更が不要。オフラインで何度でも再生でき、すべてを記録する | 録画時のオーバーヘッドが大きい。必要以上のデータを集めることがある。ファイルが大きくなる |
もうひとつ、TTDの公式ウォークスルーが指摘している重要な性質があります。失敗地点でデバッガーが止まったとき、そこは本当の原因から数ステップ進んだエラー処理コードの中であることが多い。5 ダンプは必ずこの「数ステップ後」の位置で撮られます。TTDなら、そこから1命令ずつ戻れます。
flowchart TB
accTitle: 失敗地点と本当の原因のずれ
accDescr: ダンプが撮られる失敗地点は本当の原因から数ステップ進んだエラー処理の中であることが多く、TTDではその地点から命令単位で巻き戻して原因へ遡れる
cause["本当の原因(値を壊した命令)"] --> steps["数ステップ進む"]
steps --> fail["失敗地点(例外・エラー処理)"]
fail -->|"ダンプ"| photo["ここで固定される"]
fail -->|"TTD"| back["p- / t- / g- で戻る"]
back --> cause
図3: ダンプは失敗地点に固定される。TTDは失敗地点から原因まで歩いて戻れる。
3. TTDの仕組みと代償
3.1 何を録っているのか
TTDは、対象プロセスに録画エンジンを注入し、実行された命令を命令単位で記録します。公式ドキュメントの表現では「完全な命令レベルのトレースを平均1命令あたり1バイト未満に符号化する」もので、実際には1命令あたり1ビットから1バイトの範囲に収まります。実行する関数の種類が少なく扱うデータも少ないプログラムほど小さく、逆なら大きくなります。24
録画の成果物は2つです。1
| ファイル | 役割 | 大きさの目安 |
|---|---|---|
.run |
トレース本体。録画中の命令実行を保存する | 活動中は毎秒5〜50MB成長。アイドル中は成長しない4 |
.idx |
インデックス。WinDbgが効率よく再生・メモリ照会するための補助データ。録画停止時に作られ、WinDbgが .run を開いたときにも自動生成される |
トレースの1〜2倍4 |
flowchart TB
accTitle: TTDの録画から再生までの流れ
accDescr: 対象プロセスに録画エンジンが注入されて命令実行が.runファイルに記録され、WinDbgが.runを開くとインデックス.idxを作り、位置・イベント・クエリで前後に再生する
proc["対象プロセス"] --> inj["録画エンジンの注入(TTDRecordCPU)"]
inj --> run[".run(命令実行の記録)"]
run --> open["WinDbgで開く"]
open --> idx[".idx(インデックス)を生成"]
idx --> play["位置・イベント・クエリで前後に再生"]
図4: 録るのはTTD.exeまたはWinDbg、読むのはWinDbg。共有するのは .run だけで足りる。
.run の中の時刻は「位置(position)」で表します。12:0 や 1A0:12F のように16進数2つをコロンで区切った形で、前半がシーケンシング番号(シーケンシングイベントに対応)、後半がそのイベントからのおおよその命令数です。6 FFFFFFFFFFFFFFFE:0 はトレースの末尾を意味します。7 位置は第6章で主役になります。
flowchart TB
accTitle: トレース内の位置の表し方
accDescr: 位置は16進数のシーケンシング番号とステップ数をコロンで区切った形で、先頭が0付近、末尾はFFFFFFFFFFFFFFFE:0で表され、パーセント指定でおおよその位置へも移動できる
pos["位置 xx:yy(16進数)"] --> seq["xx:シーケンシング番号"]
pos --> step["yy:そのイベントからの命令数"]
seq --> tail["末尾は FFFFFFFFFFFFFFFE:0"]
step --> pct["!tt 50 のようなパーセント指定も可"]
図5: 位置は「イベント番号:命令数」。実時刻ではないが、第6章のとおり実時刻へ変換できる。
3.2 代償
TTDは「侵襲的な技術」だと公式ドキュメント自身が書いています。2 代償を数字で並べます。
| 項目 | 内容 |
|---|---|
| 速度 | 録画中の対象プロセスは5〜20倍以上遅くなる(アプリと録画オプションによる)。UIで気づかない場合もあるが、ファイルを開くダイアログのような重い処理では体感できる23 |
| ファイル成長 | 活動中は毎秒5〜50MB。数分の録画で数GBになり得る。上限は設定されていない4 |
| ディスク切れ | 録画中にディスクが尽きると、TTDは最後のページを書いてから書けるようになるまで実質的に待つ。WinDbgは録画ダイアログを出したまま、エラーも警告も出さない。できあがるのは不完全なトレース4 |
| メモリ | 録画は対象プロセスのメモリに仮想CPU分のオーバーヘッドを載せる(既定でx64/ARM64は55、x86は32)。メモリ不足のときだけ -numVCpu で減らす2 |
| 外れない | 一度アタッチすると、TTDは自分では外れない。録画を終えたらアプリを閉じるかプロセスを終了する。システムに不可欠なプロセスならOSの再起動が要る2 |
flowchart TB
accTitle: 録画の代償と、それが長期稼働に与える影響
accDescr: 録画中の速度低下、ファイルの成長、ディスク切れ時の静かな待ち、アタッチ後に外れないという4つの代償が、長期稼働アプリに無条件で貼り付けられない理由になる
cost["TTDの録画"] --> slow["5〜20倍の速度低下"]
cost --> grow["毎秒5〜50MBの成長・上限なし"]
cost --> disk["ディスク切れは静かに待つ"]
cost --> stuck["アタッチ後は外れない"]
slow --> no["無条件の常時録画は不可"]
grow --> no
disk --> no
stuck --> no
図6: 4つの代償のどれか1つでも、常時録画を成立させない。だから第5章の「録る範囲の設計」が要る。
3.3 できないこと
- ユーザーモード専用。録れるのはプロセスのユーザーモードの実行だけで、ドライバーなどカーネルモードで実行されるコードはデバッグできません。3
- 保護されたプロセス。Protected Process Light(PPL)などWindowsの保護プロセスには、TTDが自分を注入できません。3
- 再生は読み取り専用。過去には戻れますが、歴史は変えられません。メモリを読むコマンドは使えますが、書き換えるコマンドは使えません。3
- アンチウイルス・メモリ監視系ソフトとの非互換。TTDがプロセスにフックする方式のため、システムメモリ呼び出しを追跡・シャドウするソフトと衝突します。録画時に権限不足のようなエラーが出るなら、一時的に無効化して切り分けます。Electronフレームワークも既知の衝突例で、録れたとしても対象プロセスのデッドロックやクラッシュがあり得ます。3
- UWPアプリは起動して録画できない(すでに動いているUWPアプリへのアタッチは可能)。別セッション・別セキュリティコンテキストで動く「通常でないプロセス」も現時点では対象外です。8
flowchart TB
accTitle: TTDで録れないもの・できないこと
accDescr: カーネルモードのコード、保護されたプロセス、UWPアプリの起動録画、別セッションや別セキュリティコンテキストのプロセスは録れず、再生中はメモリを書き換えられず、アンチウイルスやElectronとは衝突し得る
no["TTDの制約"] --> g1["録れないもの"]
no --> g2["制約・衝突"]
g1 --> k["カーネルモードのコード(ドライバー等)"]
k --> ppl["保護されたプロセス(PPL)"]
ppl --> uwp["UWPの起動録画(アタッチは可)"]
uwp --> sess["別セッション・別コンテキスト"]
g2 --> ro["再生は読み取り専用"]
ro --> av["アンチウイルス・Electronと衝突し得る"]
図7: 制約は「録れない」と「衝突する」の2種類。後者は環境ごとに切り分けが要る。
最後の項目は、サービスを録りたい人には引っかかります。TTD.exeのドキュメントは -attach を「サービスや長時間稼働するアプリの調査」向け、-monitor を「プログラムやサービスが起動するたびに録画」する用途として説明しており2、トラブルシューティングのページの記述と単純には噛み合いません。実務では、本番と同じ構成で ping.exe や cmd.exe を録れることを確認し、次に対象プロセスで試すという順で、環境ごとに確かめるのが安全です。8
4. 録画する ── WinDbg UIとTTD.exe
録画の入口は2つあります。WinDbgのUIから録るか、コマンドラインの TTD.exe で録るかです。
4.1 WinDbg UIから録る
WinDbgを管理者として実行し(TTDには昇格が必須です1)、File > Start debugging > Launch executable (advanced) で実行ファイルを指定して Record with Time Travel Debugging にチェックを入れます。Configure and Record を選ぶと、トレースファイルの保存先と、Record subset of execution(録画するモジュールを notepad.exe,kernelbase.dll のようにカンマ区切りで限定する)を設定できます。すでに動いているプロセスなら File > Start debugging > Attach to process で同様に Record Process with Time Travel Debugging をチェックします。9
録画中は「Stop and Debug」と「Cancel」のボタンを持つ小さなダイアログが出ます。アプリが終了(またはクラッシュ)するとトレースが閉じられ、WinDbgが自動で開いてインデックスを作ります。5
flowchart TB
accTitle: WinDbg UIからの録画の流れ
accDescr: 管理者として起動したWinDbgでLaunch executable (advanced)またはAttach to processを選び、Record with Time Travel Debuggingにチェックを入れ、Configure and Recordで保存先とモジュール限定を設定し、録画中ダイアログを経てアプリ終了時にトレースが閉じられ自動でインデックスが作られる
adm["WinDbgを管理者として起動"] --> pick["Launch executable(advanced)/ Attach to process"]
pick --> chk["Record with Time Travel Debugging にチェック"]
chk --> cfg["Configure and Record:保存先・モジュール限定"]
cfg --> rec["録画中ダイアログ(Stop and Debug)"]
rec --> fin["アプリ終了でトレースを閉じ、自動でインデックス"]
図8: UIからの録画は5手順。「管理者として起動」を飛ばすと最初のダイアログで止まる。
4.2 TTD.exeで録る
WinDbgを入れられないPCで録る、録画を自動化する、といった場面では TTD.exe 単体を使います。https://aka.ms/ttd/download からApp Installer経由で入り、インストール後は ttd.exe -help で確認します。オフライン環境向けには、MSIXバンドルを手で展開してバイナリだけを取り出す手順(PowerShellスクリプト付き)も公式に用意されています。2 録画には管理者権限が要り、通常は管理者コマンドプロンプトから実行します。2
録画のモードは3つです。2
flowchart TB
accTitle: TTD.exeの3つの録画モード
accDescr: launchは引数を渡して新しいプロセスを起動して録画するが昇格した権限で走る。attachは動いているプロセスにPIDで付く。monitorは指定プログラムが起動するたびに録画し、起動が正常な権限で行われる
m["TTD.exe の録画モード"] --> l["-launch:起動して録る"]
m --> a["-attach:動いているPIDに付く"]
m --> mo["-monitor:起動のたびに録る"]
l -.-> lp["管理者権限で起動される"]
a -.-> ap["通常の権限のまま"]
mo -.-> mp["通常の起動経路・自動化向き"]
図9: 引数を渡せるのは -launch だけだが、昇格した権限で走る。本番に近い挙動で録るなら -attach か -monitor。
:: 起動して録る(既定モード。-launch は省略可)
TTD.exe -out C:\traces MyApp.exe --config prod.json
:: 動いているプロセスに付く(出力先ディレクトリは先に作っておく)
TTD.exe -attach 21440 -out C:\traces\MyApp.run
:: 起動のたびに録る(Ctrl+C で監視終了。-out にフルパスが必須)
TTD.exe -out C:\traces\ -monitor MyApp.exe
-launchは引数を渡せる唯一のモードですが、プログラムはTTD.exeと同じ(管理者の)権限で起動されます。権限の違いで挙動が変わるアプリでは、-attachか-monitorで通常の権限のまま録ります。2-attachは出力先ディレクトリが存在している必要があります。ファイル名を指定する場合、その名前のファイルが存在していてはいけません。2-monitorはプロセス起動監視ドライバーを入れ、指定したプログラム(複数指定可)が起動するたびに録画します。再起動まで有効で、Ctrl+Cで止めます。起動を自分で組み立てなくてよく、対象は通常の権限で動き、スクリプトによる自動化に向くのが利点です。-cmdLineFilter "文字列"を付けると、コマンドラインにその文字列を含む起動だけを録ります。2-childrenを付けると子プロセスも録りますが、プロセスごとに別の.runになり、WinDbgは一度に1つしか開けません。2
録画中は「Tracing Off」(録画を止めてアプリは続行)と「Exit App」(アプリを閉じて録画終了)の2ボタンの小さなUIが出ます。自動化では -noUI で消し、EULAは -accepteula で受諾します。2 録画のログは .run と同じ場所の .out ファイルに残り、録画開始・終了の実時刻、録画セッションの長さ(simulation time)、起動かアタッチか、OSバージョンが読めます。録画がうまくいかないとき、.out にしか出ないエラーメッセージがあります。2
5. 長期稼働向けの録画設計
ここが本記事の本丸です。第3章の代償を前提にすると、「いつ起きるか分からない不具合を、何日も動くプロセスで捕まえる」には録画範囲の設計が要ります。TTD.exeが用意している手段は4つで、症状の性質で選びます。
flowchart TB
accTitle: 長期稼働の症状に応じた録画範囲の選び方
accDescr: いつ起きるか分からないならリングバッファで最後の部分だけ残し、疑わしいモジュールが分かっているならそのモジュールだけ録り、アプリを改修できるなら手動録画APIで区間を指定し、起動時や特定の起動でだけ出るなら監視モードで起動のたびに録る
q{"症状の性質は?"} -->|"いつ起きるか不明"| ring["-ring / -maxFile:最後だけ残す"]
q -->|"起動時・特定の起動だけ"| mon["-monitor:起動のたびに録る"]
ring -->|"疑わしいモジュールが明確"| mod["-module を重ねる"]
ring -->|"アプリを改修できる"| man["-recordmode Manual で区間指定"]
図10: 4つは排他ではない。-ring と -module を重ねるのが、長期稼働では最も現実的な組み合わせになる。
5.1 リングバッファ ── 最後のNMBだけ残す
-ring を付けると、トレースは -maxFile で指定した大きさのリングバッファに書かれ、ファイルはその上限を超えて成長しません。残るのは、その大きさに収まる録画の最後の部分だけです。2 -maxFile の単位はMBで、リングバッファモードの既定は2,048MB、最小1MB、最大32,768MBです(32ビットプロセスのインメモリリングの既定は256MB)。2
:: 動いている監視アプリに付き、直近4GB分の実行だけを残す
TTD.exe -accepteula -noUI -attach 21440 -ring -maxFile 4096 -out C:\traces\MyApp.run
:: 症状を検知したら、録画を止める(アプリは走り続ける)
TTD.exe -stop 21440
-stop はプロセス名・PID・all を受け取り、その録画を止めます。-wait <秒> はシステム上のすべての録画セッションが終わるまで待ち(-1 で無期限)、自動化スクリプトで「止めてからファイルを回収する」順序を作るのに使います。2
flowchart TB
accTitle: リングバッファ録画のタイムライン
accDescr: アタッチして録画を始め、古い部分はリングバッファから押し出されていき、症状が出た時点でstopすると、直近のmaxFile分だけがトレースとして残る
s["-attach -ring で録画開始"] --> old["古い区間は押し出される"]
old --> sym["症状が出る"]
sym --> stop["-stop で録画停止"]
stop --> keep["直近の maxFile 分だけ残る"]
keep -.-> note["症状の直前がバッファに収まる大きさに"]
図11: リングバッファは「症状の直前」を残す装置。-maxFile は「症状に気づいて止めるまでの時間 × 毎秒の成長量」から逆算する。
設計上の要点は2つです。
- バッファは「気づいて止めるまで」を吸収できる大きさにする。アクティブなプロセスで毎秒5〜50MB成長する4ので、4GBのリングは高負荷時で1〜2分、低負荷時で10分強に相当します。症状の検知(ログの特定行、カウンターの閾値、監視の異常通知)から
-stopまでのラグを、この時間に収める仕組みが必要です。 - 止めても外れない。
-stopで録画は止まりますが、TTDは対象プロセスから自分では外れません。2 録画を完全にやめるにはプロセスの終了が必要なので、「トレースを回収したら次のメンテナンス時刻に再起動する」ところまで運用に含めておきます。
sequenceDiagram
accTitle: 監視と連動したリングバッファ録画の停止
accDescr: 監視側がログやカウンターで症状を検知したらTTD.exeのstopを呼び、確定した.runファイルを回収し、その後のメンテナンス時刻にプロセスを再起動してTTDを外す
participant W as 監視(ログ・カウンター)
participant T as TTD.exe
participant P as 対象プロセス
W->>W: 症状を検知
W->>T: -stop PID
T->>P: 録画を停止(プロセスは継続)
T-->>W: .run が確定
W->>W: .run を回収・暗号化して保管
W->>P: 次回メンテナンス時刻に再起動
図12: 検知と -stop のラグがバッファに収まって初めて、症状の直前がトレースに残る。再起動までが運用の一部。
5.2 モジュール限定 ── 自社コードが動いている間だけ録る
-module <モジュール名> は、指定したモジュール(実行ファイル自身でも読み込まれるDLLでもよく、複数指定可)と、そのモジュールが呼び出すコードだけを録画します。対象プロセスは指定モジュールのコードが実行されるまでフルスピードで走り、モジュールに入ると録画が始まり、モジュールから出ると録画が止まってまたフルスピードに戻ります。録画のオン・オフは高コストなので、指定モジュールがプロセス内の他モジュールを呼び出している間は録画をオンのまま保ちます。2
:: 自社の計測ロジックDLLが動いている間だけ録る(リングと併用)
TTD.exe -accepteula -noUI -attach 21440 -module MeasureCore.dll -ring -maxFile 2048 -out C:\traces\
この方式のトレースは、録画が止まっていた区間を「次の命令は録画再開後の最初の命令」として飛ばすだけで、デバッグの仕方は全体を録ったトレースと変わりません。2 長期稼働のアプリでは、UIのアイドルループやフレームワークの内部処理が実行命令の大半を占めがちなので、自社モジュールに限定するだけでオーバーヘッドとファイルサイズは大きく減ります。
flowchart TB
accTitle: モジュール限定録画の動き
accDescr: 対象プロセスは指定モジュール外ではフルスピードで走り、指定モジュールのコードに入ると録画が始まり、そのモジュールが呼ぶ他モジュールの間も録画が続き、モジュールから出ると録画が止まる
out1["指定モジュール外:フルスピード"] --> in1["指定モジュールに入る:録画開始"]
in1 --> callee["呼び出し先の他モジュール:録画継続"]
callee --> out2["指定モジュールから出る:録画停止"]
out2 --> out1
図13: 自社DLLが呼ぶWin32 APIやランタイムの中まで録れるので、「自社コードがOSに何を渡したか」を追うには十分。
5.3 手動録画 ── アプリ側から区間を指定する
-recordmode Manual を指定すると、TTDが注入された後もプロセスはフルスピードで走り、プログラムがTTDのインプロセス録画APIを呼んだときだけ録画します(既定の Automatic は注入直後から録画)。2 APIのドキュメントとサンプルはGitHubのWinDbg-Samplesリポジトリにあります。10
アプリを改修できるなら、これが最も無駄の少ない方式です。「通信のリトライが3回連続で失敗した」「キューの滞留が閾値を超えた」のように、アプリ自身が異常の予兆を知っている場面で録画を始め、正常に戻ったら止める、という組み込みができます。Job Objectの記事で扱ったような監視プロセス構成なら、監視側の判断でこの区間を決めることもできます。
5.4 監視モード ── 起動のたびに録る
起動直後や特定の起動でだけ出る不具合には -monitor が向きます。公式の使い分け表でも、監視モードは「断続的な問題や起動時の問題を捕まえる」用途とされています。2 同じプログラムを何度も録る場合は、既定の連番ファイル名(MyApp01.run、MyApp02.run……)が既存ファイルの走査で非効率になるので、-timestampFilename でタイムスタンプ付きの名前にします。同時に走る録画の数は -maxConcurrentRecordings で抑えられます。2
flowchart TB
accTitle: 監視モードの動き
accDescr: monitorオプションはプロセス起動監視ドライバーを入れ、指定プログラムが起動するたびにコマンドラインフィルターで対象を絞って録画し、起動ごとに別のトレースファイルを作り、Ctrl+Cまたは再起動まで続く
drv["起動監視ドライバーを導入"] --> launch["指定プログラムの起動を検知"]
launch --> filt{"-cmdLineFilter に一致?"}
filt -->|"はい"| rec["その起動を録画(起動ごとに別ファイル)"]
filt -->|"いいえ"| skip["録画しない"]
rec --> next["次の起動を待つ(Ctrl+C か再起動まで)"]
skip --> next
図14: 監視モードは「起動を待ち伏せる」方式。起動のたびに出る不具合と、起動条件で絞れる不具合に向く。
5.5 ディスクの置き場
長期稼働の録画では、トレースの置き場を専用ボリュームにし、.run の成長を監視に入れておきます。第3.2節のとおり、ディスクが尽きても録画は静かに待つだけで、エラーは出ません。公式の回避策も「エクスプローラーで空き容量を見る」「.run が定期的に成長しているかを見る」という原始的なものです。4 空き容量の監視をしていないと、いちばん欲しい瞬間が書かれていない不完全なトレースを手にすることになります。
flowchart TB
accTitle: ディスク切れが不完全なトレースを生む経路
accDescr: 録画中にディスクが尽きるとTTDは最後のページを書いて静かに待ち、エラーも警告も出ないため、その後に起きた症状は記録されず、開けるが肝心の部分が無い不完全なトレースになる。専用ボリュームと.runの成長監視で防ぐ
full["ディスクが尽きる"] --> wait["最後のページを書いて静かに待つ"]
wait --> none["エラーも警告も出ない"]
none --> sym["その後に症状が出る"]
sym --> inc["症状の無い不完全なトレース"]
guard["専用ボリューム+.run成長の監視"] -.->|"防ぐ"| full
図15: 「開けるのに肝心の部分が無い」トレースは、ディスク監視の欠落から生まれる。
6. 再生する ── 位置・イベント・巻き戻し
6.1 開く
.run をWinDbgで開くと、インデックスが無ければ自動で !index が走り、keyframe(インデックス用に自動生成されるトレース内の位置。大きいトレースほど多い)を数えながら .idx を作ります。大きいトレースほど時間がかかります。5 インデックスの状態は !index -status で確認でき、「Index file loaded」以外なら !index -force で作り直します。それでも駄目なら、デバッガーを閉じて .idx を削除し、.run を開き直します。インデックスの再作成は .run を変更しないので、データは失われません。8
注意点として、TTD 1.11.611で大きなトレースのインデックス作成が改善された際にインデックス形式が変わり、既存のトレースは再インデックスが必要になっています。11 古い .idx を持ち回っている場合は、ここでつまずきます。
flowchart TB
accTitle: インデックスの確認と作り直し
accDescr: トレースを開いたら!index -statusで状態を確認し、Index file loaded以外なら!index -forceで作り直し、それでも駄目ならデバッガーを閉じて.idxを削除して.runを開き直す。作り直しは.runを変更しない
open["トレースを開く"] --> st["!index -status"]
st -->|"Index file loaded"| ok["解析へ進む"]
st -->|"それ以外"| force["!index -force で作り直す"]
force -->|"失敗"| del["閉じて .idx を削除し .run を開き直す"]
del --> ok
force -->|"成功"| ok
図16: インデックスの作り直しは .run に触らない。迷ったら削除して開き直せばよい。
6.2 位置で移動する
!tt に位置を渡すと、その時点へ移動します。6
!tt 0 ; トレースの先頭
!tt 50 ; おおよそ50%の位置
!tt 100 ; トレースの末尾
!tt 1A0:12F ; 位置 1A0:12F へ
位置は シーケンシング番号:ステップ数 の16進数2組です。6 Position オブジェクトには Percent(トレース内の割合)、Sequence、Steps のプロパティと、その位置へ移動する SeekTo()、おおよその実時刻(UTC)を返す ToSystemTime() があります。7 長期稼働の調査ではこの ToSystemTime() が効きます。アプリのログに残った時刻と、トレース内の位置を突き合わせられるからです。TTD.Calls の結果にも SystemTimeStart / SystemTimeEnd が出ます。12
flowchart TB
accTitle: 位置とログ時刻の突き合わせ
accDescr: アプリのログにある時刻から、Positionオブジェクトのおおよその実時刻をたどってトレース内の位置を特定し、その位置へSeekToで移動して周辺の実行を読む
log["アプリのログ:異常の時刻"] --> match["ToSystemTime の実時刻が近い位置を探す"]
match --> seek["SeekTo でその位置へ"]
seek --> read["周辺の呼び出しと値を読む"]
図17: 「ログのこの行の直前に何をしていたか」を、位置と実時刻の対応で引ける。
位置は共有にも使えます。トレースを同僚に渡すときに !tt x:y の位置を添えれば、相手は同じ時点から見始められます。バグ票に位置の範囲を書いておく運用も公式に推奨されています。2
6.3 巻き戻す
通常のステップ実行コマンドの末尾に - を付けると、時間を逆向きに進みます。13
| コマンド | 意味 | リボンのボタン |
|---|---|---|
p- |
1命令(またはソース1行)戻る。関数呼び出しは1ステップ扱い | Step Over Back |
t- |
1命令(またはソース1行)戻る。関数呼び出しの中も辿る | Step Into Back |
g- |
逆方向に実行する。ブレークポイントに当たるか、イベントで止まるか、トレースの先頭で止まる | Go Back |
g- を止めるイベントは、順方向の g を止めるものと同じです。13 つまり ba(アクセスブレークポイント)や bp を仕掛けて g- を打てば、「その条件が最後に成立した位置」まで一気に戻れます。これが第7章の基本動作です。
flowchart TB
accTitle: 逆方向コマンドの使い分け
accDescr: p-は関数呼び出しをまたいで1ステップ戻り、t-は関数の中まで1命令ずつ戻り、g-はブレークポイントかイベントかトレース先頭まで一気に戻る。順方向のgを止める条件はg-も止める
cur["現在の位置"] -->|"p-"| over["呼び出しをまたいで1つ戻る"]
cur -->|"t-"| into["関数の中まで1命令戻る"]
cur -->|"g-"| run["次に止まる条件まで一気に戻る"]
run -.-> stop["ba / bp / イベント / トレース先頭"]
図18: 近くを丁寧に見るなら t-、遠くの原因へ飛ぶなら ba + g-。
6.4 イベントから入る
どこから読み始めるか迷ったら、まずイベント一覧です。@$curprocess.TTD.Events には、スレッドの作成・終了、モジュールのロード・アンロード、例外がイベントとして並んでいます。14
dx -g @$curprocess.TTD.Events
dx @$curprocess.TTD.Events.Where(t => t.Type == "Exception").Select(e => e.Exception)
例外イベントには、位置、種類(Software / Hardware)、例外コード、発生時のプログラムカウンターが入り、出力の [Time Travel] リンクを押すとその位置へ移動します。15 公式ウォークスルーはこの流れで、アクセス違反(0xc0000005)の位置へ飛び、スタックポインターとベースポインターが食い違っていることからスタック破壊を疑い、t- で3命令戻って値を確かめています。5 WinDbgの Timelines ウィンドウは、例外・ブレークポイント・メモリアクセス・関数呼び出しをタイムラインとして可視化し、例外をダブルクリックすると同じ SeekTo() が発行されます。16
6.5 スレッドと位置
!positions は、現在の位置におけるアクティブなスレッドすべてと、それぞれのトレース内位置を表示します。17 ここに1つ落とし穴があります。~<番号>s でスレッドを切り替えてもトレース内の位置は動きません。デバッガーがメモリを読むときに使う位置は変わらないので、別スレッドの「その時点」のメモリを見たいなら、!positions の出力にある位置のリンクか !tt x:y で移動します。13
flowchart TB
accTitle: 再生の基本動線
accDescr: トレースを開いてインデックスを作り、イベント一覧から例外の位置へ移動し、逆方向のステップで原因へ遡り、必要ならpositionsで他スレッドの位置へ移る
open["トレースを開く・インデックス"] --> ev["TTD.Events で例外を探す"]
ev --> seek["[Time Travel] で位置へ移動"]
seek --> back["t- / p- / g- で遡る"]
back --> th["!positions で他スレッドの位置を確認"]
th -.-> caution["~s ではトレース位置は動かない"]
図19: 「イベントで入り、逆方向に歩く」が再生の基本動線。スレッド切り替えは位置移動ではない。
7. クエリで「いつ」を探す ── TTD.Calls と TTD.Memory
再生の本当の強みは、トレース全体を問い合わせできることです。デバッガーのデータモデル(dx コマンド)にTTDのオブジェクトが載っていて、LINQ風に絞り込み・並べ替え・集計ができます。18
7.1 TTD.Calls ── 関数呼び出しを検索する
@$cursession.TTD.Calls("module!symbol") は、指定した関数の呼び出しをトレース全体から集めます。ワイルドカードが使え、各呼び出しには開始・終了位置(TimeStart / TimeEnd)、スレッドID(再利用されない UniqueThreadId 付き)、引数(Parameters[])、戻り値(ReturnValue)、戻り先アドレス(ReturnAddress)が入ります。12
公式ドキュメントの例は GetLastError です。戻り値が0以外の呼び出しをエラーコードごとに集計すれば、トレース中に何のエラーが何回出たかが一覧になります。18
dx -g @$cursession.TTD.Calls("kernelbase!GetLastError").Where(x => x.ReturnValue != 0).GroupBy(x => x.ReturnValue).Select(x => new { ErrorNumber = x.First().ReturnValue, ErrorCount = x.Count() }).OrderByDescending(p => p.ErrorCount),d
「最後に出たMessageBoxはどこから呼ばれたか」なら、OrderBy(c => c.TimeStart).Last() で最後の呼び出しを取り、その TimeStart の [Time Travel] リンクで移動します。18
flowchart TB
accTitle: TTD.Callsクエリの組み立て
accDescr: 関数名で呼び出しを集め、戻り値や引数で絞り、エラーコードなどで集計し、時刻で並べ、目的の呼び出しの位置へTime Travelリンクで移動する
calls["TTD.Calls(関数名・ワイルドカード)"] --> where["Where:戻り値・引数で絞る"]
where --> group["GroupBy:エラーコード等で集計"]
group --> order["OrderBy:時刻で並べる"]
order --> jump["TimeStart の [Time Travel] で移動"]
図20: 「どこで」ではなく「いつ・何回・どの引数で」から入るのがクエリの使い方。
シンボルとの関係を押さえておきます。TTDはPDBのシンボル情報から、関数の引数の数と型、戻り値の型、呼び出し規約を決めます。private symbolsがあれば関数名と正しい引数が出ます。public symbolsだけなら関数名と既定の引数(64ビット符号なし整数4つ)になります。シンボルがまったく無いモジュールは関数名が UnknownOrMissingSymbols になります。1218 PDBの種類と保管については「PDBとは何か」を参照してください。
flowchart TB
accTitle: シンボルの有無とTTD.Callsの結果
accDescr: private symbolsがあれば関数名と正しい引数が得られ、public symbolsだけなら関数名と既定の4つの64ビット整数引数になり、シンボルが無ければ関数名がUnknownOrMissingSymbolsになる
sym{"モジュールのシンボルは?"} -->|"private symbols"| full["関数名+正しい引数・戻り値"]
sym -->|"public symbols"| pub["関数名+既定の引数(64ビット整数×4)"]
sym -->|"無し"| unk["UnknownOrMissingSymbols"]
図21: 自社モジュールのPDBを保管しているかどうかが、クエリの実用性をそのまま決める。
Calls は計算を伴うのでトレースが大きいほど時間がかかり、CPU使用率が上がります。結果はメモリにキャッシュされ、同じ関数への2回目以降のクエリは速くなります。12 クエリが何も返さないときの原因は4つで、呼び出しの書き方(モジュール名は x コマンドで確かめる。大文字で返ってきたらそれを使う)、対象DLLがその位置ではまだロードされていない(ロード後の位置へ移動して再実行)、関数がインライン展開されている(追跡不能)、ワイルドカードが広すぎる(絞る)、です。18
7.2 TTD.Memory ── メモリアクセスを検索する
@$cursession.TTD.Memory(開始アドレス, 終了アドレス, "アクセス種別") は、指定範囲のメモリへのアクセスをトレース全体から集めます。種別は r(読み)、w(書き)、rw、e(実行)、rwe、ec(実行/変更)です。19 「この変数を最後に書いたのは誰か」は、"w" で集めて .Last() を取り、その位置へ移動すれば答えが出ます。5
dx -g @$cursession.TTD.Memory(0x00a4fca0, 0x00a4fca4, "w")
dx @$cursession.TTD.Memory(0x00a4fca0, 0x00a4fca4, "w").Last().TimeStart.SeekTo()
現在位置から前後に探すだけなら、@$curprocess.TTD.PrevMemoryAccess("w", アドレス, サイズ) / NextMemoryAccess が軽く、複数の範囲を一度に指定できます。レジスターが変わった位置は @$curthread.TTD.PrevRegisterWrite("rcx") で探せます。6
7.3 ba + g- ── 「誰が壊したか」を答えさせる
公式ウォークスルーが示す手順を、長期稼働の調査に使える形に縮めると次のようになります。5
TTD.Eventsで例外の位置へ移動するt-で戻りながら、壊れた値を持つ変数を仮定する- その変数のアドレスを
dx &変数で取る ba w4 <アドレス>で書き込みブレークポイントを置くg-で、その変数が最後に書かれた位置まで一気に戻る- そこ(または数命令前)が原因かを見る。書いた値が別の変数由来なら、その変数にまた
baを置いてg-する - 壊した命令に届くまで繰り返す
flowchart TB
accTitle: baとg-で値の出所を遡る
accDescr: 壊れた値のアドレスに書き込みブレークポイントを置いて逆方向に実行し、最後に書いた命令に止まり、その値が別の変数由来なら同じ手順を繰り返して、壊した命令に届くまで遡る
bad["壊れた値のアドレスを特定"] --> ba["ba w に書き込みブレークポイント"]
ba --> gb["g- で逆方向に実行"]
gb --> writer["最後に書いた命令で止まる"]
writer --> q{"値は別の変数由来?"}
q -->|"はい"| bad
q -->|"いいえ"| found["壊した命令=原因"]
図22: ダンプでは「壊れている」で終わる調査が、TTDでは「誰が壊したか」まで機械的に進む。
TTD 1.11.553以降には、フレームのローカル変数の値の履歴を返す @$curframe.TTD.VariableHistory() も加わりました。変数名の一覧と、各変数がどの値をどの位置範囲で持っていたかを表で見られます。11 スタック破壊のように「いつから値がおかしいか」を知りたい場面で、ba を置く前の当たりを付けるのに使えます。
8. 長期稼働の不具合に当てはめる
ここまでの道具を、冒頭に挙げた3種類の案件に当てはめます。
flowchart TB
accTitle: 長期稼働の不具合の型と、TTDでの入口
accDescr: 断続的な例外やデータ破壊はイベントからbaとg-で遡り、資源の増加はTTD.Callsで取得と解放の呼び出しを突き合わせ、応答なしや待ちの連鎖はpositionsと待機APIの呼び出しの実時刻で追う
t1["断続的な例外・データ破壊"] --> a1["TTD.Events → ba + g-"]
t2["ハンドル・メモリの増加"] --> a2["TTD.Calls で取得と解放を突き合わせ"]
t3["応答なし・待ちの連鎖"] --> a3["!positions と待機APIの実時刻"]
a2 -.-> pre["-module で自社DLLに限定して録る"]
図23: 型ごとに入口が違う。共通するのは、録画範囲を先に絞ってから読み始めること。
型1: 断続的な例外・データ破壊。第5.1節のリングバッファで症状の直前を残し、第6.4節のイベント一覧から例外の位置へ飛び、第7.3節の ba + g- で値の出所を遡ります。ダンプ調査との違いは、「壊れている変数」を見つけた時点で調査が終わらず、そこから機械的に前へ進めることです。
型2: ハンドル・メモリの増加。「ハンドルリークで1か月後に落ちる」で解剖した種類の案件です。1か月分を録ることはできないので、第5.2節の -module で自社DLLに限定し、-ring と重ねて「増えている最中の数分」を残します。トレース上では TTD.Calls("kernelbase!CreateFileW") と TTD.Calls("kernelbase!CloseHandle") を集め、CreateFileW の戻り値(ハンドル値)と CloseHandle の第1引数(Parameters[0])を突き合わせます。CloseHandle の ReturnValue は成否を表すブール値なので、突き合わせには使えません。閉じられていないハンドル値が残ったら、その CreateFileW 呼び出しの ReturnAddress(呼び出し元)を見れば、漏らしている呼び出し元が引けます。12 ただし、リークの「有無」と「量」の観測はApplication Verifierやハンドルカウントのほうが軽く、TTDは「どの経路が漏らしているか」を確定させる段で使うのが分担として正しいです。なお、Application Verifierを有効にしたまま録画すると、メモリの使い方の関係で再生時の性能が目立って悪くなるので、録画時は無効にします。8
型3: 「応答なし」・待ちの連鎖。「「応答なし」の正体」で書いたように、ハングは「誰が誰を待っているか」の問題です。トレースはアイドル中には成長しない4ので、カーネルで待っているスレッドの待ち時間そのものは命令として写りません。写るのは待ちに入る直前の呼び出しと、戻ってきた後の命令です。!positions で各スレッドの位置を見て17、TTD.Calls("kernelbase!WaitForSingleObject") などの SystemTimeStart / SystemTimeEnd から「どのスレッドが、いつ、何を待ち始めたか」を並べると、ログの時刻と付き合わせて待ちの連鎖を組み立てられます。12 DllMainとローダーロックや条件変数のスプリアスウェイクアップのような「順序依存の不具合」は、順序そのものが録画に残るTTDと相性がよい領域です。
9. .NETアプリでのTTD
TTDの公式ドキュメントは、64ビットモードで動くSOS拡張(sos.dll)を使って、WinDbgのTTDでマネージドコードをデバッグできると述べています。1 読み込み方はSOS解析記事の第3章と同じで(.loadby sos coreclr または自動読み込み)、トレースの各位置で !clrstack や !pe が使えます。基本の動線は、TTD.Events で例外の位置へ移動してから !clrstack でマネージドのスタックを読む、という組み合わせです。
flowchart TB
accTitle: .NETアプリのTTDトレースを読む構成
accDescr: TTDトレースをWinDbgで開き、64ビットのSOS拡張を読み込んで、例外イベントの位置へ移動してから!clrstackや!peでマネージドの状態を読む。TTD.Callsはネイティブ境界の呼び出しに使う
run["TTDトレース(.run)"] --> wd["WinDbg"]
wd --> sos["SOS拡張(64ビット)"]
sos --> ev["TTD.Events で例外の位置へ"]
ev --> clr["!clrstack / !pe で読む"]
wd -.-> calls["TTD.Calls はネイティブ境界の呼び出しに"]
図24: マネージドの状態はSOSで、呼び出しの検索はネイティブ境界で。役割を分けると迷わない。
注意点を2つ。第一に、公式が保証しているのは「64ビットモードのSOS」です。x86でビルドした.NETアプリについては明記が無いので、可能なら調査対象をx64で動かしてください。第二に、TTD.Calls はPDBのシンボル情報を前提にします。12 JITコンパイルされるマネージドメソッドを名前で検索する用途は当てにせず、P/Invoke先のネイティブDLL、COM、Win32 API といったネイティブ境界の呼び出しを追う使い方が確実です。「.NETでGC待ちとメモリリークを見分ける」のようなマネージドヒープの問題は、まず dotnet-counters / dotnet-gcdump / ダンプの !gcroot で追い、TTDはネイティブ境界が絡む段で出す、という順序です。
10. ダンプ・ログ・ETW・TTDの使い分け
TTDは万能ではなく、既存の道具を置き換えるものでもありません。当社の記事で扱ってきた道具と並べます。
| 知りたいこと | まず使う道具 | TTDの出番 |
|---|---|---|
| 落ちた瞬間の状態 | クラッシュダンプ(WER LocalDumps / ProcDump) | ダンプで「壊れている」までは分かったが、誰が壊したか分からないとき |
| どのファイル・レジストリで失敗したか | Process Monitor | 失敗したAPIに渡した引数がどう作られたかまで要るとき |
| PC全体・長時間の性能 | WPR/WPA、PerfView(ETW) | 性能ではなく正しさの問題で、命令単位の順序が要るとき |
| 業務上の経過 | アプリのログ | ログの無いコードパスで起きたとき(TTDは事前のコード変更なしにすべてを記録する1) |
| 誰がその値を書いたか、呼び出しの順序 | TTD | ── |
flowchart TB
accTitle: 調査手段の選び方
accDescr: まず軽いダンプとログで状態を掴み、失敗したAPIはProcMon、性能はETWで調べ、それでも「誰がいつ何を渡したか」が要るときにTTDへ進む
start["症状"] --> light["ダンプ・ログで状態を掴む(軽い)"]
light --> api["失敗したAPIは ProcMon"]
light --> perf["性能は WPR/WPA・PerfView"]
light --> need{"誰が・いつ・何を渡したかが必要?"}
need -->|"はい"| ttd["TTD で録画範囲を設計して録る"]
need -->|"いいえ"| done["軽い道具で決着"]
図25: 軽い道具で「結果」を掴んでから、「経路」が要ると分かった案件にだけTTDを出す。
順序は「軽いものから」です。ダンプとログは採取コストがほぼゼロなので常設し、TTDは「ダンプを読んだ結果、経路が要ると分かった」案件で、第5章の設計をしてから出します。TTDのオーバーヘッドと、トレースが機密情報を含む性質を考えると、この順序を逆にする理由はありません。
11. 運用上の注意
- トレースは機密ファイルとして扱う。録画にはメモリ内容が含まれ、ファイルパス・レジストリ・メモリやファイルの中身など、個人情報・セキュリティ関連情報が入り得ます。12 顧客環境で録るなら、何が含まれ得るか(接続文字列、トークン、顧客データ)を事前に把握し、暗号化した経路と保管先、保持期間を決めます。
- 共有するのは
.runだけ。.idxは.runと同程度に大きく、WinDbgが開くときに自動生成されます。.runは圧縮がよく効きます。TTD自体の不具合を報告するときは.outも添えます。2 - バージョンをそろえる。TTDはWinDbgと一緒に更新され続けており、1.11.611ではAVX/AVX512対応プログラムの録画クラッシュ修正やインデックス形式の変更が入っています。11 録画側と再生側で古い版が混ざると、再インデックスや再録画になります。
- 異なるCPUで再生するなら
-replayCpuSupportを確認する。既定は可搬性を優先した設定で、録画したCPUと再生するCPUが違う(Intelのトレースをarm64で再生するなど)場合はMostConservativeが用意されています。逆に同等以上のCPUで再生すると分かっていれば、より小さく速い録画も選べます。2 - Windows Serverでも録れる。TTD.exeはWindows Server 2016/2019/2022/2025をサポートします。2
- 録れないときの切り分け。まず
ping.exeやcmd.exeが録れるかを試し、録れなければアンチウイルスやアプリ仮想化などの侵襲的なソフトとの衝突を疑います。8
flowchart TB
accTitle: トレースの取り扱い手順
accDescr: 録画したトレースはメモリ内容を含むため、含まれ得る情報を把握し、.runだけを圧縮して暗号化した経路で渡し、保管先と保持期間を決め、解析側では同じ版のWinDbgで開いてインデックスを作る
rec["録画完了(.run / .idx / .out)"] --> know["含まれ得る情報を把握"]
know --> share[".run だけを圧縮し暗号化して渡す"]
share --> keep["保管先と保持期間を決める"]
keep --> open["同じ版の WinDbg で開き .idx を生成"]
図26: トレースの受け渡しは「機密ファイルの受け渡し」。手順を決めてから録る。
12. まとめ
- ダンプは「状態」、TTDは「経路」。壊れた値の出所や呼び出しの順序が要る案件で、TTDは調査を機械的な作業に変える
- 録画は5〜20倍遅く、毎秒5〜50MB成長し、外れない。長期稼働に使うなら
-ring/-maxFile、-module、-recordmode Manual、-monitorで録る範囲を先に設計する - 再生は「イベントで入り、逆方向に歩く」。
TTD.Events→[Time Travel]→t-/g-、そしてba+g-で「誰が壊したか」を答えさせる TTD.CallsとTTD.Memoryはトレース全体への問い合わせ。ToSystemTime()とSystemTimeStartでログの時刻と突き合わせる- .NETは64ビットのSOSで状態を読み、
TTD.Callsはネイティブ境界に使う - トレースは機密ファイル。共有は
.runだけ、経路と保管を決めてから
ダンプは取れているのに原因に届かない、再現しないので手が止まっている、という案件は、録画範囲の設計さえできればTTDで決着することが少なくありません。録画の設計から .run の解析までを引き受けることもできますので、ダンプとログを添えてご相談ください。
関連記事
- WinDbg + SOSでクラッシュダンプを読む ── 収集した後の実務解析入門
- Windowsクラッシュダンプ収集入門 - WER/ProcDump/WinDbg
- PDB(プログラムデータベース)とは何か ── デバッグ情報・シンボル・Source Linkを理解する
- ハンドルリークで1か月後に落ちる ── 産業用カメラアプリ長期稼働障害の解剖(前編)
- 「応答なし」の正体 ── Windowsがアプリを「固まった」と判定する仕組みと、固まらない設計
- Process Monitor(ProcMon)実践ガイド
- WPR/WPA実践 ── 「PC全体が重い」をシステム全体から調べる性能調査入門
- PerfViewとdotnet-traceで「遅い」を特定する
- 親が落ちたあとに何が残るか ── Job Objectで子プロセスを飼う
関連する相談領域
合同会社小村ソフトでは、長期稼働後や断続的にしか発生しないWindowsアプリの不具合について、クラッシュダンプ・ログ・TTDトレースを組み合わせた原因調査、録画範囲の設計を含む調査体制の構築、ネイティブ境界(COM・P/Invoke・装置SDK)が絡む障害の切り分けを扱っています。「ダンプは取れたが原因に届かない」の段階からご相談ください。
参考リンク
-
Microsoft Learn, Time Travel Debugging - Overview. TTDがプロセスの実行を録画して前後に再生できること、ダンプが失敗に至る状態と実行経路を取り逃がしがちなこと、録画に管理者権限が必要なこと、録画に個人情報・セキュリティ関連情報が含まれ得ること、調査手段の比較表、
.run/.idxの役割、64ビットモードのSOS拡張でマネージドコードをデバッグできることについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Microsoft Learn, Time Travel Debugging - TTD.exe command line utility. 5〜20倍以上の速度低下、アタッチ後に自分では外れないこと、Windows Server 2016〜2025のサポート、インストールとオフライン展開、
-launch/-attach/-monitorの3モード、-out/-noUI/-accepteula/-stop/-wait/-tracingOff/-children/-cmdLineFilter/-timestampFilename/-ring/-maxFile/-maxConcurrentRecordings/-numVCpu/-replayCpuSupport/-module/-recordmodeの各オプション、.outファイルの読み方、トレース共有の助言について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26 ↩27 ↩28 ↩29 ↩30 ↩31 ↩32 ↩33 ↩34 -
Microsoft Learn, Time Travel Debugging - Overview - Things to look out for. アンチウイルスやメモリ監視ソフト・Electronとの非互換、ユーザーモード専用であること、再生が読み取り専用であること、保護されたプロセス(PPL)に注入できないこと、録画時の10〜20倍程度の性能影響について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Time Travel Debugging - Working with Trace Files. トレースサイズの要因(1命令あたり1ビット〜1バイト)、活動中は毎秒5〜50MB成長しアイドル中は成長しないこと、最大サイズの上限が無いこと、インデックスがトレースの1〜2倍になること、ディスク切れ時の録画・インデックス作成の挙動と回避策について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, Time Travel Debugging - Sample App Walkthrough. 例外イベントの位置へ移動し、
baとg-で不正な値を最後に書いた位置まで遡る一般手順、失敗地点が本当の原因から数ステップ進んだエラー処理の中にあることが多いこと、クラッシュ時にトレースが閉じられWinDbgが自動でインデックスを作ること、TTD.Memoryと.Last()の使い方について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Microsoft Learn, !tt (time travel).
!ttの位置指定(パーセント・xx:yy)、位置の2要素(シーケンシング番号とステップ数)の意味、TTD.PrevRegisterWrite/PrevMemoryAccess/NextMemoryAccessについて。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, TTD Position Objects. Positionオブジェクトの
Percent/Sequence/Steps、SeekTo()、おおよその実時刻(UTC)を返すToSystemTime()、FFFFFFFFFFFFFFFE:0がトレース末尾を表すことについて。 ↩ ↩2 -
Microsoft Learn, Time Travel Debugging - Troubleshooting. 昇格が必要なこと、UWPアプリの起動録画が未対応なこと、別セッション・別セキュリティコンテキストの「通常でないプロセス」が対象外なこと、
ping.exe/cmd.exeで切り分けること、Application Verifier併用時に再生が遅くなること、!index -status/!index -forceによるインデックスの再作成について。 ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, Time Travel Debugging - Record a trace. WinDbg UIの Launch executable (advanced) / Attach to process からの録画、Record with Time Travel Debugging チェックボックス、Configure and Record による保存先の設定、Record subset of execution によるモジュール限定について。 ↩
-
Microsoft, WinDbg-Samples - TTD in-process recording API (GitHub).
-recordmode Manualと組み合わせて、プログラム側から録画の開始・停止を制御するインプロセス録画APIのドキュメントについて。 ↩ -
Microsoft Learn, Time travel debugging release notes. 1.11.611でのAVX/AVX512対応プログラムの録画修正とインデックス形式の変更(再インデックスが必要)、1.11.553で追加された
@$curframe.TTD.VariableHistory()について。 ↩ ↩2 ↩3 -
Microsoft Learn, TTD Calls Objects.
TTD.Callsの引数と、ThreadId/UniqueThreadId/Function/ReturnValue/ReturnAddress/Parameters[]/TimeStart/TimeEnd/SystemTimeStart/SystemTimeEndの各プロパティ、PDBのシンボル情報が無い場合の既定(64ビット符号なし整数4引数・UnknownOrMissingSymbols)、計算に時間がかかり結果がキャッシュされることについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Microsoft Learn, Time Travel Debugging - Replay a trace.
p-/t-/g-による逆方向の実行、g-が順方向と同じイベントで止まること、!positions、~sがトレース内の位置を変えないことについて。 ↩ ↩2 ↩3 -
Microsoft Learn, TTD Event Objects. イベントの種類(ThreadCreated/ThreadTerminated/ModuleLoaded/ModuleUnloaded/Exception)と、Position・Module・Thread・Exceptionの子オブジェクトについて。 ↩
-
Microsoft Learn, TTD Exception Objects. 例外オブジェクトの
Type(Software/Hardware)、ProgramCounter、Code、Flags、Positionについて。 ↩ -
Microsoft Learn, WinDbg: Timelines. Timelinesウィンドウが例外・ブレークポイント・メモリアクセス・関数呼び出しを可視化し、例外のダブルクリックで
Position.SeekTo()が発行されることについて。 ↩ -
Microsoft Learn, !positions. アクティブなスレッドすべてと、それぞれのトレース内位置を表示することについて。 ↩ ↩2
-
Microsoft Learn, Introduction to Time Travel Debugging objects.
@$curprocess.TTD/@$cursession.TTDのオブジェクト、OrderBy/Where/Select/GroupByによるクエリ、GetLastErrorのエラー集計とMessageBoxWの最後の呼び出しの例、UnknownOrMissingSymbolsの意味、Callsが何も返さない4つの原因について。 ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, TTD Memory Objects.
TTD.Memoryのアクセス種別(r/w/rw/e/rwe/ec)と、結果の[Time Travel]リンクで位置へ移動できることについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
WinDbg + SOSでクラッシュダンプを読む ── 収集した後の実務解析入門
WindowsクラッシュダンプをWinDbgとSOS拡張で読む手順を解説します。シンボルパスの設定、!clrstackや!dumpheapによる例外とメモリリークの追い方、!analyze -v、dotnet-dump analyzeとの使い分けまで整理します。
DllMainとローダーロック ── 「DLLの初期化で何もするな」と言われる本当の理由
DllMainでLoadLibraryやスレッド同期をしてはいけないのはなぜか。全DLL通知を直列化するローダーロックの仕組みから、デッドロックが成立する典型シナリオ、遅延初期化などの正しい設計、ハング調査の手順までを一次情報で解説します。
スリープ・休止・Modern Standbyと長時間稼働アプリ ── 「夜中に止まっていた」を設計で防ぐ
長時間動き続けるWindowsアプリが「朝見たら止まっていた」となる原因を、S3スリープ/休止/Modern Standbyの違いから整理します。スリープ中のタイマーやTCP接続の挙動、SetThreadExecutionStateによる抑止まで解説します。
MAX_PATHとWindowsのパス・ファイル名の落とし穴 ── 260文字制限、予約名、末尾ドット、大文字小文字
「ファイルが見つかりません」の定番原因であるパス・ファイル名の制限を整理します。MAX_PATH=260文字の内訳、LongPathsEnabledによる長パス有効化、CON等の予約名、末尾ドットの正規化、Path.Combineの罠まで解説します。
ネットワークドライブとUNCパスの落とし穴 ── 業務アプリでファイルサーバー(共有フォルダ)を扱う実務
業務アプリから共有フォルダへ出力・監視するときの定番トラブルを整理します。ドライブ文字(Z:)がサービスから見えない理由、実行アカウントごとに必要な権限、エラー1219、FileSystemWatcherの注意点まで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
不具合調査 / 長期稼働テーマ
再現しにくい不具合、通信停止、長期稼働障害、失敗パス検証を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
不具合調査・原因解析
再現しにくい障害、長期稼働後の不具合、通信停止などの調査を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- Time Travel Debugging(TTD)とクラッシュダンプは何が違いますか?
- クラッシュダンプは「落ちた瞬間のメモリの写真」で、その時点の状態は分かりますが、そこに至った経路は写りません。TTDはプロセスの命令実行を丸ごと録画したもので、あとから前にも後ろにも再生でき、「その変数を最後に書き換えたのは誰か」「この例外の直前に何を呼んでいたか」を巻き戻して直接確かめられます。Microsoftの公式ドキュメントも、ダンプは失敗に至った状態と実行経路を取り逃がしがちだと明記しています。代償として録画中は5〜20倍遅くなり、トレースファイルは活動中に毎秒5〜50MB程度成長します。
- 何日も動かし続ける本番アプリにTTDを貼り付けておけますか?
- そのままでは無理です。TTDは録画中の速度低下が大きく、アクティブなプロセスではトレースが毎秒5〜50MB成長し、ファイルサイズに上限もありません。長期稼働に使うなら、TTD.exeの-ring(リングバッファ)と-maxFileで「最後のNMBだけ残す」、-moduleで自社モジュールが動いている間だけ録る、-recordmode Manualでアプリ側から録画区間を指定する、といった録画範囲の設計が前提になります。また、一度アタッチしたTTDは自分では外れないので、録画をやめるにはプロセスを終了(再起動)する運用まで含めて決めておく必要があります。
- サービスや別セッションのプロセスも録画できますか?
- TTD.exeのドキュメントは、-attachを「サービスや長時間稼働するアプリの調査」向け、-monitorを「プログラムやサービスが起動するたびに録画」する用途として説明しています。一方でトラブルシューティングのページには、別セッションや別のセキュリティコンテキストで動く「通常でないプロセス」は現時点では録画対象外だという記述もあります。実際に録れるかは環境次第なので、本番と同じ構成でまずping.exeやcmd.exeのような単純なプロセスを録画し、次に対象プロセスで試して確認してから運用に組み込んでください。
- .NETアプリのトレースでもTTDは使えますか?
- 使えます。公式ドキュメントは、64ビットモードで動くSOS拡張(sos.dll)をWinDbgのTTDトレース上で使ってマネージドコードをデバッグできると述べています。トレースの各位置で!clrstackや!peといったSOSコマンドを実行し、例外イベントの位置へ移動してからマネージドのスタックを読む、という組み合わせが基本です。TTD.Callsクエリでシンボル名から呼び出しを検索する機能はPDBのシンボル情報を前提とするので、.NETアプリではP/InvokeやCOM、Win32 APIといったネイティブ境界の呼び出しを追う使い方が確実です。
- トレースファイル(.run)を他社や社外に送っても大丈夫ですか?
- そのままでは危険です。TTDの録画にはプロセスのメモリ内容が含まれ、ファイルパス、レジストリ、メモリやファイルの中身など、個人情報や秘密情報が入り得ると公式ドキュメントが明記しています。送るなら、何が録画されているか(接続文字列、トークン、顧客データなど)を把握したうえで、暗号化した経路と保管先を決めてください。共有するのは.runだけで十分で、インデックスファイル(.idx)はWinDbgが開くときに自動生成されます。
- TTD.Callsで関数を検索しても何も返りません。なぜですか?
- 原因は主に4つです。第一にシンボルの問題で、PDBが無いモジュールの関数は「UnknownOrMissingSymbols」という名前になり、モジュール名も大文字のことがあるので、xコマンドで実際のシンボル名を確認します。第二に、対象DLLがその時点でまだ読み込まれていない場合で、DLLがロードされた後の位置へ移動してから再度クエリします。第三に、関数がインライン展開されているとクエリエンジンが追跡できません。第四に、ワイルドカードが広すぎて一致する関数が多すぎる場合で、パターンを絞ります。