WinDbg - 时间线

带有放大镜检查位的 WinDbg 徽标。

时间旅行调试 (TTD) 允许用户记录跟踪,即程序执行的记录。 时间线是执行过程中发生的事件的可视化表示。 这些事件的位置包括:断点、内存读取/写入、函数调用和返回以及异常。

调试器中显示异常、内存访问、断点和函数调用的时间线。

使用时间线窗口快速查看重要事件,了解相对位置,并轻松跳转到其在 TTD 跟踪文件中的位置。 使用多个时间线直观地浏览时间旅行跟踪中的事件并发现事件的相关性。

打开 TTD 跟踪文件时会显示时间线窗口,该窗口可显示关键事件,而无需手动创建数据模型查询。 同时,所有时间旅行对象都可用于更复杂的数据查询。

有关创建和使用时间旅行跟踪文件的详细信息,请参阅时间旅行调试 - 概述

时间线的类型

时间线窗口可以显示以下事件:

  • 异常(可以进一步筛选特定异常代码)
  • 断点
  • 函数调用(以 module!function 形式搜索)
  • 内存访问(读取/写入/在两个内存地址之间执行)

将鼠标悬停在每个事件上,通过工具提示获取详细信息。 单击某个事件将运行事件查询并显示详细信息。 双击事件将跳转到 TTD 跟踪文件中的位置。

异常

在加载跟踪文件且时间线处于活动状态时,它会自动显示记录中的任何异常。

将鼠标悬停在断点上时,会显示异常类型和异常代码等信息。

调试器中显示异常的时间线,其中包含有关特定异常代码的信息。

可以使用可选异常代码字段进一步筛选特定异常代码。

时间线调试器异常对话框,其中时间线类型设置为异常,异常代码设置为 0xC0000004。

还可以为特定异常类型添加新时间线。

断点

添加断点后,可以在时间线上显示该断点的命中位置。 例如,可以使用 bp Set 断点命令完成此操作。 将鼠标悬停在断点上时,将显示与断点关联的地址和指令指针。

调试器中的时间线显示大约 30 个断点指示器。

清除断点后,关联的断点时间线会自动删除。

函数调用

可以在时间线上显示函数调用的位置。 为此,请以 module!function 搜索形式提供搜索,例如 TimelineTestCode!multiplyTwo。 还可以指定通配符,例如TimelineTestCode!m*

在调试器中添加时间线并输入函数调用名称。

将鼠标悬停在函数调用上时,将显示函数名称、输入参数、它们的值和返回值。 此示例显示 缓冲区大小,因为这些是 DisplayGreeting!GetCppConGreeting 的参数。

调试器中显示函数调用和注册窗口的时间线。

内存访问

使用内存访问时间线显示读取或写入特定内存范围的时间或执行代码的位置。 开始和停止地址用于定义两个内存地址之间的范围。

添加时间线内存访问对话框,并选择“写入”按钮。

将鼠标悬停在内存访问项上时,将显示值和指令指针。

显示内存访问事件的调试器中的时间线。

使用时间线

将鼠标悬停在时间线上时,垂直灰线会跟随光标移动。 垂直蓝线指示跟踪中的当前位置。

单击放大镜图标来缩放时间线。

在顶部时间线控件区域中,使用矩形平移时间线视图。 拖动矩形的外部分隔符以调整当前时间线视图的大小。

调试器中的时间线显示用于选择活动视区的顶部区域。

鼠标移动

使用 Ctrl + 滚轮进行缩放。

使用 Shift + 滚轮从从一侧平移到另一侧。

时间线调试技术

为了演示调试时间线技术,此处将重复使用时间旅行调试演练。 本演示假定已完成构建示例代码的前两个步骤,并使用其中所述的前两个步骤创建了 TTD 记录。

第 1 部分:生成示例代码

第 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()

选择查看>>寄存器以显示时间线中此时的寄存器,以开始我们的调查。

调试器中的时间线显示 demolab 异常和寄存器窗口。

