时间旅行调试 - JavaScript 自动化
可以使用 JavaScript 自动化以多种方式使用 TTD 跟踪,例如命令自动化或使用查询从跟踪文件查找事件数据。
有关使用 JavaScript 的一般信息,请参阅 JavaScript 调试器脚本。 此外,还有 JavaScript 调试器示例脚本。
JavaScript TTD 命令自动化
使用 JavaScript 实现 TTD 自动化的一种方法是发送命令以自动处理时间旅行跟踪文件。
在跟踪文件中移动
此 JavaScript 演示如何使用 !tt 命令移动到时间旅行跟踪的开始。
var dbgControl = host.namespace.Debugger.Utility.Control;
dbgControl.ExecuteCommand("!tt 0",false);
host.diagnostics.debugLog(">>> Sent command to move to the start of the TTD file \n");
我们可以将其转换为 ResetTrace 函数,并使用 WinDbg 中的 JavaScript UI 将其另存为MyTraceUtils.js。
// My Trace Utils
// WinDbg TTD JavaScript MyTraceUtilsCmd Sample
"use strict";
function MyTraceUtilsCmd()
{
var dbgControl = host.namespace.Debugger.Utility.Control;
dbgControl.ExecuteCommand("!tt 0",false);
host.diagnostics.debugLog(">>> Sent command to move to the start of the TTD file \n");
}
在 WinDbg 中加载 TTD 文件后,在调试器命令窗口中使用 dx 命令调用函数 ResetTraceCmd() 函数。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.ResetTraceCmd()
>>> Sent command to move to the start of the TTD file
Debugger.State.Scripts.MyTraceUtils.Contents.ResetTraceCmd()
发送命令的限制
但对于除最简单情况外的所有情况,发送命令的方法都有缺点。 它依赖于文本输出的使用。 分析该输出会导致代码变得脆弱且难以维护。 更好的方法是直接使用 TTD 对象。
以下示例演示如何直接使用对象来直接使用对象完成相同的任务。
// My Trace Utils
// WinDbg TTD JavaScript ResetTrace Sample
"use strict";
function ResetTrace()
{
host.currentProcess.TTD.SetPosition(0);
host.diagnostics.debugLog(">>> Set position to the start of the TTD file \n");
}
运行此代码表明,我们能够移动到跟踪文件的开头。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.ResetTrace()
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: F:0
>>> Set position to the start of the TTD file
在此示例中,ResetTraceEnd 函数将位置设置为跟踪的末尾,并使用 currentThread.TTD Position 对象显示当前位置和新位置。
// WinDbg TTD JavaScript Sample to Reset Trace using objects directly
// and display current and new position
function ResetTraceEnd()
{
var PositionOutputStart = host.currentThread.TTD.Position;
host.diagnostics.debugLog(">>> Current position in trace file: "+ PositionOutputStart +"\n");
host.currentProcess.TTD.SetPosition(100);
var PositionOutputNew = host.currentThread.TTD.Position;
host.diagnostics.debugLog(">>> New position in trace file: "+ PositionOutputNew +"\n");
}
运行此代码将显示当前位置和新位置。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.ResetTraceEnd()
>>> Current position in trace file: F:0
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: D3:1
>>> New position in trace file: D3:1
在此扩展示例中,比较开始位置值与结束位置值,以查看跟踪中的位置是否发生了变化。
// WinDbg TTD JavaScript ResetTraceEx Sample
"use strict";
function ResetTraceEx()
{
const PositionOutputStart = host.currentThread.TTD.Position;
host.diagnostics.debugLog(">>> Current position in trace file: "+ PositionOutputStart +"\n");
host.currentProcess.TTD.SetPosition(0);
const PositionOutputNew = host.currentThread.TTD.Position;
host.diagnostics.debugLog(">>> New position in trace file: "+ PositionOutputNew +"\n");
if (parseInt(PositionOutputStart,16) != parseInt(PositionOutputNew,16))
{
host.diagnostics.debugLog(">>> Set position to the start of the TTD file \n");
}
else
{
host.diagnostics.debugLog(">>> Position was already set to the start of the TTD file \n");
}
}
在此示例运行中,显示了一条消息,表明我们在跟踪文件的开头已准备就绪。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.ResetTraceEx()
>>> Current position in trace file: F:0
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: F:0
>>> New position in trace file: F:0
>>> Position was already set to the start of the TTD file
若要测试脚本,请使用 !tt 命令在跟踪文件中导航一半。
0:000> !tt 50
Setting position to 50% into the trace
Setting position: 71:0
现在运行脚本会显示正确的消息,指示位置已设置为 TTD 跟踪的开头。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.ResetTraceEx()
>>> Current position in trace file: 71:0
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: F:0
>>> New position in trace file: F:0
>>> Set position to the start of the TTD file
为时间旅行跟踪文件编制索引
如果只是将跟踪文件复制到另一台电脑,则需要重新编制索引。 有关详细信息,请参阅按时间顺序查看调试 - 使用跟踪文件。
此代码显示了一个示例 IndexTrace 函数,该函数显示重新为跟踪文件重新编制索引所需的时间。
function IndexTrace()
{
var timeS = (new Date()).getTime();
var output = host.currentProcess.TTD.Index.ForceBuildIndex();
var timeE = (new Date()).getTime();
host.diagnostics.debugLog("\n>>> Trace was indexed in " + (timeE - timeS) + " ms\n");
}
这是一个小跟踪文件的输出。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.IndexTrace()
>>> Trace was indexed in 2 ms
添加 try catch 语句
若要检查在运行索引时是否引发错误,请将索引代码括在 try catch 语句中。
function IndexTraceTry()
{
var timeS = (new Date()).getTime();
try
{
var IndexOutput = host.currentProcess.TTD.Index.ForceBuildIndex();
host.diagnostics.debugLog("\n>>> Index Return Value: " + IndexOutput + "\n");
var timeE = (new Date()).getTime();
host.diagnostics.debugLog("\n>>> Trace was successfully indexed in " + (timeE - timeS) + " ms\n");
}
catch(err)
{
host.diagnostics.debugLog("\n>>> Index Failed! \n");
host.diagnostics.debugLog("\n>>> Index Return Value: " + IndexOutput + "\n");
host.diagnostics.debugLog("\n>>> Returned error: " + err.name + "\n");
}
}
如果索引成功,则下面是脚本输出。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.IndexTraceTry()
>>> Index Return Value: Loaded
>>> Trace was successfully indexed in 1 ms
如果无法为跟踪编制索引,例如,如果未在调试器中加载跟踪,则将运行 catch 循环代码。
0:007> dx Debugger.State.Scripts.MyTraceUtils.Contents.IndexTraceTry()
>>> Index Failed!
>>> Index Return Value: undefined
>>> Returned error: TypeError
JavaScript TTD 对象查询
JavaScript 和 TTD 的更高级用法是查询时间旅行对象,以查找跟踪中发生的特定调用或事件。 有关 TTD 对象的详细信息,请参阅:
JavaScript 扩展中的本机调试器对象 - 调试器对象详细信息
dx 命令显示调试器数据模型中的信息,并支持使用 LINQ 语法的查询。 Dx 对于实时查询对象非常有用。 这样就可以对所需的查询进行原型设计,然后使用 JavaScript 实现自动化。 dx 命令提供选项卡补全功能,这在浏览对象模型时非常有用。 有关使用 LINQ 查询和调试器对象的常规信息,请参阅将 LINQ 与调试器对象结合使用。
此 dx 命令计算对某个 API 的所有调用,在本例中为 GetLastError。
0:000> dx @$cursession.TTD.Calls("kernelbase!GetLastError").Count()
@$cursession.TTD.Calls("kernelbase! GetLastError").Count() : 0x12
此命令查看整个时间旅行跟踪,以查看何时调用了 GetLastError。
0:000> dx @$cursession.TTD.Calls("kernelbase!GetLastError").Where(c => c.ReturnValue != 0)
@$cursession.TTD.Calls("kernelbase!GetLastError").Where(c => c.ReturnValue != 0)
[0x0]
[0x1]
[0x2]
[0x3]
TTD.Calls Object 的字符串比较以查找调用
此示例命令演示如何使用字符串比较来查找特定调用。 在本例中,查询在 CreateFileW 函数的 lpFileName 参数中查找字符串“OLE”。
dx -r2 @$cursession.TTD.Calls("kernelbase!CreateFileW").Where(x => x.Parameters.lpFileName.ToDisplayString("su").Contains("OLE"))
添加 .Select 语句以输出 Timestart 和 lpFileName 参数的值。
dx -r2 @$cursession.TTD.Calls("kernelbase!CreateFileW").Where(x => x.Parameters.lpFileName.ToDisplayString("su").Contains("OLE")).Select(x => new { TimeStart = x.TimeStart, lpFileName = x.Parameters.lpFileName })
如果找到包含目标信息的 TTD.Calls 对象,则生成此输出。
[0x0]
TimeStart : 6E37:590
lpFileName : 0x346a78be90 : "C:\WINDOWS\SYSTEM32\OLEACCRC.DLL" [Type: wchar_t *]
显示跟踪中的调用数
使用 dx 命令浏览想要处理的对象后,可以使用 JavaScript 自动使用它们。 在此简单示例中,TTD.Calls 对象用于计算对 kernelbase!GetLastError 的调用。
function CountLastErrorCalls()
{
var LastErrorCalls = host.currentSession.TTD.Calls("kernelbase!GetLastError");
host.diagnostics.debugLog(">>> GetLastError calls in this TTD recording: " + LastErrorCalls.Count() +" \n");
}
将脚本保存在 TTDUtils.js 文件中,并使用 dx 命令调用它以显示跟踪文件中 kernelbase!GetLastError 的计数。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.CountLastErrorCalls()
>>> GetLastError calls in this TTD recording: 18
显示堆栈中的帧
若要显示堆栈中的帧,请使用数组。
function DisplayStack()
{
// Create an array of stack frames in the current thread
const Frames = Array.from(host.currentThread.Stack.Frames);
host.diagnostics.debugLog(">>> Printing stack \n");
// Print out all of the frame entries in the array
for(const [Idx, Frame] of Frames.entries())
{
host.diagnostics.debugLog(">>> Stack Entry -> " + Idx + ": "+ Frame + " \n");
}
}
在此示例跟踪中,显示了一个堆栈条目。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.DisplayStack()
>>> Printing stack
>>> Stack Entry -> 0: ntdll!LdrInitializeThunk + 0x21
查找事件并显示堆栈
在这段代码中,定位了所有异常事件,并使用循环移动到每个异常事件。 然后,TTD 线程对象的 currentThread.ID 会被用于显示线程 ID,而 currentThread.Stack 会被用于显示堆栈中的所有帧。
function HardwareExceptionDisplayStack()
{
var exceptionEvents = host.currentProcess.TTD.Events.Where(t => t.Type == "Exception");
for (var curEvent of exceptionEvents)
{
// Move to the current event position
curEvent.Position.SeekTo();
host.diagnostics.debugLog(">>> The Thread ID (TID) is : " + host.currentThread.Id + "\n");
// Create an array of stack frames in the current thread
const Frames = Array.from(host.currentThread.Stack.Frames);
host.diagnostics.debugLog(">>> Printing stack \n");
// Print out all of the frame entries in the array
for(const [Idx, Frame] of Frames.entries()) {
host.diagnostics.debugLog(">>> Stack Entry -> " + Idx + ": "+ Frame + " \n");
}
host.diagnostics.debugLog("\n");
}
}
输出显示了异常事件、TID 和堆栈帧的位置。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.HardwareExceptionDisplayStack()
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 91:0
>>> The Thread ID (TID) is : 5260
>>> Printing stack
>>> Stack Entry -> 0: 0x540020
>>> Stack Entry -> 1: 0x4d0049
>>> Stack Entry -> 2: DisplayGreeting!__CheckForDebuggerJustMyCode + 0x16d
>>> Stack Entry -> 3: DisplayGreeting!mainCRTStartup + 0x8
>>> Stack Entry -> 4: KERNEL32!BaseThreadInitThunk + 0x19
>>> Stack Entry -> 5: ntdll!__RtlUserThreadStart + 0x2f
>>> Stack Entry -> 6: ntdll!_RtlUserThreadStart + 0x1b
查找事件并发送两个命令
可以根据需要将查询 TTD 对象和发送命令结合起来。 此示例在 ThreadCreated 类型的 TTD 跟踪中定位每个事件,移动到该位置,并发送 ~ Thread Status 和 !runaway 命令显示线程状态。
function ThreadCreateThreadStatus()
{
var threadEvents = host.currentProcess.TTD.Events.Where(t => t.Type == "ThreadCreated");
for (var curEvent of threadEvents)
{
// Move to the current event position
curEvent.Position.SeekTo();
// Display Information about threads
host.namespace.Debugger.Utility.Control.ExecuteCommand("~", false);
host.namespace.Debugger.Utility.Control.ExecuteCommand("!runaway 7", false);
}
}
运行代码将显示异常发生时的线程状态。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.ThreadCreateThreadStatus()
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: F:0
. 0 Id: 948.148c Suspend: 4096 Teb: 00a33000 Unfrozen
User Mode Time
Thread Time
0:148c 0 days 0:00:00.000
Kernel Mode Time
Thread Time
0:148c 0 days 0:00:00.000
Elapsed Time
Thread Time
0:148c 3474 days 2:27:43.000
将实用工具函数链接在一起
在最后这个示例中,我们可以调用前面创建的实用工具函数。 首先,使用 IndexTraceTry 为跟踪编制索引,然后调用 ThreadCreateThreadStatus。 然后,使用 ResetTrace 移动到跟踪的开头,最后调用 HardwareExceptionDisplayStack。
function ProcessTTDFiles()
{
try
{
IndexTraceTry()
ThreadCreateThreadStatus()
ResetTrace()
HardwareExceptionDisplayStack()
}
catch(err)
{
host.diagnostics.debugLog("\n >>> Processing of TTD file failed \n");
}
}
在包含硬件异常的跟踪文件上运行此脚本,将生成此输出。
0:000> dx Debugger.State.Scripts.MyTraceUtils.Contents.ProcessTTDFiles()
>>> Index Return Value: Loaded
>>> Trace was successfully indexed in 0 ms
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: F:0
. 0 Id: 948.148c Suspend: 4096 Teb: 00a33000 Unfrozen
User Mode Time
Thread Time
0:148c 0 days 0:00:00.000
Kernel Mode Time
Thread Time
0:148c 0 days 0:00:00.000
Elapsed Time
Thread Time
0:148c 3474 days 2:27:43.000
>>> Printing stack
>>> Stack Entry -> 0: ntdll!LdrInitializeThunk
>>> Current position in trace file: F:0
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: F:0
>>> New position in trace file: F:0
(948.148c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 91:0
>>> The Thread ID (TID) is : 5260
>>> Printing stack
>>> Stack Entry -> 0: 0x540020
>>> Stack Entry -> 1: 0x4d0049
>>> Stack Entry -> 2: DisplayGreeting!__CheckForDebuggerJustMyCode + 0x16d
>>> Stack Entry -> 3: DisplayGreeting!mainCRTStartup + 0x8
>>> Stack Entry -> 4: KERNEL32!BaseThreadInitThunk + 0x19
>>> Stack Entry -> 5: ntdll!__RtlUserThreadStart + 0x2f
>>> Stack Entry -> 6: ntdll!_RtlUserThreadStart + 0x1b