Time Travel Debugging - サンプル アプリのチュートリアル

時計を特徴とするタイム トラベル デバッグのロゴ。

このラボでは、コードに欠陥のある小さなサンプル プログラムを使用して、タイム トラベル デバッグ (TTD) について説明します。 TTD を使って問題をデバッグし、その根本原因を特定します。 この小さなプログラムの問題は簡単に見つけることができますが、この一般的な手順はより複雑なコードでも使用できます。 この一般的な手順の概要は次のとおりです。

  1. 失敗したプログラムのタイム トラベル トレースをキャプチャします。
  2. dx (デバッガー オブジェクト モデル式の表示) コマンドを使用して、記録に保存されている例外イベントを見つけます。
  3. !tt (タイム トラベル) コマンドを使用して、トレース内の例外イベントの位置に移動します。
  4. トレース内のその位置から、エラーが発生したコードがスコープに入るまで、1 ステップ後退します。
  5. スコープ内のエラーが発生したコードで、ローカル値を確認し、間違った値が含まれている可能性のある変数の仮説を立てます。
  6. 正しくない値を使用して、変数のメモリ アドレスを特定します。
  7. ba (アクセス中に中断) コマンドを使用して、疑わしい変数のアドレスにメモリ アクセス (ba) ブレークポイントを設定します。
  8. g- を使用して、疑わしい変数の最後のメモリ アクセス ポイントまで逆方向に実行します。
  9. その場所か、その前のいくつかの手順にコードの欠陥がないかを確認します。 欠陥が見つかった場合は、完了です。 正しくない値が他の変数から取得されている場合は、2 番目の変数のアクセス ブレークポイントに別のブレークを設定します。
  10. g- を使用して、2 番目の疑わしい変数の最後のメモリ アクセス ポイントまで逆方向に実行します。 その場所か、その前のいくつかの手順にコードの欠陥が含まれていないかを確認します。 欠陥が見つかった場合は、完了です。
  11. エラーの原因である正しくない値を設定しているコードが見つかるまで、このプロセスを繰り返します。

この手順で説明する一般的な手法は、さまざまなコードの問題に応用が効きますが、固有のアプローチを必要とする固有のコードの問題もあります。 このチュートリアルで紹介する手法は、デバッグ ツール セットを拡張するのに役立つはずで、TTD トレースでできることの一部を垣間見られます。

ラボの目的

このラボを完了すると、タイム トラベル トレースで一般的な手順を使用して、コード内の問題を特定できるようになります。

ラボのセットアップ

ラボを完了するには、次のハードウェアが必要です。

  • Windows 10 または Windows 11 を実行しているノート PC またはデスクトップ コンピューター (ホスト)

ラボを完了するには、次のソフトウェアが必要です。

  • WinDbg。 WinDbg のインストールの詳細については、「WinDbg - インストール」を参照してください。
  • Visual Studio。サンプル C++ コードをビルドします。

このラボには、次の 3 つのセクションがあります。

セクション 1: サンプル コードをビルドする

セクション 1 では、Visual Studio を使用してサンプル コードをビルドします。