在命令输出中,请注意堆栈 (esp) 和基指针 (ebp) 指向两个完全不同的地址。 这可能表示堆栈损坏 - 可能是函数返回后损坏了堆栈。 为了验证这一点,我们需要回到 CPU 状态损坏之前,看看能否确定堆栈损坏发生的时间。

正如我们所做的那样,我们将检查局部变量和堆栈的值。

选择查看>>局部变量以显示本地值。

选择视图>>堆栈以显示代码执行堆栈。

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

主页功能区使用单步回退命令来回退三条指令。 执行此操作时,请继续检查堆栈、局部变量和寄存器窗口。

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

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。 选择内存>>文本>>ASCII以显示存储在该地址中的 ASCII 文本。

显示寄存器、堆栈和内存窗口的调试器。

内存访问时间线

识别感兴趣的内存位置后,请使用该值添加内存访问时间线。 单击+ 添加时间线并填写起始地址。 我们将查看 4 个字节,因此将其添加到 003cf7c8 的起始地址,我们得到 003cf7cb。 默认值是查看所有内存写入,但也可以只查看该地址的写入或代码执行。

添加时间线内存访问对话框,选择写入按钮且起始值为 003cf7c8。

现在,我们可以反向遍历时间线,以检查该内存位置在时间旅行跟踪的哪个时间点被写入,查看可以发现什么。 单击时间线中的此位置可以看到,局部变量值与被复制字符串的值不同。 目标值似乎不完整,好像字符串长度不正确。

内存访问时间线和局部变量窗口显示具有不同源值和目标值的局部变量值。

断点时间线

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

要探索替代时间线调试技术,请单击时间线中的异常,并使用主页功能区上的单步回退命令再次后退三步。

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

如前所述,基本指针 (esp) 不是指向指令,而是指向我们的消息文本。

使用 ba 命令在内存访问时设置断点。 我们将设置一个 w - 写入断点,以查看何时写入此内存区域。

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  

必须选中“开始”或“结束”位置框,或同时选中“开始”和“结束”位置框。

添加新的“时间线”对话框,显示新增函数调用时间线,函数搜索字符串为 DisplayGreeting!GetCppConGreeting。

由于我们的代码既不是递归的也不是可重新进入的,因此很容易在时间线中找到 GetCppConGreeting 方法的调用时间。 对 GetCppConGreeting 的调用也与断点以及我们定义的内存访问事件同时发生。 这样看来,我们已经缩小了代码区域,可以仔细查看应用程序崩溃的根本原因。

调试器中的时间线显示内存访问时间线和局部变量窗口,其中包含具有不同字符串值的消息和缓冲区。

通过查看多个时间线浏览代码执行

虽然我们的代码示例很小,但使用多个时间线的技术允许对时间旅行跟踪进行可视化探索。 可以查看跟踪文件来提问,例如“何时在命中断点之前访问内存区域?”

调试器中显示内存访问时间线和局部变量窗口的时间线。

时间线工具能够查看更多相关性并发现意料之外的东西,这与使用命令行命令与时间旅行跟踪行交互有所不同。

时间线书签

在 WinDbg 中为重要的时间旅行位置添加书签,而不是手动将位置复制粘贴到记事本。 通过书签可以更轻松地查看跟踪中相对于其他事件的不同位置,并对其进行注释。

可以为书签提供描述性名称。

显示“显示问候语”应用中首歌 API 调用的示例名称的新书签对话框。

通过查看>时间线中提供的时间线窗口访问书签。 将鼠标悬停在书签上时,将显示书签名称。

显示三个书签的时间线,光标悬停在一个书签上时,显示书签名称。

可以右键单击书签以移动到该位置,也可以重命名或删除书签。

书签右键单击弹出菜单,显示用于移动定位、编辑和删除的选项。

注意

在调试器 1.2402.24001.0 版本中,书签功能不可用。

另请参阅

WinDbg 功能

时间旅行调试演练