JavaScript 调试器脚本

本主题介绍如何使用 JavaScript 创建理解调试器对象的脚本,以及如何扩展和自定义调试器的功能。

JavaScript 调试器脚本概述

脚本提供程序将脚本语言桥接至调试器的内部对象模型。 JavaScript 调试器脚本提供程序,允许将 JavaScript 与调试器一起使用。

通过 .scriptload 命令加载 JavaScript 时,将执行脚本的根代码,脚本中存在的名称会桥接到调试器(dx 调试器)的根命名空间中,并且脚本将一直驻留在内存中,直到卸载该脚本并释放对其对象的所有引用。 该脚本可以为调试器的表达式计算器提供新函数、修改调试器的对象模型,也可以像 NatVis 可视化工具那样充当可视化工具。

本主题介绍使用 JavaScript 调试器脚本可以执行的一些操作。

这两个主题提供有关在调试器中使用 JavaScript 的其他信息。

JavaScript 调试器示例脚本

JavaScript 扩展中的本机对象

JavaScript 脚本视频

碎片整理工具 #170 - Andy 和 Bill 演示调试器中的 JavaScript 扩展性和脚本功能。

调试器 JavaScript 提供程序

调试器随附的 JavaScript 提供程序充分利用了最新的 ECMAScript6 对象和类增强功能。 有关详细信息,请参阅 ECMAScript 6 — 新功能:概述和比较

JsProvider.dll

JsProvider.dll 是为支持 JavaScript 调试器脚本而加载的 JavaScript 提供程序。

要求

JavaScript 调试器脚本设计用于所有受支持的 Windows 版本。

加载 JavaScript 脚本提供程序

在使用任何 .script 命令之前,需要加载脚本提供程序。 使用 .scriptproviders 命令确认 JavaScript 提供程序已加载。

0:000> .scriptproviders
Available Script Providers:
    NatVis (extension '.NatVis')
    JavaScript (extension '.js')

JavaScript 脚本元命令

以下命令可用于使用 JavaScript 调试器脚本。

要求

在使用任何 .script 命令之前,需要加载脚本提供程序。 使用 .scriptproviders 命令确认 JavaScript 提供程序已加载。

0:000> .scriptproviders
Available Script Providers:
    NatVis (extension '.NatVis')
    JavaScript (extension '.js')

.scriptproviders(列出脚本提供程序)

.scriptproviders 命令将列出调试器当前理解的所有脚本语言以及注册它们的扩展。

在下面的示例中,将加载 JavaScript 和 NatVis 提供程序。

0:000> .scriptproviders
Available Script Providers:
    NatVis (extension '.NatVis')
    JavaScript (extension '.js')

以“.NatVis”结尾的任何文件均被理解为 NatVis 脚本,以“.js”结尾的任何文件均被理解为 JavaScript 脚本。 可以使用 .scriptload 命令加载任一类型的脚本。

有关详细信息,请参阅 .scriptproviders(列出脚本提供程序)

.scriptload(加载脚本)

.scriptload 命令将加载脚本,并执行脚本的根代码和 initializeScript 函数。 如果初始加载和执行脚本的过程中出现任何错误,则错误将显示在控制台上。 以下命令显示成功加载 TestScript.js。

0:000> .scriptload C:\WinDbg\Scripts\TestScript.js
JavaScript script successfully loaded from 'C:\WinDbg\Scripts\TestScript.js'

脚本所做的任何对象模型操作都将保持原位,直到脚本随后被卸载或再次使用不同的内容运行。

有关详细信息,请参阅 .scriptload(加载脚本)

.scriptrun

.scriptrun 命令将加载脚本、执行脚本的根代码、initializeScriptinvokeScript 函数。 如果初始加载和执行脚本的过程中出现任何错误,则错误将显示在控制台上。

0:000> .scriptrun C:\WinDbg\Scripts\helloWorld.js
JavaScript script successfully loaded from 'C:\WinDbg\Scripts\helloWorld.js'
Hello World!  We are in JavaScript!

脚本所做的任何调试器对象模型操作都将保持原位,直到脚本随后被卸载或再次使用不同的内容运行。

有关详细信息,请参阅 .scriptrun(运行脚本)

.scriptload(卸载脚本)

.scriptunload 命令卸载加载的脚本,并调用 uninitializeScript 函数。 使用以下命令语法卸载脚本

0:000:x86> .scriptunload C:\WinDbg\Scripts\TestScript.js
JavaScript script unloaded from 'C:\WinDbg\Scripts\TestScript.js'

有关详细信息,请参阅 .scriptunload(加载脚本)

.scriptlist(列出已加载的脚本)

.scriptlist 命令将列出通过 .scriptload 或 .scriptrun 命令加载的任何脚本。 如果使用 .scriptload 成功加载 TestScript,则 .scriptlist 命令将显示加载的脚本的名称。

0:000> .scriptlist
Command Loaded Scripts:
    JavaScript script from 'C:\WinDbg\Scripts\TestScript.js'

有关详细信息,请参阅 .scriptlist(列出加载的脚本)