Visual Studio でサンプル アプリを作成する

  1. Microsoft Visual Studio で、 [ファイル]>[新規作成]>[プロジェクト/ソリューション] の順にクリックし、Visual C++ テンプレートをクリックします。

    Win32 コンソール アプリケーションを選択します。

    DisplayGreeting というプロジェクト名を入力し、 [OK] をクリックします。

  2. [Security Development Lifecycle (SDL) チェック] をオフにします。

    Visual Studio の Win32 アプリケーション ウィザードの設定。

  3. [完了] をクリックします。

  4. Visual Studio の [DisplayGreeting.cpp] ペインに次のテキストを貼り付けます。

    // DisplayGreeting.cpp : Defines the entry point for the console application.
    //
    
    #include "stdafx.h"
    #include <array>
    #include <stdio.h>
    #include <string.h>
    
    void GetCppConGreeting(wchar_t* buffer, size_t size)
    {
        wchar_t const* const message = L"HELLO FROM THE WINDBG TEAM. GOOD LUCK IN ALL OF YOUR TIME TRAVEL DEBUGGING!";
    
        wcscpy_s(buffer, size, message);
    }
    
    int main()
    {
         std::array <wchar_t, 50> greeting{};
         GetCppConGreeting(greeting.data(), sizeof(greeting));
    
         wprintf(L"%ls\n", greeting.data());
    
         return 0;
    }
    
  5. Visual Studio で、 [プロジェクト]>[DisplayGreeting のプロパティ] の順にクリックします。 次に、 [C/C++][Code Generation] (コード生成) をクリックします。

    次のプロパティを設定します。

    設定 Value
    セキュリティ チェック Disable Security Check (/GS-) (セキュリティ チェックを無効にする (/GS-))
    基本ランタイムのチェック 既定値

    Note

    これらの設定は推奨されませんが、コーディングを迅速化したり、特定のテスト環境を支援したりするために、これらの設定を使用することをだれかが勧めるシナリオを想像できます。

  6. Visual Studio で、 [ビルド]>[ソリューションのビルド] をクリックします。

    すべて問題がなければ、ビルド ウィンドウにビルドが成功したことを示すメッセージが表示されます。

  7. ビルドされたサンプル アプリ ファイルを見つける

    ソリューション エクスプローラーで [DisplayGreeting] プロジェクトを右クリックし、 [エクスプローラーでフォルダーを開く] を選択します。

    サンプルのコンパイルされた exe ファイルとシンボル pdb ファイルが格納されている Debug フォルダーに移動します。 たとえば、プロジェクトが保存されているフォルダーが C:\Projects\DisplayGreeting\Debug の場合、このフォルダーに移動します。

  8. コードの欠陥があるサンプル アプリを実行する

    exe ファイルをダブルクリックして、サンプル アプリを実行します。

    DisplayGreeting.exe ファイルを実行しているコンソールのスクリーンショット。

    次のダイアログ ボックスが表示されたら、 [Close program] (プログラムを閉じる) を選択します。

    チュートリアルの次のセクションでは、サンプル アプリの実行を記録して、この例外が発生している理由を特定できるかどうかを確認します。

セクション 2: "DisplayGreeting" サンプルのトレースを記録する

セクション 2 では、動作に問題がある "DisplayGreeting" サンプル アプリのトレースを記録します。

サンプル アプリを起動し、TTD トレースを記録するには、次の手順に従います。 TTD トレースの記録に関する一般的な情報については、「タイム トラベル デバッグ - トレースの記録」を参照してください 。

  1. タイム トラベル トレースを記録できるように、WinDbg を管理者として実行します。

  2. WinDbg で、 [ファイル]>[デバッグ開始]>[実行可能ファイルの起動 (詳細)] の順に選択します。

  3. 記録するユーザー モードの実行可能ファイルへのパスを入力するか、 [参照] を選択して実行可能ファイルに移動します。 WinDbg の [実行可能ファイルの起動] メニューの操作については、「WinDbg - ユーザー モード セッションを開始する」を参照してください。

    [実行可能ファイルの起動 (詳細)] 画面に [タイム トラベル デバッグで記録する] チェック ボックスが表示されている WinDbg のスクリーンショット。

  4. [タイム トラベル デバッグで記録] チェックボックスをオンにして、実行可能ファイルが起動したらトレースが記録されるようにします。

  5. [構成と記録] をクリックして記録を開始します。

  6. [Configure recording] (記録の構成) ダイアログ ボックスが表示されたら、 [記録] をクリックして実行可能ファイルを起動し、記録を開始します。

    パスが temp に設定された [記録の構成] ダイアログが表示されている WinDbg のスクリーンショット。

  7. トレースが記録されていることを示す記録ダイアログが表示されます。 その直後にアプリケーションがクラッシュします。

  8. [Close Program] (プログラムを閉じる) をクリックして、[DisplayGreeting has stopped working] (DisplayGreeting が動作を停止しました) ダイアログ ボックスを無視します。

    DisplayGreeting アプリが動作を停止したことを示すダイアログ ボックス。

  9. プログラムがクラッシュすると、トレース ファイルが閉じられ、ディスクに書き込まれます。

    インデックスが作成された 1/1 キーフレームを表示する WinDbg 出力のスクリーンショット。

  10. デバッガーによって、トレース ファイルが自動的に開き、インデックスが作成されます。 インデックス作成は、トレース ファイルの効率的なデバッグを可能にするプロセスです。 このインデックス作成プロセスは、トレース ファイルが大きいほど時間がかかります。

    (5120.2540): Break instruction exception - code 80000003 (first/second chance not available)
    Time Travel Position: D:0 [Unindexed] Index
    !index
    Indexed 10/22 keyframes
    Indexed 20/22 keyframes
    Indexed 22/22 keyframes
    Successfully created the index in 755ms.
    

