JavaScriptCore CSI
一次崩溃现场调查的故事

在调试 JavaScript Bug 时,Web 开发者可以使用 Web Inspector,它提供了一个调试器和许多自省工具来检查他们的代码。但是当 Bug 位于 WebKit 的 JavaScript 引擎 JavaScriptCore (JSC) 的较低层时,WebKit 工程师将需要使用一套不同的工具来诊断问题。

今天,我将通过讲述我们如何诊断 JSC 虚拟机 (VM) 中一个真实 Bug 的故事,来描述 WebKit 工程师使用的一些工具。这个故事将带我们经历……

崩溃

我们使用 WebKit Layout Tests 作为我们整个 WebKit 堆栈的主要回归测试。在 WebKit 的 AddressSanitizer (ASan) 构建版本上运行此测试套件时,我们发现一个 layout test 发生了崩溃。

$ ./Tools/Scripts/run-webkit-tests LayoutTests/inspector/debugger/regress-133182.html --no-retry

崩溃堆栈跟踪如下:

==94293==ERROR: AddressSanitizer: heap-use-after-free on address 0x61e000088a40 at pc 0x00010f9536a1 bp 0x7fff575795d0 sp 0x7fff575795c8
READ of size 8 at 0x61e000088a40 thread T0
    #0 0x10f9536a0 in JSC::VM::exception() const (/Volumes/Data/Build/Debug/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x3276a0)
    #1 0x110fce753 in JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) (/Volumes/Data/Build/Debug/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x19a2753)
    #2 0x110ee9911 in JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) (/Volumes/Data/Build/Debug/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x18bd911)
    #3 0x10fc5d20a in JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) (/Volumes/Data/Build/Debug/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x63120a)
    #4 0x10fc5d6c5 in JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&, WTF::NakedPtr<JSC::Exception>&) (/Volumes/Data/Build/Debug/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x6316c5)
    #5 0x10fc5e1ed in JSC::profiledCall(JSC::ExecState*, JSC::ProfilingReason, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&, WTF::NakedPtr<JSC::Exception>&) (/Volumes/Data/Build/Debug/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x6321ed)
...

从堆栈跟踪来看,崩溃似乎发生在 JSC VM 中。原始 Bug 已在 修订版 200879 中修复。这篇帖子将讲述我们如何修复这个 Bug。

调试崩溃的准备工作

我们调查的第一件事是检查此崩溃是否仍然在最新版本的 WebKit 源代码上重现。在本次调查时,那是修订版 200796。

由于崩溃发生在 ASan 构建版本上,我们需要使用 ASan 配置来构建 WebKit。

$ svn co -r 200796 http://svn.webkit.org/repository/webkit/trunk webkitDir
$ cd webkitDir
$ ./Tools/Scripts/set-webkit-configuration --asan
$ ./Tools/Scripts/build-webkit --debug

接下来,我们重新运行测试。

$ ./Tools/Scripts/run-webkit-tests --debug LayoutTests/inspector/debugger/regress-133182.html --no-retry

幸运的是,崩溃稳定地重现了。

run-webkit-tests 是一个测试工具脚本,它最终调用 WebKitTestRunner (WKTR) 或 DumpRenderTree (DRT) 可执行文件来运行测试。WKTR 在 WebKit2 上运行,后者是一个多进程架构。DRT 在 WebKit1 (又名 WebKitLegacy) 上运行,后者是单进程的。默认情况下,run-webkit-tests 运行 WKTR,因为 WebKit2 是所有现代 WebKit 浏览器应该构建的基础。但出于我们调试的目的,使用 DRT 会更简单。

在这里,我们将尝试通过直接使用 DRT 运行测试来重现崩溃,而不是通过 run-webkit-tests 测试工具。

$ VM=WebKitBuild/Debug/ && \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

结果:崩溃仍然重现。太棒了!现在我们准备深入诊断问题所在。

使用调试器检查 Bug

首先要做的是查看调试器 (lldb) 能告诉我们关于崩溃的什么信息。

$ VM=WebKitBuild/Debug/ && \
DYLD_FRAMEWORK_PATH=$VM \
lldb $VM/DumpRenderTree -- LayoutTests/inspector/debugger/regress-133182.html
(lldb) run

调试器运行 DRT,并由于此处的内存访问错误而迅速停止。

frame #0: 0x000000010262193e JavaScriptCore`JSC::JITCode::execute(this=, vm=0x00007fff5fbf6450, protoCallFrame=0x00007fff5fbf63c0) + 926 at JITCode.cpp:81
   78     } else
   79         entryAddress = addressForCall(MustCheckArity).executableAddress();
   80     JSValue result = JSValue::decode(vmEntryToJavaScript(entryAddress, vm, protoCallFrame));
-> 81     return vm->exception() ? jsNull() : result;
   82 }
   83

在第 81 行(调试器停止的地方),jsNull() 实际上是一个常量,而 result 应该是一个寄存器中的变量。我们在此处看到的唯一内存访问是对 vm->exception() 的读取,它访问 VM 的 m_exception 字段(参见 VM.h)。查看源代码(在 JITCode.cpp 中),我们看到 vm 是一个由其调用者传递给 JITCode::execute() 的参数。

