时间旅行调试(TTD)允许用户记录跟踪,这些跟踪是对程序执行的记录。时间线是执行过程中发生的事件的直观表示,这些事件可以是包括断点,内存读/写,函数调用和返回以及异常。
使用时间线窗口可以快速查看重要事件,了解相对位置并轻松跳转到它们在TTD跟踪文件中的位置,使用多个时间线以可视方式探索时间旅行轨迹中的事件并发现事件相关性。
打开TTD跟踪文件时将显示时间线窗口,并显示关键事件,而无需手动创建数据模型查询。同时,所有时间旅行对象均可用,以允许进行更复杂的数据查询。有关创建和使用时间旅行跟踪文件的更多信息,请点此查看。
时间线类型
时间线窗口可以显示以下事件:
1.异常(你可以进一步过滤特定的异常代码);
2.断点(添加断点时也会自动添加断点的时间线);
3.函数调用(以module!function形式搜索);
4.内存访问(在两个内存地址之间进行读/写/执行);
将鼠标停在每个事件上,通过工具提示获取更多信息。点击事件将运行事件查询并显示更多信息,双击事件将跳至TTD跟踪文件中的该位置。
异常
当你加载跟踪文件并且时间线处于活动状态时,它将自动显示记录中的任何异常。当你将鼠标悬停在断点上时,将显示诸如异常类型和异常代码之类的信息。
你可以使用可选的异常代码字段进一步过滤特定的异常代码:
你还可以为特定的异常类型添加新的时间线。
断点
添加断点后,会将断点的时间线自动添加到时间线。例如,可以使用bp Set Breakpoint命令完成此操作。当你将鼠标悬停在断点上时,将显示地址和与断点关联的指令指针。
清除断点后,关联的断点时间线将自动删除。
函数调用
你可以在时间线上显示函数调用的位置。为此,就要以module!function的形式提供搜索,例如TimelineTestCode!multiplyTwo。你还可以指定通配符,例如TimelineTestCode!m*。
将鼠标悬停在函数上时,将显示函数名称、输入参数及其值和返回值。此示例显示缓冲区和大小,因为这些是DisplayGreeting!GetCppConGreeting的参数。
内存访问
使用内存访问时间线显示何时已读取或写入特定范围的内存,或在何处执行了代码。起始地址和终止地址用于定义两个存储器地址之间的范围。
将鼠标悬停在内存访问项上时,将显示值和指令指针。
使用时间线
当将鼠标悬停在时间线上时,一条垂直的灰色线将跟随鼠标,蓝色竖线表示跟踪中的当前位置,点击放大镜图标可放大和缩小时间线。
在顶部时间线控制区域中,使用矩形来平移时间线的视图。拖动矩形的外部定界符以调整当前时间线视图的大小。
鼠标移动
使用Ctrl +滚轮放大和缩小,使用Shift +滚轮可左右滚动。
时间线调试技术
为了演示调试时间线技术,此处重用了“时间旅行调试演练(Time Travel Debugging Walkthrough)”。本演示假设你已经完成了构建样例代码的前两个步骤,并使用前面描述的前两个步骤创建了TTD记录。
第1部分:构建示例代码;
第二部分:记录“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()
选择查看>>寄存器以在时间线上显示寄存器,开始我们的调查。
在命令输出中,请注意堆栈(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。默认设置是查看所有内存写入,但是你也可以仅查看该地址处的写入或代码执行。
现在,我们可以反向遍历时间线,以检查这段时间在此行的跟踪记录的哪一点被写入,以查看可以找到的内容。点击时间线上的此位置,我们看到本地人为要复制的字符串取不同的值。目标值似乎不完整,好像我们的字符串长度不正确。
断点时间线
使用断点是在某些情况下暂停代码执行的常用方法,TTD允许你设置一个断点并及时返回,直到在记录跟踪之后找到该断点为止。在问题发生后检查过程状态,确定断点的最佳位置的能力启用了TTD特有的其他调试工作流。
要探索替代的时间线调试技术,请点击时间线中的异常,然后使用Home功能区上的“后退一步”命令再次前进三步。
在这个示例中,仅查看代码将非常容易,但是如果有数百行代码和数十个子例程,则可以使用此处描述的技术来减少定位问题所需的时间。
如前所述,基本指针(esp)并非指向指令,而是指向我们的消息文本。
使用ba命令在内存访问上设置断点,我们将设置一个w - 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的调用也与我们的断点以及我们定义的内存访问事件同时发生。因此,似乎我们已经缩小了代码范围,仔细研究了导致应用程序崩溃的根本原因。
通过查看多个时间线来探索代码执行
尽管我们的代码示例很小,但是使用多个时间线的技术可以对时间旅行轨迹进行可视化探索。你可以查看跟踪文件以询问问题,例如“何时在命中断点之前访问内存区域?”。
本文翻译自:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/windbg-timeline-preview