Note

キーフレームとは、インデックス作成に使用されるトレース内の場所です。 キーフレームは自動的に生成されます。 トレースが大きいほど、含まれるキーフレームも多くなります。

  1. この時点でトレース ファイルの先頭に移動され、時間を進めたり戻したりできるようになっています。

    これで TTD トレースが記録されたので、トレースを再生したり、トレース ファイルを操作 (同僚と共有するなど) したりできます。 トレース ファイルの操作の詳細については、「タイム トラベル デバッグ - トレース ファイルの操作」を参照してください。

このラボの次のセクションでは、トレース ファイルを分析してコードに関する問題を特定します。

セクション 3: トレース ファイルの記録を分析してコードの問題を特定する

セクション 3 では、トレース ファイルの記録を分析してコードの問題を特定します。

WinDbg 環境を構成する

  1. 次のコマンドを入力して、シンボル パスにローカル シンボルの場所を追加し、シンボルを再読み込みします。

    .sympath+ C:\MyProjects\DisplayGreeting\Debug
    .reload
    
  2. 次のコマンドを入力して、ソース パスにローカル コードの場所を追加します。

    .srcpath+ C:\MyProjects\DisplayGreeting\DisplayGreeting
    
  3. スタック変数とローカル変数の状態を表示できるようにするには、WinDbg リボンで [表示][ローカル][表示][スタック] を選択します。 ソース コード ウィンドウとコマンド ウィンドウを同時に表示できるようにウィンドウを整理します。

  4. WinDbg リボンで [ソース] を選択し、 [ソース ファイルを開く] を選択します。 DisplayGreeting.cpp ファイルを見つけて開きます。

例外を調べる

  1. トレース ファイルが読み込まれると、例外が発生したことを示す情報が表示されます。

    2fa8.1fdc): Break instruction exception - code 80000003 (first/second chance not available)
    Time Travel Position: 15:0
    eax=68ef8100 ebx=00000000 ecx=77a266ac edx=69614afc esi=6961137c edi=004da000
    eip=77a266ac esp=0023f9b4 ebp=0023fc04 iopl=0         nv up ei pl nz na pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
    ntdll!LdrpInitializeProcess+0x1d1c:
    77a266ac 83bdbcfeffff00  cmp     dword ptr [ebp-144h],0 ss:002b:0023fac0=00000000
    
  2. dx コマンドを使用して、記録内のすべてのイベントを表示します。 これらのイベントの中に例外イベントが表示されます。

    0:000> dx -r1 @$curprocess.TTD.Events
    ...
    [0x2c]           : Module Loaded at position: 9967:0
    [0x2d]           : Exception at 9BDC:0
    [0x2e]           : Thread terminated at 9C43:0
    ...
    
    

    Note

    このチュートリアルでは、無関係の出力が削除されたことを示すために 3 つのピリオドを使用しています。

  3. 例外イベントをクリックすると、その TTD イベントに関する情報が表示されます。

    0:000> dx -r1 @$curprocess.TTD.Events[17]
    @$curprocess.TTD.Events[17]                 : Exception at 68:0
        Type             : Exception
        Position         : 68:0 [Time Travel]
        Exception        : Exception of type Hardware at PC: 0X540020
    
  4. Exception フィールドをクリックして、例外データをさらにドリルダウンします。

    0:000> dx -r1 @$curprocess.TTD.Events[17].Exception
    @$curprocess.TTD.Events[17].Exception                 : Exception of type Hardware at PC: 0X540020
        Position         : 68:0 [Time Travel]
        Type             : Hardware
        ProgramCounter   : 0x540020
        Code             : 0xc0000005
        Flags            : 0x0
        RecordAddress    : 0x0
    

    例外データは、これが CPU によってスローされたハードウェア障害であることを示しています。 また、これがアクセス違反であることを示す例外コード 0xc0000005 も表示されています。 通常、これは、アクセス権のないメモリに書き込もうとしていたことを示しています。

  5. 例外イベントの [Time Travel] リンクをクリックして、トレース内のその位置に移動します。

    0:000> dx @$curprocess.TTD.Events[17].Exception.Position.SeekTo()
    Setting position: 68:0
    
    @$curprocess.TTD.Events[17].Exception.Position.SeekTo()
    (16c8.1f28): Break instruction exception - code 80000003 (first/second chance not available)
    Time Travel Position: 68:0
    eax=00000000 ebx=00cf8000 ecx=99da9203 edx=69cf1a6c esi=00191046 edi=00191046
    eip=00540020 esp=00effe4c 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 ??
    

    この出力で注目すべき点は、スタックとベース ポインターがまったく異なる 2 つのアドレスを指していることです。

    esp=00effe4c ebp=00520055
    

    これはスタックの破損を示している可能性があります。関数から制御が戻された後、スタックが破損した可能性があります。 これを検証するには、CPU の状態が破損する前まで遡り、スタックの破損がいつ発生したのかを特定できるかどうかを確認する必要があります。