JavaScript 调试器脚本入门

HelloWorld 示例脚本

本节介绍如何创建和执行一个简单的 JavaScript 调试器脚本,该脚本将输出 Hello World。

// WinDbg JavaScript sample
// Prints Hello World
function initializeScript()
{
    host.diagnostics.debugLog("***> Hello World! \n");
}

使用文本编辑器(如记事本)创建一个名为 HelloWorld.js 的文本文件,其中包含上面所示的 JavaScript 代码。

使用 .scriptload 命令加载并执行脚本。 由于使用了函数名称 initializeScript,因此在加载脚本时会运行函数中的代码。

0:000> .scriptload c:\WinDbg\Scripts\HelloWorld.js
JavaScript script successfully loaded from 'c:\WinDbg\Scripts\HelloWorld.js'
***> Hello World! 

加载脚本后,调试器中会提供其他功能。 使用 dx(显示 NatVis 表达式)命令显示 Debugger.State.Scripts,以查看脚本现在是否已驻留。

0:000> dx Debugger.State.Scripts
Debugger.State.Scripts                
    HelloWorld 

在下一个示例中,我们将添加并调用一个命名函数。

添加两个值示例脚本

本节介绍如何创建和执行一个简单的 JavaScript 调试器脚本,该脚本添加输入并添加两个数字。

此简单脚本提供单个函数 addTwoValues。

// WinDbg JavaScript sample
// Adds two functions
function addTwoValues(a, b)
 {
     return a + b;
 }

使用文本编辑器(如记事本)创建名为 FirstSampleFunction.js 的文本文件

使用 .scriptload 命令加载脚本。

0:000> .scriptload c:\WinDbg\Scripts\FirstSampleFunction.js
JavaScript script successfully loaded from 'c:\WinDbg\Scripts\FirstSampleFunction.js'

加载脚本后,调试器中会提供其他功能。 使用 dx(显示 NatVis 表达式)命令显示 Debugger.State.Scripts,以查看脚本现在是否已驻留。

0:000> dx Debugger.State.Scripts
Debugger.State.Scripts                
    FirstSampleFunction    

可以选择 FirstSampleFunction,查看它提供的函数。

0:000> dx -r1 -v Debugger.State.Scripts.FirstSampleFunction.Contents
Debugger.State.Scripts.FirstSampleFunction.Contents                 : [object Object]
    host             : [object Object]
    addTwoValues    
 ... 

为了使脚本更便于使用,请使用 dx 命令在调试器中指定一个变量来保存脚本的内容。

0:000> dx @$myScript = Debugger.State.Scripts.FirstSampleFunction.Contents

使用 dx 表达式计算器调用 addTwoValues 函数。

0:000> dx @$myScript.addTwoValues(10, 41),d
@$myScript.addTwoValues(10, 41),d : 51

还可以使用 $@scriptContents 内置别名来处理脚本。 $@scriptContents 别名合并加载的所有脚本的所有内容。

0:001> dx @$scriptContents.addTwoValues(10, 40),d
@$scriptContents.addTwoValues(10, 40),d : 50

当完成对脚本的处理后,使用 .scriptunload 命令卸载脚本。

0:000> .scriptunload c:\WinDbg\Scripts\FirstSampleFunction.js
JavaScript script successfully unloaded from 'c:\WinDbg\Scripts\FirstSampleFunction.js'

调试器命令自动化

本节介绍如何创建和执行一个简单的 JavaScript 调试器脚本,该脚本可自动发送 u (取消汇编)命令。 此示例还显示如何在循环中收集和显示命令输出。

此脚本提供单个函数 RunCommands()。

// WinDbg JavaScript sample
// Shows how to call a debugger command and display results
"use strict";

function RunCommands()
{
var ctl = host.namespace.Debugger.Utility.Control;   
var output = ctl.ExecuteCommand("u");
host.diagnostics.debugLog("***> Displaying command output \n");

for (var line of output)
   {
   host.diagnostics.debugLog("  ", line, "\n");
   }

host.diagnostics.debugLog("***> Exiting RunCommands Function \n");

}

使用文本编辑器(如记事本)创建名为 RunCommands.js 的文本文件

使用 .scriptload 命令加载 RunCommands 脚本。

0:000> .scriptload c:\WinDbg\Scripts\RunCommands.js 
JavaScript script successfully loaded from 'c:\WinDbg\Scripts\RunCommands.js'

加载脚本后,调试器中会提供其他功能。 使用 dx(显示 NatVis 表达式)命令显示 Debugger.State.Scripts.RunCommands,以查看脚本现在是否已驻留。

