时间旅行调试 - 示例应用演练

带有时钟的时间旅行调试徽标。

本实验室介绍时间旅行调试 (TTD),使用一个存在代码缺陷的小型示例程序。 TTD 用于调试、识别问题及分析其根本原因。 尽管这个小程序中的问题很容易找到,但常规过程可用于更复杂的代码。 这一常规过程可概述如下。

  1. 捕获失败程序的时间旅行跟踪。
  2. 使用 dx(显示调试器对象模型表达式)命令来查找记录中存储的异常事件。
  3. 使用 !tt(时间旅行)命令前往跟踪中异常事件的位置。
  4. 从跟踪中的这一点开始向后退一步,直到有问题的故障代码进入范围。
  5. 在范围内的故障代码中,查看本地值并对可能包含错误值的变量进行假设。
  6. 确定值不正确的变量的内存地址。
  7. 使用 ba(访问时中断)命令在可疑变量的地址上设置内存访问 (ba) 断点。
  8. 使用 g- 返回可疑变量的最后一个内存访问点。
  9. 查看该位置或之前的几条指令是否是代码缺陷的关键所在。 如果是这样,则已完成。 如果错误值来自其他变量,则在第二个变量的访问断点上设置另一个断点。
  10. 使用 g- 返回第二个可疑变量的最后一个内存访问点。 查看该位置或之前的几条指令是否包含代码缺陷。 如果是这样,则已完成。
  11. 重复此过程,直到找到设置了导致错误的错误值的代码。

虽然此过程中描述的一般技术适用于广泛的代码问题,但有些独特的代码问题需要采用独特的方法。 演练中演示的技术应有助于扩展调试工具集,并将说明使用 TTD 跟踪可以实现的一些功能。

实验室目标

完成本实验室后,将能够将常规过程与时间旅行跟踪一起使用,以便查找代码中的问题。

实验室设置

要完成实验室内容,需要使用以下硬件。

  • 运行 Windows 10 或 Windows 11 的笔记本电脑或台式计算机(主机)

需要以下软件才能完成实验室内容。

  • WinDbg。 有关安装 WinDbg 的信息,请参阅 WinDbg - 安装
  • Visual Studio,用于构建示例 C++ 代码。

此实验室包含以下三个部分。

第 1 部分:生成示例代码

在第 1 部分中,将使用 Visual Studio 构建示例代码。