ローカル変数を調べてコード ブレークポイントを設定する

トレースの失敗時点では、エラー処理コードの真の原因の数ステップ後に終了するのが一般的です。 タイム トラベルを使用すると、一度に指示を遡って真の根本原因を特定できます。

  1. [ホーム] リボンの [Step Into Back] (ステップ バック) コマンドを使用して、3 つの命令をステップ バックします。 これを行いながら、スタック ウィンドウとメモリ ウィンドウで調査を続けます。

    3 つの命令をステップ バックすると、コマンド ウィンドウにタイム トラベル位置とレジスタが表示されます。

    0:000> t-
    Time Travel Position: 67:40
    eax=00000000 ebx=00cf8000 ecx=99da9203 edx=69cf1a6c esi=00191046 edi=00191046
    eip=00540020 esp=00effe4c 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: 67:3F
    eax=00000000 ebx=00cf8000 ecx=99da9203 edx=69cf1a6c esi=00191046 edi=00191046
    eip=0019193d esp=00effe48 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+0x4d:
    0019193d c3
    
    0:000> t-
    Time Travel Position: 67:39
    eax=0000004c ebx=00cf8000 ecx=99da9203 edx=69cf1a6c esi=00191046 edi=00191046
    eip=00191935 esp=00effd94 ebp=00effe44 iopl=0         nv up ei pl nz ac po nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000212
    DisplayGreeting!main+0x45:
    

    Note

    このチュートリアルでは、コマンド ラインの使用を好むユーザーがコマンド ライン コマンドを使用できるように、コマンド出力には、UI メニュー オプションの代わりに使用できるコマンドが表示されています。

  2. トレースのこの時点では、スタックとベース ポインターの値はより理にかなっているので、コード内の破損が発生した箇所に近づいているように思われます。

    esp=00effd94 ebp=00effe44
    

    また、興味深いのは、ローカル ウィンドウにはターゲット アプリの値が含まれており、ソース コード ウィンドウでは、トレースのこの時点で実行できるコード行が強調表示されていることです。

    メモリ ASCII 出力と [ソース コード] ウィンドウを含む [ローカル] ウィンドウが表示されている WinDbg のスクリーンショット。

  3. さらに調査するために、メモリ ウィンドウを開いて、ベース ポインターのメモリ アドレス 0x00effe44 付近の内容を表示します。

  4. 関連する ASCII 文字を表示するために、[メモリ] リボンで [テキスト][ASCII] の順に選択します。

    メモリ ASCII 出力と [ソース コード] ウィンドウが表示されている WinDbg プレビューのスクリーンショット。

  5. ベース ポインターが命令を指しているのではなく、メッセージ テキストを指しています。 つまり、ここに何か問題があり、スタックが破損した時点に近い可能性があります。 さらに調査するために、ブレークポイントを設定します。

Note

この非常に小さなサンプルでは、コードを調べるのは非常に簡単ですが、数百行のコードと数十個のサブルーチンがある場合は、ここで説明する手法を使用することで、問題を特定するために必要な時間を短縮できます。

TTD とブレークポイント