0:000>dx -r3 Debugger.State.Scripts.RunCommands
Debugger.State.Scripts.RunCommands                
    Contents         : [object Object]
        host             : [object Object]
            diagnostics      : [object Object]
            namespace       
            currentSession   : Live user mode: <Local>
            currentProcess   : notepad.exe
            currentThread    : ntdll!DbgUiRemoteBreakin (00007ffd`87f2f440) 
            memory           : [object Object]

使用 dx 命令在 RunCommands 脚本中调用 RunCommands 函数。

0:000> dx Debugger.State.Scripts.RunCommands.Contents.RunCommands()
  ***> Displaying command output
  ntdll!ExpInterlockedPopEntrySListEnd+0x17 [d:\rs1\minkernel\ntos\rtl\amd64\slist.asm @ 196]:
  00007ffd`87f06e67 cc              int     3
  00007ffd`87f06e68 cc              int     3
  00007ffd`87f06e69 0f1f8000000000  nop     dword ptr [rax]
  ntdll!RtlpInterlockedPushEntrySList [d:\rs1\minkernel\ntos\rtl\amd64\slist.asm @ 229]:
  00007ffd`87f06e70 0f0d09          prefetchw [rcx]
  00007ffd`87f06e73 53              push    rbx
  00007ffd`87f06e74 4c8bd1          mov     r10,rcx
  00007ffd`87f06e77 488bca          mov     rcx,rdx
  00007ffd`87f06e7a 4c8bda          mov     r11,rdx
***> Exiting RunCommands Function

特殊 JavaScript 调试器函数

脚本提供程序本身调用的 JavaScript 脚本中有几个特殊函数。

initializeScript

当加载并执行 JavaScript 脚本时,在脚本中的变量、函数和其他对象影响调试器的对象模型之前,它要经历一系列步骤。

  • 将脚本加载到内存中并进行解析。
  • 执行脚本中的根代码。
  • 如果脚本有一个名为 initializeScript 的方法,则调用该方法。
  • initializeScript 中的返回值用于确定如何自动修改调试器的对象模型。
  • 脚本中的名称将桥接到调试器的命名空间。

如前所述,initializeScript 将在脚本的根代码执行后立即被调用。 它的工作是向提供程序返回注册对象的 JavaScript 数组,指示如何修改调试器的对象模型。

function initializeScript()
{
    // Add code here that you want to run every time the script is loaded. 
    // We will just send a message to indicate that function was called.
    host.diagnostics.debugLog("***> initializeScript was called\n");
}

invokeScript

invokeScript 方法是主脚本方法,在运行 .scriptload 和 .scriptrun 时调用。

function invokeScript()
{
    // Add code here that you want to run every time the script is executed. 
    // We will just send a message to indicate that function was called.
    host.diagnostics.debugLog("***> invokeScript was called\n");
}

uninitializeScript

uninitializeScript 方法与 initializeScript 的行为相反。 当脚本取消链接并准备好卸载时,将调用它。 它的工作是撤销脚本在执行过程中对对象模型所做的任何更改和/或销毁脚本缓存的任何对象。

如果脚本既不对对象模型执行命令性操作,也不缓存结果,则无需使用 uninitializeScript 方法。 根据 initializeScript 的返回值执行的对对象模型的任何更改都将由提供程序自动撤消。 这样的更改不需要显式 uninitializeScript 方法。

function uninitializeScript()
{
    // Add code here that you want to run every time the script is unloaded. 
    // We will just send a message to indicate that function was called.
    host.diagnostics.debugLog("***> uninitialize was called\n");
}

脚本命令调用的函数摘要

此表总结了脚本命令调用的函数

命令 .scriptload .scriptrun(运行脚本) .scriptload(卸载脚本)
root
initializeScript
invokeScript
uninitializeScript

使用此示例代码查看在加载、执行和卸载脚本时何时调用每个函数。

// Root of Script
host.diagnostics.debugLog("***>; Code at the very top (root) of the script is always run \n");


function initializeScript()
{
    // Add code here that you want to run every time the script is loaded. 
    // We will just send a message to indicate that function was called.
    host.diagnostics.debugLog("***>; initializeScript was called \n");
}

function invokeScript()
{
    // Add code here that you want to run every time the script is executed. 
    // We will just send a message to indicate that function was called.
    host.diagnostics.debugLog("***>; invokeScript was called \n");
}


function uninitializeScript()
{
    // Add code here that you want to run every time the script is unloaded. 
    // We will just send a message to indicate that function was called.
    host.diagnostics.debugLog("***>; uninitialize was called\n");
}


function main()
{
    // main is just another function name in JavaScript
    // main is not called by .scriptload or .scriptrun  
    host.diagnostics.debugLog("***>; main was called \n");
}

在 JavaScript 中创建调试器可视化工具

通过自定义可视化文件,可以在可视化结构中对数据进行分组和组织,以更好地反映数据关系和内容。 可以使用 JavaScript 调试器扩展编写调试器可视化工具,这些可视化工具的行为方式与 NatVis 非常相似。 这是通过创作 JavaScript 原型对象(或 ES6 类)来实现的,该对象充当给定数据类型的可视化工具。 有关 NatVis 和调试器的详细信息,请参阅 dx(显示 NatVis 表达式)

示例类 - Simple1DArray

请考虑表示一维数组的 C++ 类的示例。 此类有两个成员,m_size 是数组的总大小,m_pValues 是指向内存中等于 m_size 字段的一些 ints 的指针。

class Simple1DArray
{
private:

    ULONG64 m_size;
    int *m_pValues;
};

可以使用 dx 命令查看默认数据结构呈现。

0:000> dx g_array1D
g_array1D                 [Type: Simple1DArray]
    [+0x000] m_size           : 0x5 [Type: unsigned __int64]
    [+0x008] m_pValues        : 0x8be32449e0 : 0 [Type: int *]

JavaScript 可视化工具

为了可视化这种类型,需要创作一个原型(或 ES6)类,其中包含我们希望调试器显示的所有字段和属性。 我们还需要让 initializeScript 方法返回一个对象,该对象指示 JavaScript 提供程序将原型作为给定类型的可视化工具链接。

function initializeScript()
{
    //
    // Define a visualizer class for the object.
    //
    class myVisualizer
    {
        //
        // Create an ES6 generator function which yields back all the values in the array.
        //
        *[Symbol.iterator]()
        {
            var size = this.m_size;
            var ptr = this.m_pValues;
            for (var i = 0; i < size; ++i)
            {
                yield ptr.dereference();

                //
                // Note that the .add(1) method here is effectively doing pointer arithmetic on
                // the underlying pointer.  It is moving forward by the size of 1 object.
                //
                ptr = ptr.add(1);
            }
        }
    }

    return [new host.typeSignatureRegistration(myVisualizer, "Simple1DArray")];
}

将脚本保存在名为 arrayVisualizer.js 的文件中。

使用 .load(加载扩展 DLL)命令加载 JavaScript 提供程序。

0:000> .load C:\ScriptProviders\jsprovider.dll

使用 .scriptload 加载数组可视化工具脚本。

0:000> .scriptload c:\WinDbg\Scripts\arrayVisualizer.js
JavaScript script successfully loaded from 'c:\WinDbg\Scripts\arrayVisualizer.js'

现在,当使用 dx 命令时,脚本可视化工具将显示多行数组内容。

0:000> dx g_array1D
g_array1D                 : [object Object] [Type: Simple1DArray]
    [<Raw View>]     [Type: Simple1DArray]
    [0x0]            : 0x0
    [0x1]            : 0x1
    [0x2]            : 0x2
    [0x3]            : 0x3
    [0x4]            : 0x4

此外,此 JavaScript 可视化效果还提供 LINQ 功能,例如 Select。

0:000> dx g_array1D.Select(n => n * 3),d
g_array1D.Select(n => n * 3),d                
    [0]              : 0
    [1]              : 3
    [2]              : 6
    [3]              : 9
    [4]              : 12

影响可视化效果的因素

通过从 initializeScript 返回 host.typeSignatureRegistration 对象而成为本机类型可视化工具的原型或类,将把 JavaScript 中的所有属性和方法添加到本机类型中。 此外,还适用以下语义:

  • 任何不以两个下划线 (__) 开头的名称都将在可视化效果中可用。

  • 属于标准 JavaScript 对象或 JavaScript 提供程序创建的协议的名称不会显示在可视化效果中。

  • 可以通过 [Symbol.iterator] 的支持使对象可迭代。

  • 通过支持由几个函数组成的自定义协议,可以使对象具有可索引性:getDimensionality、getValueAt 和可选的 setValueAt。

本机和 JavaScript 对象桥

JavaScript 与调试器的对象模型之间的桥是双向的。 本机对象可以传递到 JavaScript 中,JavaScript 对象可以传递到调试器的表达式计算器中。 例如,考虑在脚本中添加以下方法:

function multiplyBySeven(val)
{
    return val * 7;
}

现在可以在上面的示例 LINQ 查询中使用此方法。 首先,加载 JavaScript 可视化效果。

0:000> .scriptload c:\WinDbg\Scripts\arrayVisualizer2.js
JavaScript script successfully loaded from 'c:\WinDbg\Scripts\arrayVisualizer2.js'

0:000> dx @$myScript = Debugger.State.Scripts.arrayVisualizer2.Contents

然后,可以使用 multiplyBySeven 函数内联函数,如下所示。

0:000> dx g_array1D.Select(@$myScript.multiplyBySeven),d
g_array1D.Select(@$myScript.multiplyBySeven),d                
    [0]              : 0
    [1]              : 7
    [2]              : 14
    [3]              : 21
    [4]              : 28

带 JavaScript 的条件断点

可以在遇到断点后使用 JavaScript 进行补充处理。 例如,脚本可用于检查其他运行时值,然后确定是要自动继续执行代码,还是要停止并执行其他手动调试。

有关使用断点的一般信息,请参阅控制断点的方法

DebugHandler.js 示例断点处理脚本

本示例将评估记事本的打开和保存对话框:notepad!ShowOpenSaveDialog。 此脚本将评估 pszCaption 变量,以确定当前对话是“打开”对话框还是“另存为”对话框。 如果是“打开”对话框,代码执行将继续。 如果是“另存为”对话框,代码执行将停止,调试器将中断。

 // Use JavaScript strict mode 
"use strict";

// Define the invokeScript method to handle breakpoints

 function invokeScript()
 {
    var ctl = host.namespace.Debugger.Utility.Control;

    //Get the address of my string
    var address = host.evaluateExpression("pszCaption");

    // The open and save dialogs use the same function
    // When we hit the open dialog, continue.
    // When we hit the save dialog, break.
    if (host.memory.readWideString(address) == "Open") {
        // host.diagnostics.debugLog("We're opening, let's continue!\n");
        ctl.ExecuteCommand("gc");
    }
    else
    {
        //host.diagnostics.debugLog("We're saving, let's break!\n");
    }
  }

此命令在 notepad!ShowOpenSaveDialog 上设置断点,并将在遇到断点时运行上述脚本。

bp notepad!ShowOpenSaveDialog ".scriptrun C:\\WinDbg\\Scripts\\DebugHandler.js"

然后,当在记事本中选择“文件”>“保存”选项时,脚本将运行,g 命令不会发送,代码执行中断。

JavaScript script successfully loaded from 'C:\WinDbg\Scripts\DebugHandler.js'
notepad!ShowOpenSaveDialog:
00007ff6`f9761884 48895c2408      mov     qword ptr [rsp+8],rbx ss:000000db`d2a9f2f0=0000021985fe2060

在 JavaScript 扩展中使用 64 位值

本节介绍传递到 JavaScript 调试器扩展的 64 位值的行为方式。 这个问题的出现是因为 JavaScript 只能使用 53 位存储数字。

64 位和 JavaScript 53 位存储

传递给 JavaScript 的序号值通常作为 JavaScript 数字进行封送。 问题在于 JavaScript 数字是 64 位双精度浮点值。 任何超过 53 位的序号都会在进入 JavaScript 时失去精度。 这对于 64 位指针和其他 64 位序号值(可能具有最高字节的标志)提出了问题。 为了解决此问题,任何输入 JavaScript 的 64 位本机值(无论是本机代码还是数据模型)都以库类型的形式输入,而不是以 JavaScript 数字的形式输入。 此库类型将往返到本机代码,而不会损失数值精度。

自动转换

64 位序号值的库类型支持标准 JavaScript valueOf 转换。 如果该对象用于需要转换值的数学操作或其他构造,它将自动转换为 JavaScript 数字。 如果发生精度损失(该值使用了超过 53 位的序号精度),则 JavaScript 提供程序将引发异常。

注意:如果在 JavaScript 中使用按位运算符,则序号精度进一步限制为 32 位。

此示例代码对两个数字求和,用于测试 64 位值的转换。

function playWith64BitValues(a64, b64)
{
    // Sum two numbers to demonstrate 64-bit behavior.
    //
    // Imagine a64==100, b64==1000
    // The below would result in sum==1100 as a JavaScript number.  No exception is thrown.  The values auto-convert.
    //
    // Imagine a64==2^56, b64=1
    // The below will **Throw an Exception**.  Conversion to numeric results in loss of precision!
    //
    var sum = a64 + b64;
    host.diagnostics.debugLog("Sum   >> ", sum, "\n");

}

function performOp64BitValues(a64, b64, op)
{
    //
    // Call a data model method passing 64-bit value.  There is no loss of precision here.  This round trips perfectly.
    // For example:
    //  0:000> dx @$myScript.playWith64BitValues(0x4444444444444444ull, 0x3333333333333333ull, (x, y) => x + y)
    //  @$myScript.playWith64BitValues(0x4444444444444444ull, 0x3333333333333333ull, (x, y) => x + y) : 0x7777777777777777
    //
    return op(a64, b64);
}

使用文本编辑器(如记事本)创建名为 PlayWith64BitValues.js 的文本文件

使用 .scriptload 命令加载脚本。

0:000> .scriptload c:\WinDbg\Scripts\PlayWith64BitValues.js
JavaScript script successfully loaded from 'c:\WinDbg\Scripts\PlayWith64BitValues.js'

为了使脚本更便于使用,请使用 dx 命令在调试器中指定一个变量来保存脚本的内容。

0:000> dx @$myScript = Debugger.State.Scripts.PlayWith64BitValues.Contents

使用 dx 表达式计算器调用 addTwoValues 函数。

首先,我们将计算 2^53 =9007199254740992(十六进制 0x20000000000000)的值。

首先进行测试,我们将使用 (2^53) - 2,看看它是否返回正确的总和值。

0:000> dx @$myScript.playWith64BitValues(9007199254740990, 9007199254740990)
Sum   >> 18014398509481980

然后,我们将计算 (2^53) -1 =9007199254740991。 这将返回一个错误,指示转换过程将损失精度,因此这是 JavaScript 代码中求和方法可以使用的最大值。

0:000> dx @$myScript.playWith64BitValues(9007199254740990, 9007199254740991)
Error: 64 bit value loses precision on conversion to number

调用传递 64 位值的数据模型方法。 此时不会损失精度。

0:001> dx @$myScript.performOp64BitValues( 0x7FFFFFFFFFFFFFFF,  0x7FFFFFFFFFFFFFFF, (x, y) => x + y)
@$myScript.performOp64BitValues( 0x7FFFFFFFFFFFFFFF,  0x7FFFFFFFFFFFFFFF, (x, y) => x + y) : 0xfffffffffffffffe

比较

64 位库类型是 JavaScript 对象,而不是 JavaScript 数字之类的值类型。 这对比较操作有一些影响。 通常,对象的相等性 (==) 表示操作数引用的是同一个对象,而不是同一个值。 JavaScript 提供程序通过跟踪对 64 位值的实时引用并为未收集的 64 位值返回相同的“不可变”对象来缓解此问题。 这意味着为了进行比较,将出现以下情况。

// Comparison with 64 Bit Values

function comparisonWith64BitValues(a64, b64)
{
    //
    // No auto-conversion occurs here.  This is an *EFFECTIVE* value comparison.  This works with ordinals with above 53-bits of precision.
    //
    var areEqual = (a64 == b64);
    host.diagnostics.debugLog("areEqual   >> ", areEqual, "\n");
    var areNotEqual = (a64 != b64);
    host.diagnostics.debugLog("areNotEqual   >> ", areNotEqual, "\n");

    //
    // Auto-conversion occurs here.  This will throw if a64 does not pack into a JavaScript number with no loss of precision.
    //
    var isEqualTo42 = (a64 == 42);
    host.diagnostics.debugLog("isEqualTo42   >> ", isEqualTo42, "\n");
    var isLess = (a64 < b64);
    host.diagnostics.debugLog("isLess   >> ", isLess, "\n");

使用文本编辑器(如记事本)创建名为 ComparisonWith64BitValues.js 的文本文件

使用 .scriptload 命令加载脚本。

0:000> .scriptload c:\WinDbg\Scripts\ComparisonWith64BitValues.js
JavaScript script successfully loaded from 'c:\WinDbg\Scripts\ComparisonWith64BitValues.js'

为了使脚本更便于使用,请使用 dx 命令在调试器中指定一个变量来保存脚本的内容。

0:000> dx @$myScript = Debugger.State.Scripts.comparisonWith64BitValues.Contents

首先进行测试,我们将使用 (2^53) - 2,看看它是否返回预期值。

0:001> dx @$myScript.comparisonWith64BitValues(9007199254740990, 9007199254740990)
areEqual   >> true
areNotEqual   >> false
isEqualTo42   >> false
isLess   >> false

我们还将尝试将数字 42 作为第一个值,以验证比较操作符是否正常工作。

0:001> dx @$myScript.comparisonWith64BitValues(42, 9007199254740990)
areEqual   >> false
areNotEqual   >> true
isEqualTo42   >> true
isLess   >> true

然后,我们将计算 (2^53) -1 =9007199254740991。 此值将返回一个错误,指示转换过程将损失精度,因此这是 JavaScript 代码中比较运算符可以使用的最大值。

0:000> dx @$myScript.playWith64BitValues(9007199254740990, 9007199254740991)
Error: 64 bit value loses precision on conversion to number

在操作中保持精度

为了允许调试器扩展保持精度,将一组数学函数投射到 64 位库类型的顶部。 如果扩展需要(或可能需要)传入 64 位值的精度高于 53 位,则应使用以下方法,而不是依赖于标准操作符:

“方法名称” Signature 描述
asNumber .asNumber() 将 64 位值转换为 JavaScript 数字。 如果发生精度损失,**将引发异常**
convertToNumber .convertToNumber() 将 64 位值转换为 JavaScript 数字。 如果发生精度损失,**不会引发异常**
getLowPart .getLowPart() 将 64 位值的后 32 位转换为 JavaScript 数字
getHighPart .getHighPart() 将 64 位值的前 32 位转换为 JavaScript 数字
add .add(value) 将值添加到 64 位值并返回结果
subtract .subtract(value) 从 64 位值中减去一个值并返回结果
multiply .multiply(value) 将 64 位值乘以提供的值并返回结果
divide .divide(value) 将 64 位值除以提供的值并返回结果
bitwiseAnd .bitwiseAnd(value) 使用提供的值计算 64 位值的按位值并返回结果
bitwiseOr .bitwiseOr(value) 使用提供的值计算 64 位值的按位“或”值并返回结果
bitwiseXor .bitwiseXor(value) 使用提供的值计算 64 位值的按位“异或”值并返回结果
bitwiseShiftLeft .bitwiseShiftLeft(value) 将 64 位值向左移动给定的量并返回结果
bitwiseShiftRight .bitwiseShiftRight(value) 将 64 位值向右移动给定的量并返回结果
toString .toString([radix]) 将 64 位值转换为默认基数(或可选提供的基数)的显示字符串

这种方法也是可用的。

“方法名称” Signature 描述
compareTo .compareTo(value) 将 64 位值与另一个 64 位值进行比较。

JavaScript 调试

本节介绍如何使用调试器的脚本调试功能。 调试器集成了对使用 .scriptdebug(调试 JavaScript)命令调试 JavaScript 脚本的支持。

注意

若要将 JavaScript 调试与 WinDbg 配合使用,请以管理员身份运行调试器。

使用此示例代码探索调试 JavaScript。 在本演练中,我们将其命名为 DebuggableSample.js,并将其保存在 C:\MyScripts 目录中。

"use strict";

class myObj
{
    toString()
    {
        var x = undefined[42];
        host.diagnostics.debugLog("BOO!\n");
    }
}

class iterObj
{
    *[Symbol.iterator]()
    {
        throw new Error("Oopsies!");
    }
}

function foo()
{
    return new myObj();
}

function iter()
{
    return new iterObj();
}

function throwAndCatch()
{
    var outer = undefined;
    var someObj = {a : 99, b : {c : 32, d: "Hello World"} };
    var curProc = host.currentProcess;
    var curThread = host.currentThread;

    try
    {
        var x = undefined[42];
    } catch(e) 
    {
        outer = e;
    }

    host.diagnostics.debugLog("This is a fun test\n");
    host.diagnostics.debugLog("Of the script debugger\n");
    var foo = {a : 99, b : 72};
    host.diagnostics.debugLog("foo.a = ", foo.a, "\n");

    return outer;
}

function throwUnhandled()
{
    var proc = host.currentProcess;
    var thread = host.currentThread;
    host.diagnostics.debugLog("Hello...  About to throw an exception!\n");
    throw new Error("Oh me oh my!  This is an unhandled exception!\n");
    host.diagnostics.debugLog("Oh...  this will never be hit!\n");
    return proc;
}

function outer()
{
    host.diagnostics.debugLog("inside outer!\n");
    var foo = throwAndCatch();
    host.diagnostics.debugLog("Caught and returned!\n");
    return foo;
}

function outermost()
{
    var x = 99;
    var result = outer();
    var y = 32;
    host.diagnostics.debugLog("Test\n");
    return result;
}

function initializeScript()
{
    //
    // Return an array of registration objects to modify the object model of the debugger
    // See the following for more details:
    //
    //     https://aka.ms/JsDbgExt
    //
}

加载示例脚本。

.scriptload C:\MyScripts\DebuggableSample.js

使用 .scriptdebug 命令,开始主动调试脚本。

0:000> .scriptdebug C:\MyScripts\DebuggableSample.js
>>> ****** DEBUGGER ENTRY DebuggableSample ******
           No active debug event!

>>> Debug [DebuggableSample <No Position>] >

看到提示符 >>> Debug [DebuggableSample <No Position>] > 和输入请求后,将位于脚本调试器内部。

使用 .help 命令在 JavaScript 调试环境中显示命令列表。

>>> Debug [DebuggableSample <No Position>] >.help
Script Debugger Commands (*NOTE* IDs are **PER SCRIPT**):
    ? .................................. Get help
    ? <expr>  .......................... Evaluate expression <expr> and display result
    ?? <expr>  ......................... Evaluate expression <expr> and display result
    |  ................................. List available scripts
    |<scriptid>s  ...................... Switch context to the given script
    bc \<bpid\>  ......................... Clear breakpoint by specified \<bpid\>
    bd \<bpid\>  ......................... Disable breakpoint by specified \<bpid\>
    be \<bpid\>  ......................... Enable breakpoint by specified \<bpid\>
    bl  ................................ List breakpoints
    bp <line>:<column>  ................ Set breakpoint at the specified line and column
    bp <function-name>  ................ Set breakpoint at the (global) function specified by the given name
    bpc  ............................... Set breakpoint at current location
    dv  ................................ Display local variables of current frame
    g  ................................. Continue script
    gu   ............................... Step out
    k  ................................. Get stack trace
    p  ................................. Step over
    q  ................................. Exit script debugger (resume execution)
    sx  ................................ Display available events/exceptions to break on
    sxe <event>  ....................... Enable break on <event>
    sxd <event>  ....................... Disable break on <event>
    t  ................................. Step in
    .attach <scriptId>  ................ Attach debugger to the script specified by <scriptId>
    .detach [<scriptId>]  .............. Detach debugger from the script specified by <scriptId>
    .frame <index>  .................... Switch to frame number <index>
    .f+  ............................... Switch to next stack frame
    .f-  ............................... Switch to previous stack frame
    .help  ............................. Get help

使用 sx 脚本调试器命令查看可捕获的事件列表。

>>> Debug [DebuggableSample <No Position>] >sx              
sx                                                          
    ab  [   inactive] .... Break on script abort            
    eh  [   inactive] .... Break on any thrown exception    
    en  [   inactive] .... Break on entry to the script     
    uh  [     active] .... Break on unhandled exception     

使用 sxe 脚本调试器命令打开中断条目,以便在脚本调试器中的任何代码执行后立即将脚本捕获到脚本调试器中。

>>> Debug [DebuggableSample <No Position>] >sxe en          
sxe en                                                      
Event filter 'en' is now active                             

退出脚本调试器,我们将对脚本进行函数调用,该脚本将捕获到调试器中。

>>> Debug [DebuggableSample <No Position>] >q

此时,你又回到了正常的调试器中。 执行以下命令以调用脚本。

dx @$scriptContents.outermost()

现在,你回到了脚本调试器中,并在最外层 JavaScript 函数的第一行上中断。

>>> ****** SCRIPT BREAK DebuggableSample [BreakIn] ******   
           Location: line = 73, column = 5                  
           Text: var x = 99                                 

>>> Debug [DebuggableSample 73:5] >                         

除了在调试器中看到中断之外,还可以获取发生中断的行 (73) 和列 (5) 的信息,以及相关的源代码片段:var x = 99

让我们执行几次操作,并进入脚本中的另一个位置。

    p
    t
    p
    t
    p
    p

此时,应进入第 34 行的 throwAndCatch 方法。

...
>>> ****** SCRIPT BREAK DebuggableSample [Step Complete] ******                       
           Location: line = 34, column = 5                                            
           Text: var curProc = host.currentProcess                                    

可以通过执行堆栈跟踪来验证这一点。

>>> Debug [DebuggableSample 34:5] >k                                                  
k                                                                                     
    ##  Function                         Pos    Source Snippet                        
-> [00] throwAndCatch                    034:05 (var curProc = host.currentProcess)   
   [01] outer                            066:05 (var foo = throwAndCatch())           
   [02] outermost                        074:05 (var result = outer())                

从这里,可以研究变量的值。

>>> Debug [DebuggableSample 34:5] >??someObj                
??someObj                                                   
someObj          : {...}                                    
    __proto__        : {...}                                
    a                : 0x63                                 
    b                : {...}                                
>>> Debug [DebuggableSample 34:5] >??someObj.b              
??someObj.b                                                 
someObj.b        : {...}                                    
    __proto__        : {...}                                
    c                : 0x20                                 
    d                : Hello World                          

我们在当前代码行上设置一个断点,看看现在设置了哪些断点。

>>> Debug [DebuggableSample 34:5] >bpc                      
bpc                                                         
Breakpoint 1 set at 34:5                                    
>>> Debug [DebuggableSample 34:5] >bl                       
bl                                                          
      Id State    Pos                                       
       1 enabled  34:5                                      

从这里,我们将使用 sxd 脚本调试器命令禁用条目 (en) 事件。

>>> Debug [DebuggableSample 34:5] >sxd en                                                                              
sxd en                                                                                                                 
Event filter 'en' is now inactive                                                                                      

然后继续,让脚本继续到最后。

>>> Debug [DebuggableSample 34:5] >g                                                                                   
g                                                                                                                      
This is a fun test                                                                                                     
Of the script debugger                                                                                                 
foo.a = 99                                                                                                             
Caught and returned!                                                                                                   
Test                                                                                                                   
...

再次执行脚本方法,并观察遇到的断点。

0:000> dx @$scriptContents.outermost()                                                
inside outer!                                                                         
>>> ****** SCRIPT BREAK DebuggableSample [Breakpoint 1] ******                        
           Location: line = 34, column = 5                                            
           Text: var curProc = host.currentProcess                                    

显示调用堆栈。

>>> Debug [DebuggableSample 34:5] >k                                                  
k                                                                                     
    ##  Function                         Pos    Source Snippet                        
-> [00] throwAndCatch                    034:05 (var curProc = host.currentProcess)   
   [01] outer                            066:05 (var foo = throwAndCatch())           
   [02] outermost                        074:05 (var result = outer())                

此时,我们希望停止调试该脚本,因此我们将其与脚本分离。

>>> Debug [DebuggableSample 34:5] >.detach                  
.detach                                                     
Debugger has been detached from script!                     

然后,键入 q 退出。

q                                                           
This is a fun test                                          
Of the script debugger                                      
foo.a = 99                                                  
Caught and returned!                                        
Test                                                        

再次执行该函数将不再中断调试器。

0:007> dx @$scriptContents.outermost()
inside outer!
This is a fun test
Of the script debugger
foo.a = 99
Caught and returned!
Test

VSCode 中的 JavaScript - 添加 IntelliSense

如果要在 VSCode 中使用调试器数据模型对象,可以使用 Windows 开发工具包中提供的定义文件。 IntelliSense 定义文件提供对所有 host.* 调试器对象 API 的支持。 如果在 64 位电脑的默认目录中安装了该工具包,则它位于以下位置:

C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\winext\JsProvider.d.ts

若要在 VSCode 中使用 IntelliSense 定义文件,请执行以下操作:

  1. 查找定义文件 - JSProvider.d.ts

  2. 将定义文件复制到与脚本相同的文件夹中。

  3. /// <reference path="JSProvider.d.ts" /> 添加到 JavaScript 脚本文件的顶部。

在 JavaScript 文件中有了这个引用,除了脚本中的结构外,VS Code 还会自动为你提供 JSProvider 提供的主机 API 上的 IntelliSense。 例如,键入“host”。 将看到所有可用调试器模型 API 的 IntelliSense。

JavaScript 资源

以下是开发 JavaScript 调试扩展时可能有用的 JavaScript 资源。

另请参阅

JavaScript 调试器示例脚本

JavaScript 扩展中的本机对象