(lldb) up
frame #1: 0x0000000102507b58 JavaScriptCore`JSC::Interpreter::executeCall(this=, callFrame=, function=, callType=, callData=, thisValue=JSValue @ 0x00007fff5fbfa380, args=) + 2968 at Interpreter.cpp:1020
   1017 {
   1018     // Execute the code:
   1019     if (isJSCall)
-> 1020         result = callData.js.functionExecutable->generatedJITCodeForCall()->execute(&vm, &protoCallFrame);

查看 Interpreter::executeCall() 的代码(在 Interpreter.cpp 中),我们看到在执行到达 JITCode::execute() 之前,vm 的值被使用而没有触发崩溃。这意味着 vm 之前有一个有效值。JITCode::execute() 也没有任何代码会修改 vm(注意 vmEntryToJavaScript() 接受 VM* 而不是 VM&)。

这确实很奇怪。vm 应该有效,但实际上无效。让我们看看机器代码在崩溃点实际做了什么。

(lldb) disassemble
...
   0x10262192e <+910>: callq 0x10376994c ; symbol stub for: __asan_report_store8
   0x102621933 <+915>: movq 0x80(%rbx), %rax
   0x10262193a <+922>: movq 0x18(%rbx), %rcx
-> 0x10262193e <+926>: movq %rcx, (%rax)
...

我们看到在尝试存储到 %rax 寄存器中的地址时崩溃了。%rax 中的值是在仅仅两条指令之前使用 %rbx 寄存器计算出来的。让我们看看寄存器值。

(lldb) reg read
General Purpose Registers:
    rax = 0x0000000045e0360e
    rbx = 0x00007fff5fbf6300
    ...
    rbp = 0x00007fff5fbfa230
    rsp = 0x00007fff5fbfa050
    ...

请注意,%rbx 包含一个接近栈指针 %rsp 的值。比较它们的值,我们发现 %rbx (0x00007fff5fbf6300) 指向的地址低于栈指针 (0x00007fff5fbfa050)。由于栈从高地址向低地址增长,这意味着 %rbx 指向栈中未为此帧分配的部分。这解释了为什么 ASan 将此内存访问标记为无效,从而导致崩溃。

此测试运行是在 X86_64 硬件上。根据 X86_64 的应用程序二进制接口 (ABI),%rbx 寄存器是一个被调用者保存寄存器。例如,假设我们有以下函数:

void goo() {
    ...    // Uses %rbx to do its work.
}

void foo() {
    ...    // Sets register %rbx to 42.
    goo(); // Let goo() do some work.
    ...    // %rbx should still be 42.
}

因为 %rbx 是一个被调用者保存寄存器,ABI 规定函数 goo() 在使用 %rbx 的值之前必须保存它,并相应地在返回给其调用者之前恢复该值。从函数 foo() 的角度来看,它在调用 goo() 之前不必先保存 %rbx,因为保留寄存器值的责任是被调用者 goo() 的,因此得名被调用者保存寄存器

JITCode::execute() 的完整反汇编机器代码中搜索 %rbx,我们看到 %rbx 在函数顶部被设置为栈指针。

    0x10262162c <+12>:   pushq  %rbx
    ...
    0x102621638 <+24>:   movq   %rsp, %rbx

……并且在函数末尾恢复之前再也没有被设置过。

    0x102621abe <+1182>: popq   %rbx

在整个函数中(包括崩溃点之前),%rbx 用于计算被读写栈地址。我们没有在 %rbx 的任何这些先前的用途上崩溃。

由于 %rbx 是一个被调用者保存寄存器,并且此函数中没有其他对其的写入(在函数顶部和崩溃点之间),我们知道 JITCode::execute() 的某个被调用者肯定修改了 %rbx 并且在返回之前未能恢复它。JSC 确实在 LLInt 解释器和即时 (JIT) 编译器生成的代码中包含保存和恢复被调用者保存寄存器的代码。由于 JITCode::execute() 作为 LLInt 或 JIT 生成代码的入口点,也许 Bug 在 LLInt 或 JIT 代码中。

隔离 Bug

JSC 配备了多层执行引擎。您可能在这里阅读过它们。回顾一下,共有 4 层:

  • 第 1 层:LLInt 解释器
  • 第 2 层:基线 JIT 编译器
  • 第 3 层:DFG JIT
  • 第 4 层:FTL JIT(现在带有我们新的 B3 后端

我们可以缩小 Bug 搜索范围的一种方法是检查重现 Bug 需要哪些层级。

使用 JSC 选项禁用 JIT 层级

在 JSC 的 Options.h 中,您会找到一个选项列表,可以用来配置 JSC VM 在运行时如何行为。我们可以通过在调用 DRT 时将它们设置为环境变量来使用这些选项,如下所示:

$ VM=WebKitBuild/Debug/ && \
JSC_someOption=someValue \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

为了测试任何 JIT 层级是否对 Bug 有影响,我们希望禁用每个层级并重新测试 Bug。为此,我们需要以下选项:

选项 描述 可以运行的层级 无法运行的层级
JSC_useJIT=false 禁用所有 JIT LLInt Baseline, DFG, FTL
JSC_useDFGJIT=false 禁用 DFG 及以上层级 LLInt, Baseline DFG, FTL
JSC_useFTLJIT=false 禁用 FTL LLInt, Baseline, DFG FTL

让我们从最低层开始向上移动,即只允许 JavaScript (JS) 代码与 LLInt 解释器一起运行。

$ VM=WebKitBuild/Debug/ && \
JSC_useJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

结果:崩溃没有重现。这意味着 Bug 肯定存在于一个或多个 JIT 中。接下来,允许到基线 JIT 层级。

$ VM=WebKitBuild/Debug/ && \
JSC_useDFGJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

结果:崩溃仍然没有重现。看起来 Bug 可能存在于 DFG 或更高层级。接下来,允许到 DFG JIT 层级。

$ VM=WebKitBuild/Debug/ && \
JSC_useFTLJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

结果:崩溃重现了。啊哈!当允许 DFG 层级运行时,我们发生了崩溃。所以,为了简化我们的调试工作,我们继续不使用 FTL。接下来要做的是通过只编译最少一组函数来缩小搜索范围。希望这最少一组函数将只包含 1 个函数。

提高 JIT 可预测性

但在那之前,我们再添加一个有用的选项:

选项 描述
JSC_useConcurrentJIT=false 禁用并发编译。

DFG 和 FTL JIT 可能会在后台线程中进行编译。我们可以禁用这些并发后台线程的使用,并强制所有 DFG 和 FTL 编译与 JS 代码的执行同步,如下所示:

$ VM=WebKitBuild/Debug/ && \
JSC_useConcurrentJIT=false \
JSC_useFTLJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

结果:崩溃仍然重现。很好。Bug 不是由 JIT 编译线程的任何竞争条件引起的。现在,我们可以继续调查哪个编译后的 JS 函数触发了崩溃。

报告已编译函数

我们可以通过应用编译过滤器来减少已编译函数的集合。但在应用过滤器之前,我们必须首先知道哪些函数正在被编译。我们可以通过告诉 JIT 报告每个函数的编译时间来找出这一点:

选项 描述
JSC_reportCompileTimes=true 报告所有 JIT 编译时间。
JSC_reportBaselineCompiletimes=true 仅报告基线 JIT 编译时间。
JSC_reportDFGCompileTimes=true 仅报告 DFG 和 FTL 编译时间。
JSC_reportFTLCompileTimes=true 仅报告 FTL 编译时间。

既然我们认为 Bug 在 DFG 中,那么我们只报告 DFG 编译时间。我们通过将 JSC_reportDFGCompileTimes=trueJSC_useFTLJIT=false 结合使用来做到这一点。

$ VM=WebKitBuild/Debug/ && \
JSC_reportDFGCompileTimes=true \
JSC_useConcurrentJIT=false \
JSC_useFTLJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

如果有任何函数是 DFG 编译的,我们应该在 stderr 上看到类似以下内容的日志,针对每个编译的函数:

Optimized foo#BAbkxs:[0x62d0000eb840->0x62d0000eba60->0x62d00007d700, NoneFunctionCall, 12 (NeverInline)] using DFGMode with DFG into 951 bytes in 56.254306 ms.

我们这样解读这个日志:

Optimized <function name>#<function hash>:[<pointers to internal data structures representing this function>, NoneFunctionCall, <bytecode size of the function> (NeverInline)] using <JIT that compiled the function> into <size of compiled function> bytes in <time used to compile the function> ms.

<函数名>#<函数哈希> 合在一起称为函数签名<函数的字节码大小> 是 JSC 为此函数生成的解释器字节码的大小。我们将在下面使用这些。

回到我们的测试用例,我们实际上没有看到任何 DFG 编译时间日志。这很奇怪。我们需要启用 DFG 才能重现崩溃,但没有函数被 DFG 编译。

让我们向下到基线 JIT 层,看看在那里有哪些函数被编译了。

$ VM=WebKitBuild/Debug/ && \
JSC_reportBaselineCompileTimes=true \
JSC_useConcurrentJIT=false \
JSC_useFTLJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

这次,我们得到了一些编译时间日志:

Optimized isSymbol#BTGpXV:[0x62d000214100->0x62d0001f8aa0, BaselineFunctionCall, 48 (ShouldAlwaysBeInlined)] with Baseline JIT into 1617 bytes in 0.440446 ms.
Optimized toString#D57Jzo:[0x62d000214dc0->0x62d0001f8d60, BaselineFunctionCall, 65 (ShouldAlwaysBeInlined)] with Baseline JIT into 1772 bytes in 0.356497 ms.
Optimized endsWith#AfTryh:[0x62d00028e300->0x62d0001f8b50, BaselineFunctionCall, 166 (ShouldAlwaysBeInlined)] with Baseline JIT into 3428 bytes in 0.628524 ms.
Optimized processDescriptor#DsYIGz:[0x62d00028e0e0->0x62d0002afee0, BaselineFunctionCall, 402] with Baseline JIT into 6243 bytes in 0.891232 ms.
Optimized createFakeValueDescriptor#BPpnwK:[0x62d00028dec0->0x62d000234100, BaselineFunctionCall, 530] with Baseline JIT into 9933 bytes in 1.286554 ms.
Optimized processProperties#CgNq2F:[0x62d00028e520->0x62d0002afe30, BaselineFunctionCall, 1031] with Baseline JIT into 14777 bytes in 2.026135 ms.
Optimized isPrimitiveValue#BLrwAH:[0x62d000215420->0x62d0001f87e0, BaselineFunctionCall, 113 (ShouldAlwaysBeInlined)] with Baseline JIT into 2708 bytes in 0.488067 ms.
...
Optimized _isHTMLAllCollection#BlkszW:[0x62d000215200->0x62d000236ba0, BaselineFunctionCall, 88 (ShouldAlwaysBeInlined)] with Baseline JIT into 2029 bytes in 0.408096 ms.
Optimized _subtype#DYV24q:[0x62d000214320->0x62d000236af0, BaselineFunctionCall, 375] with Baseline JIT into 7250 bytes in 0.974044 ms.
Optimized next#EXE83Q:[0x62d000217840->0x62d000235390, BaselineFunctionCall, 158 (StrictMode)] with Baseline JIT into 3065 bytes in 0.711777 ms.
Optimized arrayIteratorValueNext#A7WxpW:[0x62d0002171e0->0x62d000086570, BaselineFunctionCall, 127 (ShouldAlwaysBeInlined) (StrictMode)] with Baseline JIT into 3981 bytes in 0.651072 ms.

下一步是将这组函数减少到最小。

按字节码大小过滤要编译的函数

从基线编译时间日志中,我们可以看到基线编译的函数的字节码大小范围从 48 到 1031。

我们可以通过限制编译的字节码大小范围来过滤要编译的函数。我们可以使用以下选项来完成此操作:

选项 描述
JSC_bytecodeRangeToJITCompile=N:M 仅 JIT 编译字节码大小在 N 和 M 之间(包括 N 和 M)的函数。
JSC_bytecodeRangeToDFGCompile=N:M 仅 DFG 编译字节码大小在 N 和 M 之间(包括 N 和 M)的函数。
JSC_bytecodeRangeToFTLCompile=N:M 仅 FTL 编译字节码大小在 N 和 M 之间(包括 N 和 M)的函数。

让我们尝试从范围 1:100 开始。

$ VM=WebKitBuild/Debug/ && \
JSC_bytecodeRangeToJITCompile=1:100 \
JSC_reportBaselineCompileTimes=true \
JSC_useConcurrentJIT=false \
JSC_useFTLJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

现在,我们只得到以下日志:

Optimized isSymbol#BTGpXV:[0x62d000214100->0x62d0001f8aa0, BaselineFunctionCall, 48 (ShouldAlwaysBeInlined)] with Baseline JIT into 1617 bytes in 0.444351 ms.
Optimized toString#D57Jzo:[0x62d000214dc0->0x62d0001f8d60, BaselineFunctionCall, 65 (ShouldAlwaysBeInlined)] with Baseline JIT into 1772 bytes in 0.462039 ms.
Optimized _isHTMLAllCollection#BlkszW:[0x62d000215200->0x62d000236ba0, BaselineFunctionCall, 88 (ShouldAlwaysBeInlined)] with Baseline JIT into 2029 bytes in 0.479594 ms.

……并且崩溃仍然重现。我们走在正确的轨道上。

我们可以继续使用缩小的字节码范围进行过滤,但让我借此机会向您介绍另一种过滤要编译函数的方法……

使用白名单过滤要编译的函数

有时,重现问题所需的最小编译函数集不止一个,而且这些函数的字节码大小可能不适合一个连续的范围,从而排除所有其他函数。

例如,假设我们已将函数列表过滤到 3 个:

foo#Abcdef with bytecode size 10
goo#Bcdefg with bytecode size 50
hoo#Cdefgh with bytecode size 100

……并且我们想检查是否可以仅使用 foohoo 来重现问题。为此,我们不能使用字节码范围过滤器,因为包含 foohoo 的范围 (10:100) 也会允许 goo 被编译。相反,对于这种情况,我们需要能够按函数签名进行过滤:

选项 描述
JSC_jitWhitelist=<白名单文件> 仅 JIT 编译其签名在白名单文件中的函数。
JSC_dfgWhitelist=<白名单文件> 仅 DFG 编译其签名在白名单文件中的函数。

让我们将其应用于我们调查中的函数列表。首先,我们将创建一个文件,其中包含我们从编译时间日志中看到的其余函数签名。

$ vi whitelist.txt
isSymbol#BTGpXV
// toString#D57Jzo
// _isHTMLAllCollection#BlkszW

每个签名必须在白名单文件中独占一行,并且每个条目必须从行的第一个字符开始。白名单机制支持以 // 开头的 C++ 风格注释。使用时,// 也必须从行的第一个字符开始。请注意,我们注释掉了第二和第三个函数签名。这是为了我们可以一次测试一个签名。我们将从只测试第一个开始:isSymbol#BTGpXV

由于我们正在查看基线 JIT 编译,让我们使用 JSC_jitWhitelist 选项。

$ VM=WebKitBuild/Debug/ && \
JSC_jitWhitelist=whitelist.txt \
JSC_reportBaselineCompileTimes=true \
JSC_useConcurrentJIT=false \
JSC_useFTLJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

……并且它仍然崩溃。这正是我们所希望的,即我们只需要调试一个编译函数:isSymbol#BTGpXV

转储已编译函数

接下来,我们可以转储已编译函数的编译产物,看看其中是否有任何错误。我们可以使用以下选项来完成此操作:

选项 描述
JSC_dumpDisassembly=true 转储所有 JIT 编译函数的反汇编。
JSC_dumpDFGDisassembly=true 转储 DFG 和 FTL 编译函数的反汇编。
JSC_dumpFTLDisassembly=true 转储 FTL 编译函数的反汇编。
JSC_dumpSourceAtDFGTime=true 转储 DFG / FTL 编译函数的源代码。
JSC_dumpBytecodeAtDFGTime=true 转储 DFG / FTL 编译函数的字节码。
JSC_dumpGraphAfterParsing=true 在 DFG / FTL 编译时解析函数字节码后,转储 DFG 图。
JSC_dumpGraphAtEachPhase=true 在 DFG / FTL 编译的每个阶段后,转储 DFG 图。

让我们使用 JSC_dumpDisassembly=true 来查看基线 JIT 为我们的函数 isSymbol#BTGpXV 生成了什么。

$ VM=WebKitBuild/Debug/ && \
JSC_dumpDisassembly=true \
JSC_jitWhitelist=whitelist.txt \
JSC_reportBaselineCompileTimes=true \
JSC_useConcurrentJIT=false \
JSC_useFTLJIT=false \
DYLD_FRAMEWORK_PATH=$VM \
$VM/DumpRenderTree LayoutTests/inspector/debugger/regress-133182.html

我们得到了 isSymbol#BTGpXV 的转储,看起来像这样(为简洁起见,我省略了许多生成的代码部分,缩短了此转储):

Generated Baseline JIT code for isSymbol#BTGpXV:[0x62d000287400->0x62d0001f8890, BaselineFunctionCall, 48], instruction count = 48
   Source: function isSymbol(obj) {return typeof obj==="symbol";}
   Code at [0x374dbee009a0, 0x374dbee00ff0):
          0x374dbee009a0: push %rbp
          0x374dbee009a1: mov %rsp, %rbp
          ...
   [   0] enter             
          0x374dbee00a5f: mov $0xa, -0x20(%rbp)
          ...
   [   1] log_shadow_chicken_prologue 
          0x374dbee00b5e: mov $0x2, 0x24(%rbp)
          ...
   [   2] get_scope         loc3
          0x374dbee00bc9: mov 0x18(%rbp), %rax
          ...
   [   4] mov               loc4, loc3
          0x374dbee00bd5: mov -0x20(%rbp), %rax
          0x374dbee00bd9: mov %rax, -0x28(%rbp)
   [   7] create_lexical_environment loc5, loc3, Cell: 0x62d00029bd60 (0x62d000007700:[SymbolTable, {}, NonArray, Leaf]), ID: 18(const0), Undefined(const1)
          0x374dbee00bdd: mov $0x8, 0x24(%rbp)
          ...
   [  12] mov               loc3, loc5
          0x374dbee00c21: mov -0x30(%rbp), %rax
          0x374dbee00c25: mov %rax, -0x20(%rbp)
   [  15] put_to_scope      loc5, obj(@id0), arg1, 2052<ThrowIfNotFound|LocalClosureVar|NotInitialization>, <structure>, 0
          0x374dbee00c29: mov 0x30(%rbp), %rax
          ...
   [  22] debug             didEnterCallFrame, 0
          ...
   [  25] debug             willExecuteStatement, 0
          ...
   [  28] get_from_scope    loc7, loc5, obj(@id0), 1050627<DoNotThrowIfNotFound|ClosureVar|NotInitialization>, 0    predicting Stringident
          ...
   [  36] typeof            loc8, loc7
          ...
   [  39] stricteq          loc8, loc8, String (atomic) (identifier): symbol, ID: 4(const2)
          ...
   [  43] debug             willLeaveCallFrame, 0
          ...
   [  46] ret               loc8
          ...
          0x374dbee00e4d: ret 
   (End Of Main Path)
   (S) [  39] stricteq          loc8, loc8, String (atomic) (identifier): symbol, ID: 4(const2)
          0x374dbee00e4e: mov $0x28, 0x24(%rbp)
          ...
          0x374dbee00ea0: jmp 0x374dbee00dd8
   (End Of Slow Path)
          0x374dbee00ea5: mov $0x62d000287400, %rsi
          ...
          0x374dbee00fee: jmp *%rsi
Optimized isSymbol#BTGpXV:[0x62d000287400->0x62d0001f8890, BaselineFunctionCall, 48 (ShouldAlwaysBeInlined)] with Baseline JIT into 1616 bytes in 17.329743 ms.

注意:我们还会获得许多不属于我们函数的其他代码段的转储(在我上面的摘录中未显示)。这是因为 JSC_dumpDisassembly=true 字面上会转储所有反汇编。其他转储来自 JIT thunk 代码,这些代码是我们编译的,但不是由基线 JIT 生成的。我们暂时可以忽略这些。

查看 isSymbol#BTGpXV 的转储,我们看到了为函数中每个字节码生成的代码段。手动检查这些代码量很大(转储中有 324 行)。

我们需要进一步缩小搜索范围。

使用日志进行调试

让我们花点时间重新整理并思考我们目前所掌握的事实。最初,我们看到 JITCode::execute() 中一个被调用者保存寄存器 %rbx 被破坏了。JITCode::execute() 是 JSC 调用 JS 代码的地方。它通过调用名为 vmEntryToJavaScript 的 thunk 来完成此操作。从 vmEntryToJavaScript 返回后,它立即进行了异常检查,就在那里崩溃了。

以下是相关代码:

JSValue JITCode::execute(VM* vm, ProtoCallFrame* protoCallFrame)
{
    ...
    JSValue result = JSValue::decode(vmEntryToJavaScript(entryAddress, vm, protoCallFrame));
    return vm->exception() ? jsNull() : result;
}

vmEntryToJavaScript 使用 doVMEntry 宏在 LLInt 汇编中实现(参见 LowLevelInterpreter.asmLowLevelInterpreter64.asm)。JSC VM 通过 doVMEntry 进入所有 LLInt 或 JIT 代码,并将通过 doVMEntry 的末尾(正常返回)或通过 _handleUncaughtException(因未捕获异常退出)退出。

有一个想法……我们应该在这些入口和出口点探测 %rbx 的值。毕竟,我们想要保留的 %rbx 值来自 JITCode::execute(),它是 vmEntryToJavaScript / doVMEntry 的调用者。

探测 %rbx 值的一种方法是在代码中感兴趣的点设置调试器断点,并在调试器中断时检查寄存器值。然而,这种技术只适用于我们已经知道 Bug 会在调试器中断的前几次就出现的情况。

但如果我们不确定 Bug 何时出现,我们应该通过在感兴趣的点记录 %rbx 的值来探测。这使我们能够鸟瞰 Bug 出现的时间。如有必要,我们之后可以更具针对性地返回并应用断点。由于我们尚不知道 Bug 会在哪个 VM 入口/出口处出现,让我们进行一些日志记录。

LLInt 探测和 VM 条目计数

为了从 LLInt 代码(手写汇编)进行日志记录,我们需要一个探测机制,它能保存 CPU 寄存器,调用 C++ 函数进行日志记录,然后在调用后恢复 CPU 寄存器。这样我们就可以在 LLInt 代码中插入探测,而不会干扰其操作。

以下是 LLInt 代码片段(仅适用于 OSX 上的 X86_64),它将完成这项工作:

macro probe(func)
    # save all the registers that the LLInt may use.
    push a0, a1
    push a2, a3
    push t0, t1
    push t2, t3
    push t4, t5

    emit "mov %rbx, %rdi" # pass %rbx as arg0 (i.e. %rdi on X86_64).
    move sp, a1 # pass the stack pointer as arg1.
    call func # call the probe function.

    # restore all the registers we saved previously.
    pop t5, t4
    pop t3, t2
    pop t1, t0
    pop a3, a2
    pop a1, a0
    end

我们将此 LLInt 探测宏添加到 LowLevelInterpreter64.asm 的顶部。接下来,我们在 LowLevelInterpreter.asm 的各个位置插入探测:

    _vmEntryToJavaScript:
    ...
    doVMEntry(makeJavaScriptCall, _jsEntryEnterProbe, _jsEntryExitProbe)

    ...
    _vmEntryToNative:
    ...
    doVMEntry(makeHostFunctionCall, _hostEnterProbe, _hostExitProbe)

……以及 LowLevelInterpreter64.asm

macro doVMEntry(makeCall, entryProbe, exitProbe)
    functionPrologue()
    pushCalleeSaves()

    probe(entryProbe)
    ...
    probe(exitProbe)

    popCalleeSaves()
    functionEpilogue()

    ret
end

_handleUncaughtException:
    probe(_uncaughtExceptionEnterProbe)
    loadp Callee[cfr], t3
    ...
    probe(_uncaughtExceptionExitProbe)

    popCalleeSaves()
    functionEpilogue()
    ret

……并在 LLIntSlowPaths.cpp 底部添加这些探测对应的回调函数:

extern int vmEntryCount;

extern "C" void jsEntryEnterProbe(void* rbx, void* rsp);
void jsEntryEnterProbe(void* rbx, void* rsp)
{
    dataLog("ENTRY[", vmEntryCount, "] jsEntry ENTER: rbx=",
        RawPointer(rbx), " rsp=", RawPointer(rsp), "\n");
}

extern "C" void jsEntryExitProbe(void* rbx, void* rsp);
void jsEntryExitProbe(void* rbx, void* rsp)
{
    dataLog("ENTRY[", vmEntryCount, "] jsEntry EXIT: rbx=",
        RawPointer(rbx), " rsp=", RawPointer(rsp), "\n");
}

extern "C" void hostEnterProbe(void* rbx, void* rsp);
void hostEnterProbe(void* rbx, void* rsp)
{
    dataLog("ENTRY[", vmEntryCount, "] host ENTER: rbx=",
        RawPointer(rbx), " rsp=", RawPointer(rsp), "\n");
}

extern "C" void hostExitProbe(void* rbx, void* rsp);
void hostExitProbe(void* rbx, void* rsp)
{
    dataLog("ENTRY[", vmEntryCount, "] host EXIT: rbx=",
        RawPointer(rbx), " rsp=", RawPointer(rsp), "\n");
}

extern "C" void uncaughtExceptionEnterProbe(void* rbx, void* rsp);
void uncaughtExceptionEnterProbe(void* rbx, void* rsp)
{
    dataLog("ENTRY[", vmEntryCount, "] uncaughtException ENTER: rbx=",
        RawPointer(rbx), " rsp=", RawPointer(rsp), "\n");
}

extern "C" void uncaughtExceptionExitProbe(void* rbx, void* rsp);
void uncaughtExceptionExitProbe(void* rbx, void* rsp)
{
    dataLog("ENTRY[", vmEntryCount, "] uncaughtException EXIT: rbx=",
        RawPointer(rbx), " rsp=", RawPointer(rsp), "\n");
}

dataLog()(参见 DataLog.hDataLog.cpp)是 WebKit 中 printf() 的等价物,除了:
1. 它打印到 stderr 或文件。
2. 它不接受格式化字符串。它只接受一个可变的项目列表进行打印。
3. 它知道如何打印任何实现了 dump() 方法的 WebKit 类(例如 ScopeOffset::dump())或具有关联的 printInternal() 函数(例如 CodeType 的这个)。

我们还应该在 JITCode::execute() 中添加几行来计数我们进入 VM 的深度:

int vmEntryCount = 0;
...
JSValue JITCode::execute(VM* vm, ProtoCallFrame* protoCallFrame)
{
    ...
    vmEntryCount++;
    JSValue result = JSValue::decode(vmEntryToJavaScript(entryAddress, vm, protoCallFrame));
    vmEntryCount--;
    return vm->exception() ? jsNull() : result;
}

有了这个,我们现在可以看到 %rbx 在 VM 每个入口和出口处的值。于是,我们重建 WebKit 并重新运行测试。以下是所得日志的摘录:

...
ENTRY[1] jsEntry ENTER: rbx=0x7fff5fbfa0a0 rsp=0x7fff5fbf9f70
...
ENTRY[2] jsEntry ENTER: rbx=0x7fff5fbf5a00 rsp=0x7fff5fbf58d0
ENTRY[2] host ENTER: rbx=0x7fff5fbf2cc0 rsp=0x7fff5fbf2940
ENTRY[2] host EXIT: rbx=0x7fff5fbf2cc0 rsp=0x7fff5fbf2940
ENTRY[2] jsEntry EXIT: rbx=0x7fff5fbf5a00 rsp=0x7fff5fbf58d0
...
ENTRY[2] host ENTER: rbx=0x7fff5fbf2ca0 rsp=0x7fff5fbf2920
ENTRY[2] host EXIT: rbx=0x7fff5fbf2ca0 rsp=0x7fff5fbf2920

Optimized isSymbol#BTGpXV:[0x62d000214100->0x62d0001f8aa0, BaselineFunctionCall, 48 (ShouldAlwaysBeInlined)] with Baseline JIT into 2177 bytes in 1.188317 ms.

...
ENTRY[2] host ENTER: rbx=0x7fff5fbf2ca0 rsp=0x7fff5fbf2920
ENTRY[2] host EXIT: rbx=0x7fff5fbf2ca0 rsp=0x7fff5fbf2920
...
ENTRY[2] host ENTER: rbx=0x7fff5fbf4860 rsp=0x7fff5fbf44e0
ENTRY[2] host EXIT: rbx=0x7fff5fbf4860 rsp=0x7fff5fbf44e0
...
ENTRY[2] jsEntry ENTER: rbx=0x7fff5fbf6920 rsp=0x7fff5fbf67f0
ENTRY[2] jsEntry EXIT: rbx=0x7fff5fbf6920 rsp=0x7fff5fbf67f0
ENTRY[2] jsEntry ENTER: rbx=0x7fff5fbf5380 rsp=0x7fff5fbf5250
...
ENTRY[2] jsEntry EXIT: rbx=0x7fff5fbf5380 rsp=0x7fff5fbf5250

ENTRY[1] uncaughtException ENTER: rbx=0x7fff5fbfa0a0 rsp=0x7fff5fbf9ec0
ENTRY[1] uncaughtException EXIT: rbx=0x7fff5fbf6280 rsp=0x7fff5fbf9f70
*** CRASHED here ***

查看从崩溃点开始的倒序日志,我们发现以下内容:
1. 在我们崩溃于 JITCode::execute() 之前的最后一个出口点是来自 ENTRY[1]_handleUncaughtException 处理器。
2. 当我们进入 _handleUncaughtException%rbx 的值为 0x7fff5fbfa0a0,这与我们上次进入 VM(在 ENTRY[1] 处)时的 %rbx 值相符。
3. 然而,当我们退出 _handleUncaughtException 时,%rbx 的值是一个不同(且不正确)的值,0x7fff5fbf6280

%rbx 的值在进入和退出 _handleUncaughtException 之间是如何被破坏的?

查看 _handleUncaughtException(在 LowLevelInterpreter64.asm 中),我们看到有一个对 restoreCalleeSavesFromVMCalleeSavesBuffer() 宏的调用。restoreCalleeSavesFromVMCalleeSavesBuffer()(在 LowLevelInterpreter.asm 中)基本上将值从 VM::calleeSaveRegistersBuffer 缓冲区复制到被调用者保存寄存器。这解释了为什么 %rbx 的值在 _handleUncaughtException 中改变了。剩下的问题是那个坏值是如何进入 VM::calleeSaveRegistersBuffer 缓冲区的。

谁在修改 VM::calleeSaveRegistersBuffer?

在源代码中快速搜索 calleeSaveRegistersBuffer 后,我们看到 VM::calleeSaveRegistersBuffer 仅在少数几个地方被写入:

  1. LowLevelInterpreter.asm 中的 copyCalleeSavesToVMCalleeSavesBuffer() 宏。
  2. Interpreter.cpp 中的 UnwindFunctor::copyCalleeSavesToVMCalleeSavesBuffer()
  3. FTLOSRExitCompiler.cpp 中的 compileStub() 发出代码写入缓冲区。
  4. AssemblyHelpers.h 中的 AssemblyHelpers::copyCalleeSavesToVMCalleeSavesBuffer()AssemblyHelpers::copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer() 发出代码写入缓冲区。

记录 VM::calleeSaveRegistersBuffer 的修改

接下来要做的是记录写入 VM::calleeSaveRegistersBuffer 的值。首先,我们可以排除 FTLOSRExitCompiler.cpp 中的那个,因为我们已经为测试禁用了 FTL。

对于 LowLevelInterpreter.asm 中的 copyCalleeSavesToVMCalleeSavesBuffer() 宏,我们可以添加一个 LLInt 探测,如下所示:

macro copyCalleeSavesToVMCalleeSavesBuffer(vm, temp)
    if ARM64 or X86_64 or X86_64_WIN
        leap VM::calleeSaveRegistersBuffer[vm], temp
        if ARM64
            probe(_llintSavingRBXProbe)
            storep csr0, [temp]
            storep csr1, 8[temp]
            ...

……以及在 LLIntSlowPath.cpp 底部添加以下内容:

extern "C" void llintSavingRBXProbe(void* rbx, void* rsp);
void llintSavingRBXProbe(void* rbx, void* rsp)
{
    dataLog("ENTRY[", vmEntryCount, "] LLInt set the saved %rbx to ",
        RawPointer(rbx), "\n");
}

UnwindFunctor::copyCalleeSavesToVMCalleeSavesBuffer() 是 C++ 代码。所以我们只需添加一些日志记录,如下所示:

extern int vmEntryCount;
...
void copyCalleeSavesToVMCalleeSavesBuffer(StackVisitor& visitor) const
{
    ...
    unsigned registerCount = currentCalleeSaves->size();
    for (unsigned i = 0; i < registerCount; i++) {
        RegisterAtOffset currentEntry = currentCalleeSaves->at(i);
        ...
        vm.calleeSaveRegistersBuffer[vmCalleeSavesEntry->offsetAsIndex()] = *(frame + currentEntry.offsetAsIndex());
        // Begin logging.
        auto reg = currentEntry.reg();
        if (reg.isGPR() && reg.gpr() == X86Registers::ebx) {
            void* rbx = reinterpret_cast<void*>(
                vm.calleeSaveRegistersBuffer[vmCalleeSavesEntry->offsetAsIndex()]);
            dataLog("ENTRY[", vmEntryCount, "] UnwindFunctor set the saved %rbx to ",
                RawPointer(rbx), "\n");
        }
        // End logging.
    }
    ...
}

从 JIT 代码打印

对于 AssemblyHelpers::copyCalleeSavesToVMCalleeSavesBuffer()AssemblyHelpers::copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer(),我们正在处理 JIT 生成的代码。

JSC 有自己的宏汇编器,JIT 用它来为编译后的 JS 函数发出机器指令。宏汇编器提供了用于生成机器指令或由一系列机器指令组成的伪指令的发射器函数。

print 发射器就是这样一个仅用于调试的伪指令发射器:

template<typename... Arguments>
void print(Arguments... args);

print 接受一个逗号分隔的参数列表,它将连接并打印到 stderr。除了打印常见数据类型(如 const char* 字符串和 int)外,它还知道如何打印 CPU 寄存器、内存位置的运行时值,或转储所有寄存器。更多详细信息请参见 MacroAssemblerPrinter.h

要使用 print,请将 ENABLE_MASM_PROBE(在 Platform.h 中)设置为非零值,并在您的文件中包含 MacroAssemblerPrinter.h。以下是我们在 AssemblyHelpers.h 中使用它的方式:

#include "MacroAssemblerPrinter.h"
...
    void copyCalleeSavesToVMCalleeSavesBuffer(const TempRegisterSet& usedRegisters = { RegisterSet::stubUnavailableRegisters() })
    {
#if NUMBER_OF_CALLEE_SAVES_REGISTERS > 0
        ...
        for (unsigned i = 0; i < registerCount; i++) {
            RegisterAtOffset entry = allCalleeSaves->at(i);
            ...
            if (entry.reg().isGPR()) {
                // Begin logging.
                auto entryGPR = entry.reg().gpr();
                if (entryGPR == X86Registers::ebx) {
                    print("ENTRY[", MemWord(AbsoluteAddress(&vmEntryCount)),
                        "] AH::copyCalleeSavesToVMCalleeSavesBuffer set the saved %rbx to ",
                        entryGPR, "\n");
                }
                // End logging.
                storePtr(entry.reg().gpr(), Address(temp1, entry.offset()));
            } else
                ...

    void copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer(const TempRegisterSet& usedRegisters = { RegisterSet::stubUnavailableRegisters() })
    {
#if NUMBER_OF_CALLEE_SAVES_REGISTERS > 0
        ...
        RegisterAtOffsetList* currentCalleeSaves = codeBlock()->calleeSaveRegisters();
        ...
        for (unsigned i = 0; i < registerCount; i++) {
            RegisterAtOffset vmEntry = allCalleeSaves->at(i);
            ...
            if (vmEntry.reg().isGPR()) {
                GPRReg regToStore;
                if (currentFrameEntry) {
                    // Load calleeSave from stack into temp register
                    regToStore = temp2;
                    loadPtr(Address(framePointerRegister, currentFrameEntry->offset()), regToStore);
                } else
                    // Just store callee save directly
                    regToStore = vmEntry.reg().gpr();

                // Begin logging.
                if (vmEntry.reg().gpr() == X86Registers::ebx) {
                    print("ENTRY[", MemWord(AbsoluteAddress(&vmEntryCount)),
                        "] AH::copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer set the saved %rbx to ",
                        regToStore, "\n");
                }
                // End logging.
                storePtr(regToStore, Address(temp1, vmEntry.offset()));
            } else {
                ...

在上面,我们使用 print 来记录一个看起来像这样的字符串:

ENTRY[0x10cddf280:<0x00000002 2>] AH::copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer set the saved %rbx to ebx:<0x7fff57a75bc0 140734663973824>

请注意,在 copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer() 中,传递给 printMemWord(AbsoluteAddress(&vmEntryCount)) 参数被打印为 0x10cddf280:<0x00000002 2>0x10cddf280 是地址 &vmEntryCount,而 <0x00000002 2> 是在执行 print 生成的代码时在该地址找到的运行时 int 值。

类似地,regToStore 变量被打印为 ebx:<0x7fff57a75bc0 140734663973824>。尽管 regToStore 在 JIT 编译时是一个变量,但 print 将其值捕获为要打印的寄存器 ID。在此示例中,那是 %rbx 寄存器。它被打印为 ebx 是因为 ebx 是宏汇编器用来表示 WebKit 的 32 位 x86 上的 %ebx 寄存器和 64 位 X86_64 端口上的 %rbx 寄存器的 ID(请参阅 X86Assembler.h 中的 RegisterID 枚举列表)。0x7fff57a75bc0 是在执行为 print 生成的代码时 %rbx 寄存器中的值。

追踪线索

添加了所有这些日志代码后,日志输出中有趣的部分现在看起来像这样:

...
ENTRY[0x10a92d280:<0x00000002 2>] AH::copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer set the saved %rbx to ebx:<0x7fff59f212e0 140734702424800>
...
ENTRY[0x10a92d280:<0x00000002 2>] AH::copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer set the saved %rbx to ebx:<0x7fff59f212e0 140734702424800>
...
ENTRY[0x10a92d280:<0x00000002 2>] AH::copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer set the saved %rbx to ebx:<0x7fff59f212e0 140734702424800>
...
ENTRY[1] uncaughtException ENTER: rbx=0x7fff59f25100 rsp=0x7fff59f24f20
ENTRY[1] uncaughtException EXIT:  rbx=0x7fff59f212e0 rsp=0x7fff59f24fd0
ASAN:SIGSEGV

看来 copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer() 是唯一一个向 VM::calleeSaveRegistersBuffer 写入数据,并用 0x7fff59f212e0 覆盖 %rbx 的保存值(该值与我们在崩溃点之前在 _handleUncaughtException 中恢复到 %rbx 的损坏值匹配)。日志还显示损坏发生不止一次(实际上,比我选择包含在上述日志摘录中的 3 次要多得多)。

快速搜索 copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer() 发现它仅从两个函数中调用:JIT::emitEnterOptimizationCheck()JIT::emitSlow_op_loop_hint()。这两个函数都为基线 JIT 生成代码。

向这些函数添加一些 print,我们发现 JIT::emitEnterOptimizationCheck() 是我们损坏的来源。

从这里开始,通过更多的日志记录和代码搜索,我们将发现拼凑整个损坏故事所需的其余细节。所以,这是崩溃发生的故事:

  1. 测试代码深入进入 VM 2 层。
  2. 在第 1 层进入期间,抛出一个未捕获的 JS 异常。这导致 %rbx 被保存到 VM::calleeSaveRegistersBuffer 中。
  3. 第 2 层进入是为了 Web Inspector,它正在检查抛出的异常。在第 2 层进入期间,Web Inspector 大量调用 isSymbol#BTGpXV,从而使其成为一个热函数。
  4. 因为 isSymbol#BTGpXV 很热,VM 尝试对其进行 DFG 编译(通过 JIT::emitEnterOptimizationCheck()),但失败了。这就是为什么必须启用 DFG,尽管没有函数被 DFG 编译。
  5. JIT::emitEnterOptimizationCheck() 发出的代码首先将被调用者保存寄存器保存到 VM::calleeSaveRegistersBuffer(通过 copyCalleeSavesFromFrameOrRegisterToVMCalleeSavesBuffer())。因此,缓冲区中 %rbx 的值在此处被覆盖。
  6. 当执行返回到第一层入口的 _handleUncaughtException 时,它将 VM::calleeSaveRegistersBuffer 中现在已损坏的值复制到 %rbx,然后当我们尝试使用 %rbx 的值时,我们稍后会遇到崩溃。

换句话说,这个 Bug 是 VM 需要为进入 VM 的每个层级提供一个 calleeSaveRegistersBuffer 缓冲区,但它只提供了一个所有层级共用的缓冲区。如果您感兴趣,可以在此处查看修复。

总结

我们已经看到了如何结合使用 JSC 选项和日志记录来诊断 Bug 的根本原因。我们还了解到 print 伪指令可用于从 JIT 生成的代码中进行日志记录。这些工具使我们能够将 VM Bug 隔离到特定的执行引擎层级,进行转储以让我们查看 JIT 生成的工件,以及轻松添加日志记录以对 JIT 编译代码在运行时使用的寄存器和值进行细粒度检查。

如果您正在开发 JavaScriptCore 的移植版本,或者只是对探索 JSC 的内部工作原理感兴趣,我希望这些工具能对您的 JavaScriptCore 之旅有所帮助。

一如既往,如果您在使用工具时遇到任何 Bug,或有任何增强请求,请在 bugs.webkit.org 提交 Bug。也欢迎您加入 WebKit 社区并贡献 Bug 修复或自行实现增强功能。

祝好。