ブレークポイントの使用は、関心のあるイベントでコードの実行を一時停止する一般的な方法です。 TTD を使用すると、ブレークポイントを設定し、トレースが記録された後にそのブレークポイントに到達するまで時間を遡ることができます。 問題の発生後にプロセスの状態を調査し、ブレークポイントの最適な場所を決定する機能により、TTD に固有の追加のデバッグ ワークフローが可能になります。

メモリ アクセス ブレークポイント

メモリ位置にアクセスしたときに発生するブレークポイントを設定できます。 次の構文で ba (アクセス時に中断) コマンドを使用します。

ba <access> <size> <address> {options}
オプション 説明

e

execute (CPU がアドレスから命令をフェッチする場合)

r

read/write (CPU がアドレスに対して読み取りまたは書き込みを実行する場合)

w

write (CPU がアドレスに書き込む場合)

特定の時点で設定できるデータ ブレークポイントは 4 つだけであることに注意し、データを正しく配置する必要があります。そうしないと、ブレークポイントをトリガーできません (ワードは 2 で割り切れるアドレスで終わる必要があり、dword は 4 で、クワッドワードは 0 または 8 で割り切れる必要があります)。

ベース ポインターのメモリ アクセス ブレークポイントに中断を設定する

  1. トレースのこの時点で、ベース ポインターへの書き込みメモリ アクセスにブレークポイントを設定します。この例では、ebp は 00effe44 です。 これを行うには、監視するアドレスを使用して ba コマンドを使用します。 4 バイトの書き込みを監視するので、w4 を指定します。

    0:000> ba w4 00effe44
    
  2. [表示][ブレークポイント] の順に選択して、意図したとおりに設定されていることを確認します。

    1 つのブレークポイントが表示されている [WinDbg ブレークポイント] ウィンドウのスクリーンショット。

  3. [ホーム] メニューから [戻る] を選択して、ブレークポイントにヒットするまで時間を遡ります。

    0:000> g-
    Breakpoint 0 hit
    Time Travel Position: 5B:92
    eax=0000000f ebx=003db000 ecx=00000000 edx=00cc1a6c esi=00d41046 edi=0053fde8
    eip=00d4174a esp=0053fcf8 ebp=0053fde8 iopl=0         nv up ei pl nz ac pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
    DisplayGreeting!DisplayGreeting+0x3a:
    00d4174a c745e000000000  mov     dword ptr [ebp-20h],0 ss:002b:0053fdc8=cccccccc
    
  4. [表示][ローカル] の順に選択します。 [ローカル] ウィンドウでは、 destination 変数にメッセージの一部しか含まれていませんが、 source にはすべてのテキストが含まれていることがわかります。 この情報は、スタックが破損したという考えを裏付けています。

    [ローカル] ウィンドウが表示されている WinDbg のスクリーンショット。

  5. この時点で、プログラム スタックを調べてアクティブなコードを確認できます。 [表示] リボンから [スタック] を選択します。

    [スタック] ウィンドウが表示されている WinDbg のスクリーンショット。

Microsoft が提供する wscpy_s() 関数にこのようなコード バグがある可能性は非常に低いため、スタックをさらに調べます。 スタックは、Greeting!main が Greeting!GetCppConGreeting を呼び出していることを示しています。 この非常に小さなコード サンプルでは、この時点でコードを開くだけでエラーを簡単に見つけることができると思われます。 しかし、より大規模で複雑なプログラムで使用できる手法を説明するために、新しいブレークポイントを設定してさらに調査を行います。