在 Visual Studio 中创建示例应用

  1. 在 Microsoft Visual Studio 中,单击文件>新建>项目/解决方案...,然后单击 Visual C++ 模板。

    选择 Win32 控制台应用程序。

    提供 DisplayGreeting 的项目名称,然后单击确定

  2. 取消选中安全开发生命周期 (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++代码生成

    设置以下属性。

    设置 “值”
    安全检查 禁用安全检查 (/GS-)
    基本运行时检查 默认

    注意

    虽然不建议使用这些设置,但可以想象有人会建议使用这些设置来加快编码或方便某些测试环境。

  6. 在 Visual Studio 中,单击构建>构建解决方案

    如果一切顺利,创建窗口将会显示一条消息,表明创建成功。

  7. 找到生成的示例应用文件

    在解决方案资源管理器中,右键单击 DisplayGreeting 项目,然后选择在文件资源管理器中打开文件夹

    导航到包含示例的已编译 exe 和符号 pdb 文件的 Debug 文件夹。 例如,如果这是存储项目的文件夹,则导航到 C:\Projects\DisplayGreeting\Debug

  8. 运行具有代码缺陷的示例应用

    双击 exe 文件以运行示例应用。

    运行 DisplayGreeting.exe 文件的控制台屏幕截图。

    如果出现此对话框,请选择关闭程序

    显示“DisplayGreeting.exe 已停止工作”的对话框的屏幕截图。

    在本演练的下一部分中,我们将记录示例应用的执行情况,看一看是否可以确定发生此异常的原因。

第 2 部分:记录“DisplayGreeting”示例的跟踪

在第 2 部分中,将记录示例“DisplayGreeting”应用的不当行为跟踪

要启动示例应用并记录 TTD 跟踪,请执行以下步骤。 有关记录 TTD 跟踪的常规信息,请参阅时间旅行调试 - 记录跟踪

  1. 以管理员身份运行 WinDbg,以便能够记录时间旅行跟踪。

  2. 在 WinDbg 中,选择文件>开始调试>启动可执行文件(高级)

  3. 输入要记录的用户模式可执行文件的路径,或选择浏览导航到可执行文件。 有关在 WinDbg 中使用“启动可执行文件”菜单的信息,请参阅 WinDbg - 启动用户模式会话

    WinDbg 的屏幕截图,其中“启动可执行文件(高级)”屏幕中带有“记录时间旅行调试”复选框。

  4. 选中用时间旅行调试记录进程框,从而在启动可执行文件时记录跟踪。

  5. 单击配置和记录开始记录。

  6. 出现“配置记录”对话框时,单击记录以启动可执行文件并开始记录。

    WinDbg 的屏幕截图,显示路径设置为 temp 的“配置记录”对话框。

  7. 将显示记录对话框,指示正在记录跟踪。 之后不久,应用程序会崩溃。

  8. 单击关闭程序,关闭“DisplayGreeting 已停止工作”对话框。

    显示 DisplayGreeting 应用已停止工作的对话框。

  9. 当程序崩溃时,跟踪文件将被关闭并写入磁盘。

    WinDbg 输出的屏幕截图,其中显示了已编制索引的 1/1 关键帧。

  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.
    

注意

关键帧是用于编制索引的跟踪中的位置。 自动生成关键帧。 较大的跟踪将包含更多关键帧。

  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
    ...
    
    

    注意

    在本演练中,使用了三个句点来表示已删除多余的输出。

  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. 单击“异常”字段以进一步深入查看异常数据。

    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. 单击异常事件中的 [时间旅行] 链接以移动到跟踪中的该位置。

    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 ??
    

    值得注意的是,堆栈和基指针指向的是两个完全不同的地址。

    esp=00effe4c ebp=00520055
    

    这可能表示堆栈损坏 - 可能是函数返回后损坏了堆栈。 为了验证这一点,我们需要回到 CPU 状态损坏之前,看看能否确定堆栈损坏发生的时间。

检查局部变量并设置代码断点

在跟踪故障点时,通常会在错误处理代码的真正原因之后几步结束。 通过时间旅行,我们可以一次返回指令,找到真正的根本原因。

  1. 主页功能区使用单步回退命令来回退三条指令。 在此过程中,请继续检查堆栈和内存窗口。

    指令窗口将显示时间旅行位置和后退三个指令时的寄存器。

    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:
    

    注意

    在本演练中,命令输出显示了可替代 UI 菜单选项的命令,以便习惯使用命令行的用户使用命令行命令。

  2. 在跟踪的这一点上,我们的堆栈和基指针的值更有意义,因此我们似乎越来越接近代码中发生损坏的点。

    esp=00effd94 ebp=00effe44
    

    同样值得注意的是,局部变量窗口包含了目标应用中的值,而源代码窗口则突出显示了跟踪中此时准备执行的代码行。

    WinDbg 的屏幕截图,其中显示了包含内存 ASCII 输出和源代码窗口的“局部变量”窗口。

  3. 为了进一步调查,我们可以打开内存窗口,查看 0x00effe44 基指针内存地址附近的内容。

  4. 要显示关联的 ASCII 字符,请在“内存”功能区中选择文本,然后选择ASCII

    显示内存 ASCII 输出和源代码窗口的 WinDbg 预览屏幕截图。

  5. 基指针不再指向指令,而是指向消息文本。 因此,这里有些问题,这可能接近我们损坏堆栈的时间点。 为了进一步调查,我们将设置断点。

注意

在这个非常小的示例中很简单,只用在代码中查找就可以了,但要是有数百行代码和数十个子例程,则可以使用此处介绍的技术来减少查找问题所需的时间。

TTD 和断点

使用断点是一种常见方法,用于在感兴趣的某个事件中暂停代码执行。 TTD 可以设置断点并及时返回,直到记录跟踪后命中该断点。 在问题发生后检查进程状态,以确定断点的最佳位置,这一功能可实现 TTD 独有的额外调试工作流程。

内存访问断点

可以设置在访问内存位置时触发的断点。 使用 ba(访问时中断)命令,语法如下。

ba <access> <size> <address> {options}
选项 说明

e

execute(当 CPU 从地址中提取指令时)

r

read/write(当 CPU 读取或写入地址时)

w

write(当 CPU 写入地址时)

请注意,任何时候都只能设置四个数据断点,并且必须确保正确对齐数据,否则将无法触发断点(word 必须以可被 2 整除的地址结尾,dword 必须可被 4 整除,quadword 必须可被 0 或 8 整除)。

为基指针设置内存访问断点上的中断

  1. 在跟踪的这一点上,我们希望在写入内存访问基指针 - ebp(在本示例中为 00effe44)时设置一个断点。 为此,请运用使用要监控的地址的 ba 命令。 我们希望监控四个字节的写入,因此指定 w4。

    0:000> ba w4 00effe44
    
  2. 选择视图,然后选择断点以确认它们已按预期进行设置。

    显示单个断点的 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. 选择视图,然后选择局部变量。 在局部变量窗口中,我们可以看到目标变量只包含消息的一部分,而包含所有文本。 这些信息支持了堆栈已损坏的观点。

    显示“局部变量”窗口的 WinDbg 的屏幕截图。

  5. 此时,我们可以检查程序堆栈以查看哪些代码处于活动状态。 在视图功能区中选择堆栈

    显示“堆栈”窗口的 WinDbg 的屏幕截图。

由于 Microsoft 提供的 wscpy_s() 函数不太可能存在这样的代码错误,因此我们进一步查看堆栈。 堆栈显示 Greeting!main 调用 Greeting!GetCppConGreeting。 在这个的非常小的代码示例中,我们此时只用打开代码,就可能很容易找到错误。 但为了说明可用于更大、更复杂程序的技术,我们将设置一个新的断点来进一步研究。

为 GetCppConGreeting 函数设置访问断点上的断点

  1. 使用断点窗口通过右键单击现有断点并选择删除来清除现有断点。

  2. 使用 dx 命令确定 isplayGreeting!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. 确认硬件读取断点在断点窗口中处于活动状态。

    显示了单个硬件读取断点的 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. 在“主页”菜单中,选择后退一步或使用g-u命令后退一步。

    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. 看来我们找到了根本原因。 我们声明的问候数组长度为 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. 修复代码的一种方法是将字符数组的大小扩展到 100。

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

    我们还需要在此代码行中将 sizeof(greeting) 更改为 size(greeting)。

     GetCppConGreeting(greeting.data(), size(greeting));
    
  11. 要验证这些修补程序,我们可以重新编译代码,并确认代码运行时没有错误。

使用源窗口设置断点

  1. 执行此调查的另一种方法是通过单击任何代码行来设置断点。 例如,单击源窗口中 std:array 定义行的右侧将设置一个断点。

    WinDbg 中源窗口的屏幕截图,其中设置了 std::array 上的断点。

  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
    

问候变量设置访问断点上的断点

执行此调查的另一种替代方法是对可疑变量设置断点,并检查哪些代码正在改变它们。 例如,要在 GetCppConGreeting 方法中对问候变量设置断点,请使用此过程。

本演练的这一部分假定仍位于上一部分的断点处。

  1. 来自视图局部变量。 在局部变量窗口中,问候语在当前上下文中可用,因此我们能够确定其内存位置。

  2. 使用 dx 命令检查问候语数组。

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

    在此跟踪中,问候语位于 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. 在“主页”菜单上,选择转到以前进到问候数组的第一个内存访问点。

    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 对象查看内存访问情况

另一种确定跟踪内存中已访问的点的方法是使用 TTD.Memory 对象和 dx 命令。

  1. 使用 dx 命令检查问候语数组。

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

    在此跟踪中,问候语位于 ddf800 的内存中。

  2. 使用 dx 命令查看内存中的四个字节,从具有读取写入访问权限的地址开始。

    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. 单击 [时间旅行] 将跟踪定位在时间点。

    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. 如果对跟踪中最后出现的读/写内存访问感兴趣,可以单击列表中的最后一项或追加 .Last() 函数到 dx 命令的末尾。

    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. 然后,我们可以单击 [时间旅行] 移动到跟踪中的该位置,并使用本实验室前面所述的技术进一步查看该点的代码执行。

有关 TTD.Memory 对象的详细信息,请参阅 TTD.Memory 对象

总结

在此非常小的示例中,问题可以通过查看几行代码来确定,但在较大的程序中,此处提供的技术可用于减少查找问题所需的时间。

记录跟踪后,可以共享跟踪和重现步骤,并且在任何电脑上可按需重现问题。

另请参阅

时间旅行调试 - 概述

时间旅行调试 - 记录

时间旅行调试 - 重放跟踪

时间旅行调试 - 使用跟踪文件