WinDbg:时间轴

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

时间旅行调试(TTD)允许用户记录执行轨迹,这些轨迹就是程序的执行过程。 时间线是执行期间发生的事件的可视表示形式。 这些事件可以是断点、内存读取/写入、函数调用和返回以及异常的位置。

调试器中时间线的屏幕截图,其中显示了异常、内存访问、断点和函数调用。

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

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

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

时间线类型

时间线 ”窗口显示以下时间线中的事件:

  • 异常:可以筛选特定异常代码。
  • 断点:可以在时间线上看到断点何时命中。
  • 内存访问:可以在两个内存地址之间读取、写入和执行。
  • 函数调用:可以按以下 module!function形式搜索。

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

例外

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

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

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

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

时间线调试器异常对话框的屏幕截图,时间线类型设置为“异常”,“异常代码”设置为“0xC0000004”。

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

断点

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

调试器中时间线的屏幕截图,其中显示大约 30 个断点指示器。

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

函数调用

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

在调试器中添加时间线的屏幕截图,其中输入了函数调用名称。

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

调试器中时间线的屏幕截图,其中显示了函数调用和“注册”窗口。

内存访问

使用 内存访问 时间线查看何时读取或写入了特定内存范围,或者代码执行的位置。 启动和停止地址用于定义两个内存地址之间的范围。

“添加内存访问”对话框的屏幕截图,其中选择了“写入”选项。

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

调试器中显示内存访问事件的日程表的屏幕截图。

使用时间线

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

选择放大和缩小时间线的放大镜图标。

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

调试器中时间线的屏幕截图,其中显示了用于选择活动视图的顶部区域。

鼠标移动

若要放大和缩小,请选择 Ctrl 并使用滚轮。

若要从一侧到另一侧平移,请按住 Shift 并使用滚轮。

时间线调试技术

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

在此方案中,第一步是在时间旅行跟踪中查找异常。 请双击您在时间线上看到的唯一异常。

“命令” 窗口中,可以看到在选择异常时发出了以下命令。

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

选择 “查看>寄存器”以显示时间线中当前的寄存器,从而开始调查。

调试器中时间线的屏幕截图,其中显示了演示实验室异常和“注册”窗口。

在命令输出中,堆栈(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 的调试工作流成为可能。

若要探索替代时间线调试技术,请在时间线中选择异常,并使用单步退回命令在Home功能区上再次向后单步三次。

在此小型示例中,可以轻松地在代码中查找。 如果你有数百行代码和数十个子例程,请使用此处所述的技术来减少查找问题所需的时间。

如前所述,基本指针(esp)指向邮件文本,而不是指向指令。

使用 ba 命令在内存访问时设置断点。 您设置了一个w -写入断点以确定何时将数据写入此内存区域。

0:000> ba w4 003cf7c8

尽管您使用简单的内存访问断点,但可以将断点构建为更加复杂的条件语句。 有关详细信息,请参阅 bp、bu、bm(设置断点)。

“开始 ”菜单上,选择 “返回” 以及时返回,直到命中断点。

此时,可以检查程序堆栈以查看哪些代码处于活动状态。

调试器中显示内存访问时间线和堆栈窗口的屏幕截图。

由于Microsoft提供的 wscpy_s() 函数不太可能出现这种代码 bug,因此请进一步检查调用栈。 堆栈显示 Greeting!main 调用 Greeting!GetCppConGreeting。 在小型代码示例中,此时可以打开代码,并可能轻松找到错误。 但是,为了说明可用于更大、更复杂的程序的技术,请添加函数调用时间线。

函数调用时间线

选择 “+ 添加时间线 ”并填写 DisplayGreeting!GetCppConGreeting 函数搜索字符串。

开始位置和结束位置”复选框指示跟踪中函数调用的开始和结束。

可以使用 dx 命令来显示函数调用对象,以查看与函数调用的起始位置和结束位置相对应的 TimeStartTimeEnd 字段。

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 调用的示例名称。

选择“查看>时间线”以打开“日程表”窗口,以便可以访问书签时间线。 将鼠标悬停在书签上时,将显示书签名称。

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

右键单击书签以移动到书签位置、重命名书签或删除书签。

书签右键单击弹出菜单的屏幕截图,其中显示了前往位置、编辑和删除的选项。

注释

书签功能在 WinDbg 版本 1.2402.24001.0 中不可用。