GetCppConGreeting 関数のアクセス ブレークポイントに中断を設定する

  1. [ブレークポイント] ウィンドウで既存のブレークポイントを右クリックし、 [削除] を選択して、既存のブレークポイントを解除します。

  2. dx コマンドを使用して、DisplayGreeting!GetCppConGreeting 関数のアドレスを確認します。

    0:000> dx &DisplayGreeting!GetCppConGreeting
    &DisplayGreeting!GetCppConGreeting                 : 0xb61720 [Type: void (__cdecl*)(wchar_t *,unsigned int)]
        [Type: void __cdecl(wchar_t *,unsigned int)]
    
  3. ba コマンドを使用して、メモリ アクセスにブレークポイントを設定します。 関数は実行のためにメモリから読み取られるだけなので、r - 読み取りブレークポイントを設定する必要があります。

    0:000> ba r4 b61720
    
  4. [ブレークポイント] ウィンドウで、ハードウェア読み取りブレークポイントがアクティブであることを確認します。

    1 つのハードウェア読み取りブレークポイントが表示されている [WinDbg ブレークポイント] ウィンドウのスクリーンショット。

  5. あいさつ文字列のサイズが気になるので、sizeof(greeting) の値を表示するように [ウォッチ] ウィンドウを設定します。 [表示] リボンで [ウォッチ] を選択し、 sizeof(greeting) を指定します。

    [ウォッチ] ウィンドウが表示されている WinDbg のスクリーンショット。

  6. [タイム トラベル] メニューで、 [開始するタイム トラベル] を使用するか、 !tt 0コマンドを使用してトレースの先頭に移動します。

    0:000> !tt 0
    Setting position to the beginning of the trace
    Setting position: 15:0
    (1e5c.710): Break instruction exception - code 80000003 (first/second chance not available)
    Time Travel Position: 15:0
    eax=68e28100 ebx=00000000 ecx=77a266ac edx=69e34afc esi=69e3137c edi=00fa2000
    eip=77a266ac esp=00ddf3b8 ebp=00ddf608 iopl=0         nv up ei pl nz na pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
    ntdll!LdrpInitializeProcess+0x1d1c:
    77a266ac 83bdbcfeffff00  cmp     dword ptr [ebp-144h],0 ss:002b:00ddf4c4=00000000
    
  7. [ホーム] メニューの [移動] を選択するか、 g コマンドを使用して、ブレークポイントにヒットするまでコード内を進みます。

    0:000> g
    Breakpoint 2 hit
    Time Travel Position: 4B:1AD
    eax=00ddf800 ebx=00fa2000 ecx=00ddf800 edx=00b61046 esi=00b61046 edi=00b61046
    eip=00b61721 esp=00ddf7a4 ebp=00ddf864 iopl=0         nv up ei pl nz na po nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
    DisplayGreeting!GetCppConGreeting+0x1:
    00b61721 8bec            mov     ebp,esp
    
  8. [ホーム] メニューの [Step Out Back] (ステップ アウト バック) を選択するか、 g-u コマンドを使用して 1 ステップ戻ります。

    0:000> g-u
    Time Travel Position: 4B:1AA
    eax=00ddf800 ebx=00fa2000 ecx=00ddf800 edx=00b61046 esi=00b61046 edi=00b61046
    eip=00b61917 esp=00ddf7ac ebp=00ddf864 iopl=0         nv up ei pl nz na po nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
    DisplayGreeting!main+0x27:
    00b61917 e8def7ffff      call    DisplayGreeting!ILT+245(?GetCppConGreetingYAXPA_WIZ) (00b610fa)
    
  9. 根本原因が見つかったようです。 宣言されている greeting 配列の長さは 50 文字ですが、GetCppConGreeting に渡す sizeof(greeting) は 0x64 (100) です。

    DisplayGreeting コードを表示する WinDbg のスクリーンショット。[ウォッチ ローカル] ウィンドウに 0x64 が表示されています。

    サイズの問題をさらに見ていくと、メッセージの長さが 75 文字で、文字列の終端文字を含めると 76 文字であることもわかります。

    HELLO FROM THE WINDBG TEAM. GOOD LUCK IN ALL OF YOUR TIME TRAVEL DEBUGGING!
    
  10. このコードを修正する 1 つの方法として、文字配列のサイズを 100 に拡張します。

    std::array <wchar_t, 100> greeting{};
    

    また、このコード行で sizeof(greeting) を size(greeting) に変更する必要があります。

     GetCppConGreeting(greeting.data(), size(greeting));
    
  11. これらの修正を検証するために、コードを再コンパイルし、エラーなしで実行されることを確認します。

