WinDbg - タイムライン
タイム トラベル デバッグ (TTD) を使用すると、ユーザーはプログラムの実行の記録であるトレースを記録できます。 タイムラインは、実行中に発生するイベントを視覚的に表現したものです。 イベントとして表示される場所は、ブレークポイント、メモリの読み取り/書き込み、関数の呼び出しと戻り、例外などがあり得ます。
タイムライン ウィンドウを使用すると、重要なイベントをすばやく表示し、相対位置を理解し、TTD トレース ファイル内のその位置に簡単にジャンプできます。 複数のタイムラインを使用して、タイムトラベル トレース内のイベントを視覚的に調査し、イベントの相関関係を発見します。
タイムライン ウィンドウは、TTD トレース ファイルを開くときに表示され、データ モデル クエリを手動で作成しなくても主要なイベントを表示します。 同時に、すべてのタイム トラベル オブジェクトを使用して、より複雑なデータ クエリを実行できるようになります。
タイム トラベル トレース ファイルの作成と操作の詳細については、「タイム トラベル デバッグ - 概要」を参照してください。
タイムラインの種類
タイムライン ウィンドウには次のイベントを表示できます。
- 例外 (特定の例外コードでさらにフィルタリングできます)
- ブレークポイント
- 関数呼び出し (module!function の形式で検索)
- メモリ アクセス (2 つのメモリ アドレス間の読み取り / 書き込み / 実行)
各イベントの上にマウスを置くと、ツールチップから詳細情報が表示されます。 イベントをクリックすると、そのイベントのクエリが実行され、詳細情報が表示されます。 イベントをダブルクリックすると、TTD トレース ファイル内のその場所にジャンプします。
例外
トレース ファイルをロードし、タイムラインがアクティブになると、記録内の例外が自動的に表示されます。
ブレークポイントの上にマウスを置くと、例外の種類や例外コードなどの情報が表示されます。
オプションの例外コード フィールドを使用して、特定の例外コードをさらにフィルタリングできます。
特定の例外タイプに新しいタイムラインを追加することもできます。
ブレークポイント
ブレークポイントを追加した後、そのブレークポイントがヒットしたときの位置をタイムライン上に表示できます。 これは、たとえば bp Set Breakpoint コマンドを使用して実行できます。 ブレークポイントの上にマウスを移動すると、ブレークポイントに関連付けられたアドレスと命令ポインタが表示されます。
ブレークポイントがクリアされると、関連付けられたブレークポイント タイムラインが自動的に削除されます。
関数呼び出し
関数呼び出しの位置をタイムライン上に表示できます。 これを行うには、 たとえば module!function
の形式 TimelineTestCode!multiplyTwo
で検索を提供します。 ワイルドカード (例: TimelineTestCode!m*
) を指定することもできます。
関数呼び出しの上にカーソルを置くと、関数名、入力パラメータ、その値、および戻り値が表示されます。 この例では、 バッファ と サイズ が DisplayGreeting!GetCppConGreeting のパラメータであるため、示しています。
メモリ アクセス
メモリ アクセス タイムラインを使用して、メモリの特定の範囲がいつ読み書きされたか、またはどこでコードが実行されたかを表示します。 開始アドレスと停止アドレスは、2 つのメモリ アドレス間の範囲を定義するために使用されます。
メモリ アクセス項目の上にマウスを置くと、値と命令ポインタが表示されます。
タイムラインを操作する
タイムライン上にカーソルを置くと、灰色の垂直線がカーソルの後に表示されます。 青い垂直線は、トレース内の現在位置を示します。
虫眼鏡アイコンをクリックして、タイムラインを拡大または縮小します。
上部のタイムライン コントロール領域で、長方形を使用してタイムラインのビューをパンします。 長方形の外側の区切り文字をドラッグして、現在のタイムライン ビューのサイズを変更します。
マウスの動き
Ctrl + スクロールホイールを使用してズームインおよびズームアウトします。
Shift + スクロール ホイールを使用して左右にパンします。
タイムラインのデバッグ手法
デバッグ タイムライン手法をデモンストレーションするために、ここでは タイム トラベル デバッグ ウォークスルー を再利用します。 このデモは、サンプル コードを構築するための最初の 2 つの手順が完了し、そこに記載されている最初の 2 つの手順を使用して TTD 記録を作成していることを前提としています。
セクション 2: 「DisplayGreeting」サンプルのトレースを記録する
このシナリオでは、最初のステップはタイム トラベル トレース内の例外を見つけることです。 これは、タイムライン上にある唯一の例外をダブルクリックすることで実行できます。
コマンド ウィンドウを見ると、例外をクリックしたときに次のコマンドが発行されたことがわかります。
(2dcc.6600): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: CC:0
@$curprocess.TTD.Events.Where(t => t.Type == "Exception")[0x0].Position.SeekTo()
View>>Registers を選択してタイムラインのこの時点のレジスタを表示し、調査を開始します。
コマンド出力では、スタック (esp) とベース ポインター (ebp) が 2 つのまったく異なるアドレスを指していることに注意してください。 これはスタックの破損を示している可能性があります。関数が返されてスタックが破損した可能性があります。 これを検証するには、CPU 状態が破損する前に戻り、いつスタック破損が発生したかを判断できるかどうかを確認する必要があります。
これを行う際に、ローカル変数とスタックの値を調べます。
View>>Locals を選択してローカル値を表示します。
View>>Stack を選択して、コード実行スタックを表示します。
トレースの失敗時点では、エラー処理コードの真の原因の数ステップ後に終了するのが一般的です。 タイム トラベルを使用すると、一度に指示を遡って真の根本原因を特定できます。
[ホーム] リボンから [ステップイン] コマンドを使用して、3 つの命令に戻ります。 これを実行しながら、スタック、ローカル、およびレジスタ ウィンドウを調べ続けます。
コマンド ウィンドウには、3 命令前に戻ると、タイムトラベル位置とレジスタが表示されます。
0:000> t-
Time Travel Position: CB:41
eax=00000000 ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=00540020 esp=003cf7d0 ebp=00520055 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
00540020 ?? ???
0:000> t-
Time Travel Position: CB:40
eax=00000000 ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=00061767 esp=003cf7cc ebp=00520055 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
DisplayGreeting!main+0x57:
00061767 c3 ret
0:000> t-
Time Travel Position: CB:3A
eax=0000004c ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=0006175f esp=003cf718 ebp=003cf7c8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
DisplayGreeting!main+0x4f:
0006175f 33c0 xor eax,eax
トレースのこの時点では、スタックとベース ポインターにはより意味のある値が含まれているため、コード内の破損が発生したポイントに近づいているように見えます。
esp=003cf718 ebp=003cf7c8
また、興味深いのは、ローカル ウィンドウにはターゲット アプリの値が含まれており、ソース コード ウィンドウには、トレースのこの時点でソース コード内で実行する準備ができているコード行が強調表示されていることです。
さらに調査するには、メモリ ウィンドウを開いて、スタック ポインタ (esp) メモリ アドレス付近の内容を表示します。 この例では、値は 003cf7c8 です。 [Memory>>Text>>ASCII] を選択すると、そのアドレスに保存されている ASCII テキストが表示されます。
メモリ アクセスのタイムライン
対象のメモリ位置が特定されたら、その値を使用してメモリ アクセス タイムラインを追加します。 [+ タイムラインを追加] をクリックし、開始アドレスを入力します。 4 バイトを見ていきますので、それを開始アドレス 003cf7c8 に追加すると、003cf7cb になります。 デフォルトではすべてのメモリ書き込みを確認しますが、そのアドレスでの書き込みまたはコード実行のみを確認することもできます。
ここで、タイムラインを逆にたどって、このタイムトラベルトレースのどの時点でこの記憶の場所が書き込まれたのかを調べ、何が見つかるかを確認できます。 タイムラインのこの位置をクリックすると、コピーされる文字列に対して地元の人が異なる値を評価していることがわかります。 文字列の長さが正しくないかのように、宛先の値が不完全であるように見えます。
ブレークポイントのタイムライン
ブレークポイントの使用は、関心のあるイベントでコードの実行を一時停止する一般的な方法です。 TTD を使用すると、ブレークポイントを設定し、トレースが記録された後にそのブレークポイントに到達するまで時間を遡ることができます。 問題の発生後にプロセスの状態を調査し、ブレークポイントの最適な場所を決定する機能により、TTD に固有の追加のデバッグ ワークフローが可能になります。
別のタイムライン デバッグ手法を検討するには、タイムラインで例外をクリックし、 [ホーム] リボンの [ステップイン] コマンドを使用して、もう一度 3 ステップ前に戻ります。
この非常に小さなサンプルでは、コードを調べるだけで非常に簡単ですが、数百行のコードと数十のサブルーチンがある場合は、ここで説明する手法を使用して、問題を特定するのに必要な時間を短縮できます。
前述したように、ベース ポインター (esp) は命令を指すのではなく、メッセージ テキストを指します。
ba コマンドを使用して、メモリ アクセスにブレークポイントを設定します。 aw - write ブレークポイントを設定して、メモリのこの領域にいつ書き込まれるかを確認します。
0:000> ba w4 003cf7c8
ここでは単純なメモリ アクセス ブレークポイントを使用しますが、ブレークポイントはより複雑な条件ステートメントとして構築できます。 詳細については、「bp、bu、bm (ブレークポイントの設定)」を参照してください。
[ホーム] メニューから [戻る] を選択すると、ブレークポイントに到達するまで過去に戻ります。
この時点で、プログラム スタックを調べて、どのコードがアクティブであるかを確認できます。
Microsoft が提供する wscpy_s() 関数にこのようなコード バグがある可能性は非常に低いため、スタックをさらに調べます。 スタックは、Greeting!main が Greeting!GetCppConGreeting を呼び出していることを示しています。 私たちの非常に小さなコード サンプルでは、この時点でコードを開くだけで、おそらくエラーを簡単に見つけることができます。 ただし、より大規模で複雑なプログラムで使用できるテクニックを説明するために、関数呼び出しタイムラインの追加を設定します。
関数呼び出しのタイムライン
[+ タイムラインの追加] をクリックし、 関数の検索文字列の DisplayGreeting!GetCppConGreeting
に入力します。
[開始位置] チェック ボックスと [終了位置] チェック ボックスは、トレース内の関数呼び出しの開始位置と終了位置を示します。
dx コマンドを使用して関数呼び出しオブジェクトを表示し、関数呼び出しの開始位置と終了位置に対応する関連する TimeStart フィールドと TimeEnd フィールドを確認できます。
dx @$cursession.TTD.Calls("DisplayGreeting!GetCppConGreeting")[0x0]
EventType : 0x0
ThreadId : 0x6600
UniqueThreadId : 0x2
TimeStart : 6D:BD [Time Travel]
SystemTimeStart : Thursday, October 31, 2019 23:36:05
TimeEnd : 6D:742 [Time Travel]
SystemTimeEnd : Thursday, October 31, 2019 23:36:05
Function : DisplayGreeting!GetCppConGreeting
FunctionAddress : 0x615a0
ReturnAddress : 0x61746
Parameters
[開始] または [終了] のいずれか、または [開始] と [終了] の両方の場所ボックスをオンにする必要があります。
私たちのコードは再帰的でもリエントラントでもないので、GetCppConGreeting メソッドが呼び出されたときにタイムライン上で見つけるのは非常に簡単です。 GetCppConGreeting への呼び出しも、ブレークポイントと定義したメモリ アクセス イベントと同時に発生します。 したがって、アプリケーションクラッシュの根本原因を注意深く調査するコードの領域を絞り込んだようです。
複数のタイムラインを表示してコードの実行を調査する
コード サンプルは小さいですが、複数のタイムラインを使用する手法により、タイム トラベル トレースを視覚的に探索できます。 トレース ファイル全体を調べて、「ブレークポイントに到達する前にメモリ領域にアクセスされたのはいつですか?」などの質問をすることができます。
追加の相関関係を確認したり、予期していなかったものを見つけたりできる機能により、タイムライン ツールは、コマンド ライン コマンドを使用してタイム トラベル トレースを操作する場合と異なります。
タイムラインのブックマーク
重要なタイム トラベル位置をメモ帳に手動でコピーして貼り付けるのではなく、WinDbg でブックマークします。 ブックマークを使用すると、他のイベントに対するトレース内のさまざまな位置を一目で確認したり、注釈を付けたりすることが容易になります。
ブックマークにわかりやすい名前を付けることができます。
[タイムライン > の表示] で使用できるタイムライン ウィンドウからブックマークにアクセスします。 ブックマークの上にマウスを置くと、ブックマーク名が表示されます。
ブックマークを右クリックしてその位置に移動したり、ブックマークの名前を変更したり削除したりできます。
Note
バージョン 1.2402.24001.0 では、ブックマーク機能は使用できません。