ソース ウィンドウを使用してブレークポイントを設定する

  1. この調査を行う別の方法は、任意のコード行をクリックしてブレークポイントを設定することです。 たとえば、ソース ウィンドウで std:array 定義行の右側をクリックすると、そこにブレークポイントが設定されます。

    std::array にブレークポイントが設定されている WinDbg の [ソース] ウィンドウのスクリーンショット。

  2. [タイム トラベル] メニューで、 [開始するタイム トラベル] コマンドを使用して、トレースの先頭に移動します。

    0:000> !tt 0
    Setting position to the beginning of the trace
    Setting position: 15:0
    (1e5c.710): Break instruction exception - code 80000003 (first/second chance not available)
    Time Travel Position: 15:0
    eax=68e28100 ebx=00000000 ecx=77a266ac edx=69e34afc esi=69e3137c edi=00fa2000
    eip=77a266ac esp=00ddf3b8 ebp=00ddf608 iopl=0         nv up ei pl nz na pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
    ntdll!LdrpInitializeProcess+0x1d1c:
    77a266ac 83bdbcfeffff00  cmp     dword ptr [ebp-144h],0 ss:002b:00ddf4c4=00000000
    
  3. [ホーム] リボンの [移動] をクリックして、ブレークポイントにヒットするまで戻ります。

    Breakpoint 0 hit
    Time Travel Position: 5B:AF
    eax=0000000f ebx=00c20000 ecx=00000000 edx=00000000 esi=013a1046 edi=00effa60
    eip=013a17c1 esp=00eff970 ebp=00effa60 iopl=0         nv up ei pl nz na po nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
    DisplayGreeting!DisplayGreeting+0x41:
    013a17c1 8bf4            mov     esi,esp
    

greeting 変数のアクセス ブレークポイントで中断を設定する

この調査を行うもう 1 つの方法は、疑わしい変数にブレークポイントを設定し、どのコードがそれらの変数を変更しているのかを調べることです。 たとえば、GetCppConGreeting メソッドの greeting 変数にブレークポイントを設定するには、次の手順を使用します。

チュートリアルのこの部分は、前のセクションのブレークポイントにまだいることを前提としています。

  1. [表示][ローカル]の順に選択します。 [ローカル] ウィンドウでは、現在のコンテキストで greeting を使用できるため、そのメモリ位置を特定できます。

  2. dx コマンドを使用して、 greeting 配列を調べます。

    0:000> dx &greeting
    &greeting                 : 0xddf800 [Type: std::array<wchar_t,50> *]
       [+0x000] _Elems           : "꽘棶檙瞝???" [Type: wchar_t [50]]
    

    このトレースでは、 greeting はメモリ内の ddf800 にあります。

  3. [ブレークポイント] ウィンドウで既存のブレークポイントを右クリックし、 [削除] を選択して、既存のブレークポイントを解除します。

  4. 書き込みアクセスを監視するメモリ アドレスを使用して、 ba コマンドでブレークポイントを設定します。

    ba w4 ddf800
    
  5. [タイム トラベル] メニューで、 [開始するタイム トラベル] コマンドを使用して、トレースの先頭に移動します。

    0:000> !tt 0
    Setting position to the beginning of the trace
    Setting position: 15:0
    (1e5c.710): Break instruction exception - code 80000003 (first/second chance not available)
    Time Travel Position: 15:0
    eax=68e28100 ebx=00000000 ecx=77a266ac edx=69e34afc esi=69e3137c edi=00fa2000
    eip=77a266ac esp=00ddf3b8 ebp=00ddf608 iopl=0         nv up ei pl nz na pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
    ntdll!LdrpInitializeProcess+0x1d1c:
    77a266ac 83bdbcfeffff00  cmp     dword ptr [ebp-144h],0 ss:002b:00ddf4c4=00000000
    
  6. [ホーム] メニューで [移動] を選択して、greeting 配列のメモリ アクセスの最初のポイントまで進みます。

    0:000> g-
    Breakpoint 0 hit
    Time Travel Position: 5B:9C
    eax=cccccccc ebx=002b1000 ecx=00000000 edx=68d51a6c esi=013a1046 edi=001bf7d8
    eip=013a1735 esp=001bf6b8 ebp=001bf7d8 iopl=0         nv up ei pl nz na po nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
    DisplayGreeting!GetCppConGreeting+0x25:
    013a1735 c745ec04000000  mov     dword ptr [ebp-14h],4 ss:002b:001bf7c4=cccccccc
    

    または、トレースの末尾まで移動し、コードを遡って、配列のメモリ位置が書き込まれたトレース内の最後のポイントを見つけることもできます。

TTD.Memory オブジェクトを使用してメモリ アクセスを表示する

トレース内のどのポイントでメモリへのアクセスが発生したのかを確認するもう 1 つの方法は、TTD.Memory オブジェクトと dx コマンドを使用することです。

  1. dx コマンドを使用して、 greeting 配列を調べます。

    0:000> dx &greeting
    &greeting                 : 0xddf800 [Type: std::array<wchar_t,50> *]
       [+0x000] _Elems           : "꽘棶檙瞝???" [Type: wchar_t [50]]
    

    このトレースでは、 greeting はメモリ内の ddf800 にあります。

  2. dx コマンドを使用して、読み取り/書き込みアクセスで、そのアドレスから始まるメモリ内の 4 バイトを確認します。

    0:000> dx -r1 @$cursession.TTD.Memory(0xddf800,0xddf804, "rw")
    @$cursession.TTD.Memory(0x1bf7d0,0x1bf7d4, "rw")                
        [0x0]           
        [0x1]           
        [0x2]           
        [0x3]           
        [0x4]           
        [0x5]           
        [0x6]           
        [0x7]           
        [0x8]           
        [0x9]           
        [0xa]           
        ...         
    
  3. 発生したメモリ アクセスのいずれかをクリックして、そのメモリ アクセスに関する詳細情報を表示します。

    0:000> dx -r1 @$cursession.TTD.Memory(0xddf800,0xddf804, "rw")[5]
    @$cursession.TTD.Memory(0xddf800,0xddf804, "rw")[5]                
        EventType        : MemoryAccess
        ThreadId         : 0x710
        UniqueThreadId   : 0x2
        TimeStart        : 27:3C1 [Time Travel]
        TimeEnd          : 27:3C1 [Time Travel]
        AccessType       : Write
        IP               : 0x6900432f
        Address          : 0xddf800
        Size             : 0x4
        Value            : 0xddf818
    
  4. [Time Travel] をクリックして、その時点のトレースの位置を特定します。

    0:000> dx @$cursession.TTD.Memory(0xddf800,0xddf804, "rw")[5].TimeStart.SeekTo()
    @$cursession.TTD.Memory(0xddf800,0xddf804, "rw")[5].TimeStart.SeekTo()
    (1e5c.710): Break instruction exception - code 80000003 (first/second chance not available)
    Time Travel Position: 27:3C1
    eax=00ddf81c ebx=00fa2000 ecx=00ddf818 edx=ffffffff esi=00000000 edi=00b61046
    eip=6900432f esp=00ddf804 ebp=00ddf810 iopl=0         nv up ei pl nz ac po nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000212
    ucrtbased!_register_onexit_function+0xf:
    6900432f 51              push    ecx
    
  5. トレース内の最後に発生した読み取り/書き込みメモリ アクセスに関心がある場合は、リストの最後の項目をクリックするか、dx コマンドの末尾に .Last() 関数を追加します。

    0:000> dx -r1 @$cursession.TTD.Memory(0xddf800,0xddf804, "rw").Last()
    @$cursession.TTD.Memory(0xddf800,0xddf804, "rw").Last()                
        EventType        : MemoryAccess
        ThreadId         : 0x710
        UniqueThreadId   : 0x2
        TimeStart        : 53:100E [Time Travel]
        TimeEnd          : 53:100E [Time Travel]
        AccessType       : Read
        IP               : 0x690338e4
        Address          : 0xddf802
        Size             : 0x2
        Value            : 0x45
    
  6. [Time Travel] をクリックしてトレース内のその位置に移動し、このラボで前述した手法を使用して、その時点でのコードの実行をさらに調べることができます。

TTD.Memory オブジェクトの詳細については、 TTD.Memory オブジェクトに関する記事を参照してください。

まとめ

この非常に小さなサンプルでは、数行のコードを調べることで問題を特定できましたが、大規模なプログラムでは、ここで紹介した手法を使用することで、問題を突き止めるために必要な時間を短縮できます。

トレースが記録されたら、トレースと再現手順を共有することができ、どの PC でもオンデマンドで問題を再現できます。

参照

Time Travel Debugging - 概要

Time Travel Debugging - 記録

Time Travel Debugging - トレースの再生

Time Travel Debugging - トレース ファイルの使用