黑客技巧

这些技巧已从 MDN 存档:这可能已过时!

此处存档是为了保留有价值的文档,即使可能已过时,也能提供灵感。


此页面列出了一些技巧,可帮助您调查与 SpiderMonkey 相关的问题。此处列出的所有技巧都与在 SpiderMonkey 的构建文档 结束时获得的 JavaScript shell 相关。它分为两部分,一部分与调试相关,另一部分与草拟优化相关。许多技巧仅适用于 JS shell 的调试版本;它们在发布版本中不起作用。

工具

除了标准调试器之外,以下是一些可能对您有所帮助的调试工具

  • rr 是 Linux 的记录和回放确定性调试器

  • Pernosco 获取 rr 记录并添加全知调试工具来帮助您

    • 这是一项付费服务,提供免费试用

    • Mozilla 为内部开发人员提供许可证;您可以联系 Matthew Gaudet mgaudet@mozilla.com 获取详细信息

调试技巧

获取帮助(来自 JS shell)

使用 help 函数获取 shell 中所有基本函数及其说明的列表。请注意,某些函数已移动到 'os' 对象下,并且 help(os) 将仅提供有关该“命名空间”成员的简要帮助。

您还可以使用 help(/Regex/) 获取与给定正则表达式匹配的全局命名空间成员的帮助。

获取函数的字节码(来自 JS shell)

shell 有一个名为 dis 的小函数,用于转储函数的字节码及其源代码注释。在没有参数的情况下,它将转储其调用者的字节码。

js> function f () {
  return 1;
}
js> dis(f);
flags:
loc     op
-----   --
main:
00000:  one
00001:  return
00002:  stop

Source notes:
 ofs  line    pc  delta desc     args
---- ---- ----- ------ -------- ------
  0:    1     0 [   0] newline
  1:    2     0 [   0] colspan 2
  3:    2     2 [   2] colspan 9

获取函数的字节码(来自 gdb)

jsopcode.cpp 中,名为 js::DisassembleAtPC 的函数可以打印脚本的字节码。此函数的一些变体,如 js::DumpScript 等,便于调试。

打印 JS 堆栈(来自 gdb)

jsobj.cpp 中,名为 js::DumpBacktrace 的函数打印 JS 堆栈的类似 gdb 的回溯。回溯按以下顺序包含:堆栈深度、解释器帧指针(请参阅 js/src/vm/Stack.hStackFrame 类)或如果使用 IonMonkey 编译则为 (nil)、调用位置的文件和行号以及括号内的 JSScript 指针和正在执行的 jsbytecode 指针 (pc)。

$ gdb --args js
[…]
(gdb) b js::ReportOverRecursed
(gdb) r
js> function f(i) {
  if (i % 2) f(i + 1);
  else f(i + 3);
}
js> f(0)

Breakpoint 1, js::ReportOverRecursed (maybecx=0xfdca70) at /home/nicolas/mozilla/ionmonkey/js/src/jscntxt.cpp:495
495         if (maybecx)
(gdb) call js::DumpBacktrace(maybecx)
#0          (nil)   typein:2 (0x7fffef1231c0 @ 0)
#1          (nil)   typein:2 (0x7fffef1231c0 @ 24)
#2          (nil)   typein:3 (0x7fffef1231c0 @ 47)
#3          (nil)   typein:2 (0x7fffef1231c0 @ 24)
#4          (nil)   typein:3 (0x7fffef1231c0 @ 47)
[…]
#25157 0x7fffefbbc250   typein:2 (0x7fffef1231c0 @ 24)
#25158 0x7fffefbbc1c8   typein:3 (0x7fffef1231c0 @ 47)
#25159 0x7fffefbbc140   typein:2 (0x7fffef1231c0 @ 24)
#25160 0x7fffefbbc0b8   typein:3 (0x7fffef1231c0 @ 47)
#25161 0x7fffefbbc030   typein:5 (0x7fffef123280 @ 9)

请注意,您可以使用 lldb(在 Apple 删除 gdb 后,在 OSX 上需要)执行上述完全相同的操作,方法是运行 lldb -f js,然后按照其余步骤操作。

从 SpiderMonkey 48 开始,我们有一个 gdb 解卷器。此解卷器能够读取 JIT 创建的帧,并显示这些 JIT 帧之后的帧。

$ gdb --args out/dist/bin/js ./foo.js
[…]
SpiderMonkey unwinder is disabled by default, to enable it type:
        enable unwinder .* SpiderMonkey
(gdb) b js::math_cos
(gdb) run
[…]
#0  js::math_cos (cx=0x14f2640, argc=1, vp=0x7fffffff6a88) at js/src/jsmath.cpp:338
338         CallArgs args = CallArgsFromVp(argc, vp);
(gdb) enable unwinder .* SpiderMonkey
(gdb) backtrace 10
#0  0x0000000000f89979 in js::math_cos(JSContext*, unsigned int, JS::Value*) (cx=0x14f2640, argc=1, vp=0x7fffffff6a88) at js/src/jsmath.cpp:338
#1  0x0000000000ca9c6e in js::CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), JS::CallArgs const&) (cx=0x14f2640, native=0xf89960 , args=...) at js/src/jscntxtinlines.h:235
#2  0x0000000000c87625 in js::Invoke(JSContext*, JS::CallArgs const&, js::MaybeConstruct) (cx=0x14f2640, args=..., construct=js::NO_CONSTRUCT) at js/src/vm/Interpreter.cpp:476
#3  0x000000000069bdcf in js::jit::DoCallFallback(JSContext*, js::jit::BaselineFrame*, js::jit::ICCall_Fallback*, uint32_t, JS::Value*, JS::MutableHandleValue) (cx=0x14f2640, frame=0x7fffffff6ad8, stub_=0x1798838, argc=1, vp=0x7fffffff6a88, res=JSVAL_VOID) at js/src/jit/BaselineIC.cpp:6113
#4  0x00007ffff7f41395 in

请注意,当您启用解卷器时,当前版本的 gdb (7.10.1) 不会刷新回溯。因此,在您到达下一个断点之前,JIT 帧不会出现。要解决此问题,您可以使用 gdb 的记录功能,单步执行一条指令,然后使用以下 gdb 命令返回到您来自的位置

(gdb) record full
(gdb) si
(gdb) record goto 0
(gdb) record stop

如果您有核心文件,您可以以相同的方式使用 gdb 解卷器,或从命令行执行以下所有操作

$ gdb -ex 'enable unwinder .* SpiderMonkey' -ex 'bt 0' -ex 'thread apply all backtrace' -ex 'quit' out/dist/bin/js corefile

dist/bin/js-gdb.py 应该加载 gdb 解卷器并加载位于 js/src/gdb/mozilla 下 gdb 中的 python 脚本。如果 gdb 未默认加载解卷器,您可以使用 source 命令和 js-gdb.py 文件强制加载。

在生成的代码中设置断点(来自 gdb,x86 / x86-64,arm)

要在使用 IonMonkey 编译的特定 JSScript 的生成代码中设置断点,请在您感兴趣的指令上设置断点。如果您不确定要查看哪个函数,可以在 js::ion::CodeGenerator::visitStart 函数上设置断点。可选地,可以添加对 LIR 指令的 ins->id() 的条件来精确选择要查找的指令。断点到达 LIR 指令的 CodeGenerator 函数后,添加一个命令在生成的代码中生成静态断点。

$ gdb --args js
[…]
(gdb) b js::ion::CodeGenerator::visitStart
(gdb) command
>call masm.breakpoint()
>continue
>end
(gdb) r
js> function f(a, b) { return a + b; }
js> for (var  i = 0; i < 100000; i++) f(i, i + 1);

Breakpoint 1, js::ion::CodeGenerator::visitStart (this=0x101ed20, lir=0x10234e0)
    at /home/nicolas/mozilla/ionmonkey/js/src/ion/CodeGenerator.cpp:609
609     }

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00007ffff7fb165a in ?? ()
(gdb)

断点到达生成的断点后,您可以将其替换为 gdb 断点以使其成为条件断点。该过程是首先用 nop 指令替换生成的断点,然后在 nop 的地址处设置断点。

(gdb) x /5i $pc - 1
   0x7ffff7fb1659:      int3
=> 0x7ffff7fb165a:      mov    0x28(%rsp),%rax
   0x7ffff7fb165f:      mov    %eax,%ecx
   0x7ffff7fb1661:      mov    0x30(%rsp),%rdx
   0x7ffff7fb1666:      mov    %edx,%ebx

(gdb) # replace the int3 by a nop
(gdb) set *(unsigned char *) ($pc - 1) = 0x90
(gdb) x /1i $pc - 1
   0x7ffff7fb1659:      nop

(gdb) # set a breakpoint at the previous location
(gdb) b *0x7ffff7fb1659
Breakpoint 2 at 0x7ffff7fb1659

打印 Ion 生成的汇编代码(来自 gdb)

如果您想查看 IonMonkey 生成的汇编代码,可以按照以下步骤操作

  1. 在 CodeGenerator.cpp 上的 CodeGenerator::link 方法处设置断点。

  2. 连续单步执行几次,以便生成“code”变量

  3. 打印 code->code_,它是代码的地址

  4. 反汇编在该地址读取的代码(使用 x/Ni address,其中 N 是您想要查看的指令数)

以下是一个示例。使用 CodeGenerator::link 行号而不是完整限定名称来设置断点可能会更简单。例如,假设此函数的行号为 4780

(gdb) b CodeGenerator.cpp:4780
Breakpoint 1 at 0x84cade0: file /home/code/mozilla-central/js/src/ion/CodeGenerator.cpp, line 4780.
(gdb) r
Starting program: /home/code/mozilla-central/js/src/32-release/js -f /home/code/jaeger.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0xf7903b40 (LWP 12563)]
[New Thread 0xf6bdeb40 (LWP 12564)]
Run#0

Breakpoint 1, js::ion::CodeGenerator::link (this=0x86badf8)
    at /home/code/mozilla-central/js/src/ion/CodeGenerator.cpp:4780
4780    {
(gdb) n
4781        JSContext *cx = GetIonContext()->cx;
(gdb) n
4783        Linker linker(masm);
(gdb) n
4784        IonCode *code = linker.newCode(cx, JSC::ION*CODE);
(gdb) n
4785        if (!code)
(gdb) p code->code*
$1 = (uint8_t \*) 0xf7fd25a8 "\201", <incomplete sequence \354\200>
(gdb) x/2i 0xf7fd25a8
   0xf7fd25a8:    sub    $0x80,%esp
   0xf7fd25ae:    mov    0x94(%esp),%ecx

在 arm 上,编译后的 JS 代码始终为 ARM 机器代码,而 SpiderMonkey 本身通常为 Thumb2。由于 JIT 代码没有调试信息,因此您需要告诉 gdb 您正在查看 ARM 代码

(gdb) set arm force-mode arm

或者,您可以将 x 命令包装在您自己的命令中

def xi
    set arm force-mode arm
    eval "x/%di %d", $arg0, $arg1
    set arm force-mode auto
end

打印 asm.js/wasm 生成的汇编代码(来自 gdb)

  • js::wasm::Instance::callExport(截至 2016 年 11 月 18 日在 WasmInstance.cpp 中定义)上设置断点。这将触发任何 asm.js/wasm 调用,因此您应该找到一种方法来仅为要查看的生成代码设置此断点。

  • 运行程序。

  • 在 gdb 中执行 next 直到到达 funcPtr 的定义

// Call the per-exported-function trampoline created by GenerateEntry.
auto funcPtr = JS*DATA_TO_FUNC_PTR(ExportFuncPtr, codeBase() + func.entryOffset());
if (!CALL_GENERATED_2(funcPtr, exportArgs.begin(), &tlsData*))
    return false;
  • 设置完成后,x/64i funcPtr 将显示弹跳代码。在某个时刻应该有一个对某个地址的调用;这就是我们的目标。复制该地址。

   0x7ffff7ff6000:    push   %r15
   0x7ffff7ff6002:    push   %r14
   0x7ffff7ff6004:    push   %r13
   0x7ffff7ff6006:    push   %r12
   0x7ffff7ff6008:    push   %rbp
   0x7ffff7ff6009:    push   %rbx
   0x7ffff7ff600a:    movabs $0xea4f80,%r10
   0x7ffff7ff6014:    mov    0x178(%r10),%r10
   0x7ffff7ff601b:    mov    %rsp,0x40(%r10)
   0x7ffff7ff601f:    mov    (%rsi),%r15
   0x7ffff7ff6022:    mov    %rdi,%r10
   0x7ffff7ff6025:    push   %r10
   0x7ffff7ff6027:    test   $0xf,%spl
   0x7ffff7ff602b:    je     0x7ffff7ff6032
   0x7ffff7ff6031:    int3
   0x7ffff7ff6032:    callq  0x7ffff7ff5000     <------ right here
  • x/64i address(在本例中为:x/64i 0x7ffff7ff6032)。

  • 如果要在函数的入口处设置断点,可以执行:b *address(例如此处,b* 0x7ffff7ff6032)。然后,您可以使用 x/20i $pc, 显示 pc 周围的指令,并使用 stepi 一条一条地执行指令。

查找 Ion 生成的汇编代码所属的脚本(来自 gdb)

当遇到您位于 IonMonkey 生成的代码中间的错误时,首先要注意的是 gdb 的回溯不可靠,因为生成的代码不保留帧指针。要弄清楚这一点,您必须读取堆栈以推断 IonMonkey 帧。

(gdb) x /64a $sp
[…]
0x7fffffff9838: 0x7ffff7fad2da  0x141
0x7fffffff9848: 0x7fffef134d40  0x2
[…]
(gdb) p (*(JSFunction**) 0x7fffffff9848)->u.i.script_->lineno
$1 = 1
(gdb) p (*(JSFunction**) 0x7fffffff9848)->u.i.script_->filename
$2 = 0xff92d1 "typein"

堆栈的顺序如 js/src/ion/IonFrames-x86-shared.h 中所定义。它由返回地址、描述符(一个小值)、JSFunction(如果为偶数)或 JSScript(如果为奇数;将其删除以取消引用指针)组成,并且帧以实际参数的数量(一个小值)结尾。如果您想知道代码在哪个 LIR 处失败,可以对 js::ion::CodeGenerator::generateBody 函数进行检测,以便在每条指令之前转储 LIR id

for (; iter != current->end(); iter++) {
    IonSpew(IonSpew_Codegen, "instruction %s", iter->opName());
    […]

    masm.store16(Imm32(iter->id()), Address(StackPointer, -8)); // added
    if (!iter->accept(this))
        return false;

此修改将添加一条指令,该指令滥用堆栈指针 以将立即值(LIR id)存储到任何合理的编译器都永远不会生成的某个位置。因此,在使用 gdb 转储汇编时,这种指令很容易被注意到。

查看 Ion/Odin 编译的 MIR 图(来自 gdb)

通过 gdb 检测,当执行停止时,我们可以在 gdb 中调用 iongraph 程序。此检测在提供 MIRGenerator* 实例时添加了 iongraph 命令,将调用 iongraphgraphviz 和您首选的 png 查看器以在执行的精确时间显示 MIR 图。要查找 MIRGenetator* 实例,最好在堆栈中查找 OptimizeMIRCodeGenerator::generateBodyOptimizeMIR 函数有一个 mir 参数,而 CodeGenerator::generateBody 函数有一个成员 this->gen

(gdb) bt
#0  0x00000000007eaad4 in js::InlineList<js::jit::MBasicBlock>::begin() const (this=0x33dbbc0) at …/js/src/jit/InlineList.h:280
#1  0x00000000007cb845 in js::jit::MIRGraph::begin() (this=0x33dbbc0) at …/js/src/jit/MIRGraph.h:787
#2  0x0000000000837d25 in js::jit::BuildPhiReverseMapping(js::jit::MIRGraph&) (graph=...) at …/js/src/jit/IonAnalysis.cpp:2436
#3  0x000000000083317f in js::jit::OptimizeMIR(js::jit::MIRGenerator*) (mir=0x33dbdf0) at …/js/src/jit/Ion.cpp:1570
…
(gdb) frame 3
#3  0x000000000083317f in js::jit::OptimizeMIR(js::jit::MIRGenerator*) (mir=0x33dbdf0) at …/js/src/jit/Ion.cpp:1570
(gdb) iongraph mir
 function 0 (asm.js compilation): success; 1 passes.
/* open your png viewer with the result of iongraph */

此 gdb 检测工具旨在与调试版本配合使用,或与使用 --enable-jitspew 配置标志编译的优化版本配合使用。外部程序,例如 iongraphdot 和您的 png 查看器,会在 PATH 中进行搜索;否则,自定义程序可以通过环境变量 (GDB_IONGRAPHGDB_DOTGDB_PNGVIEWER) 在启动 gdb 之前进行配置,或通过 gdb 参数 (set iongraph-bin <path>set dot-bin <path>set pngviewer-bin <path>) 在 gdb 内进行配置。

启用 GDB 检测工具可能需要启动一个 JS shell 可执行文件,该文件与名为“js-gdb.py”的文件共享目录。如果 js/src/js 没有提供“iongraph”命令,请尝试使用 js/src/shell/js。GDB 可能会抱怨 ~/.gdbinit 需要修改以授权用户脚本,如果出现这种情况,它会打印出说明。

查找生成 JIT 指令的代码 (来自 rr)

如果您正在查看 JIT 指令并且需要知道是什么代码生成了它,您可以使用 jitsrc.py。此脚本向 rr 添加了一个 jitsrc 命令,该命令将从 JIT 指令向后跟踪到生成它的代码。

要使用 jitsrc 命令,请将以下行添加到您的 .gdbinit 文件中,或手动运行它

source js/src/gdb/mozilla/jitsrc.py

然后您可以像这样使用该命令:jitsrc <address of JIT instruction>

运行该命令将使应用程序停留在最初发出该 JIT 指令的执行点。例如,回溯可能包含一个位于 js::jit::MacroAssemblerX64::loadPtr 的帧。

其工作原理是在 JIT 指令上设置一个观察点,并 reverse-continue 程序执行以到达分配该内存地址的点。JIT 指令内存可能会被复制或移动,因此 jitsrc 命令会自动更新跨复制/移动的观察点,以继续返回到 JIT 指令的原始源。

在 valgrind 错误处中断

有时,错误可以在 valgrind 下重现,但在 gdb 下很难重现。一种调查方法是让 valgrind 为您启动 gdb;此处记录的另一种方法是让 valgrind 充当 gdb 服务器,可以从 gdb 远程控制该服务器。

$ valgrind --smc-check=all-non-file

此命令将告诉您如何以远程方式启动 gdb。请注意,通常会转储某些输出的函数将在启动 valgrind 的 shell 中执行此操作,而不是在启动 gdb 的 shell 中执行。因此,当从 gdb 调用时,诸如 js::DumpBacktrace 之类的函数将在包含 valgrind 的 shell 中打印其输出。

添加编译、跳出和失效的输出 (来自 gdb)

如果您在 rr 中,并且忘记使用 IONFLAGS 启用输出或因为这是一个优化版本,那么您可以在 gdb 中使用额外的断点添加类似的输出。gdb 能够使用命令设置断点,但更简单/更友好的版本是使用 dprintf,带有一个位置,然后是类似 printf 的参数。

(gdb) dprintf js::jit::IonBuilder::IonBuilder, "Compiling %s:%d:%d-%d\n", info->script*->scriptSource()->filename*.mTuple.mFirstA, info->script*->lineno*, info->script*->sourceStart*, info->script*->sourceEnd*
Dprintf 1 at 0x7fb4f6a104eb: file /home/nicolas/mozilla/contrib-push/js/src/jit/IonBuilder.cpp, line 159.
(gdb) cond 1 inliningDepth == 0
(gdb) dprintf js::jit::BailoutIonToBaseline, "Bailout from %s:%d:%d-%d\n", iter.script()->scriptSource()->filename*.mTuple.mFirstA, iter.script()->lineno*, iter.script()->sourceStart*, iter.script()->sourceEnd*
Dprintf 2 at 0x7fb4f6fe43dc: js::jit::BailoutIonToBaseline. (2 locations)
(gdb) dprintf Ion.cpp:3196, "Invalidate %s:%d:%d-%d\n", co->script*->scriptSource()->filename*.mTuple.mFirstA, co->script*->lineno*, co->script*->sourceStart*, co->script*->sourceEnd*
Dprintf 3 at 0x7fb4f6a0b62a: file /home/nicolas/mozilla/contrib-push/js/src/jit/Ion.cpp, line 3196.
`(gdb) continue`
Compiling self-hosted:650:20470-21501
Bailout from self-hosted:20:403-500
Invalidate self-hosted:20:403-500

注意:上面列出的第 3196 行对应于 jit::Invalidate 函数内部的 Jit 输出 的位置。

黑客技巧

使用 Gecko Profiler (浏览器/xpcshell)

请参阅专门介绍 使用 Gecko Profiler 进行性能分析 的部分。此性能分析方法的优点是可以将 JavaScript 堆栈与 C++ 堆栈混合,这对于分析库函数问题很有用。

一个技巧是从具有反向 JS 堆栈的脚本开始查看,以找到最昂贵的 JS 函数,然后专注于此 JS 函数的帧,并删除反向堆栈并查看此函数的 C++ 部分,以确定成本来自何处。

这些存档的 使用 Gecko Profiler 的技巧常见问题解答 也可能对您有所启发,但它们已经足够旧了,可能不再准确。

使用 callgrind (JS shell)

由于 SpiderMonkey 即时编译器会重写执行的程序,因此应通过在命令行中添加 –smc-check=all-non-file 来通知 valgrind。

$ valgrind --tool=callgrind --callgrind-out-file=bench.clg \
     --smc-check=all-non-file

然后可以使用 kcachegrind 处理输出文件,该文件提供调用图的图形视图。

使用 IonMonkey 输出 (JS shell)

IonMonkey 输出非常详细(不像 INFER 输出那么详细),但您可以对其进行过滤以专注于已编译脚本或通道的列表,可以使用 IONFLAGS 环境变量选择 IonMonkey 输出通道,并且可以使用 IONFILTER 过滤编译输出。

IONFLAGS 包含 用逗号分隔的每个通道的名称logs 通道生成一个文件(/tmp/ion.json),旨在与 iongraph(由 Sean Stangl 制作)一起使用。此工具将显示 IonMonkey 在编译期间执行的 MIR 和 LIR 步骤。要使用 iongraph,您必须安装 Graphviz

可以使用 IONFILTER 环境变量过滤编译日志和输出,该变量包含其他输出通道输出的位置。可以使用逗号作为分隔符指定多个位置。

$ IONFILTER=pdfjs.js:16934 IONFLAGS=logs,scripts,osi,bailouts ./js --ion-offthread-compile=off ./run.js 2>&1 | less

bailouts 通道可能是您首先应该关注的内容,因为这意味着某些内容没有保留在 IonMonkey 中并回退到解释器。此通道输出最新 MIR 和最新 LIR 阶段的位置(作为 id() 函数返回的指令)。这些位置应对应于 logs 的阶段,并且可以使用过滤器删除不感兴趣的函数。

使用 ARM 模拟器

ARM 模拟器可用于在 x86/x64 硬件上测试 ARM JIT 后端。ARM 模拟器构建是一个 x86 shell(或浏览器),具有 ARM JIT 后端。它不会进入 JIT 代码,而是在 ARM 代码的模拟器(解释器)中运行它。要使用模拟器,请编译 x86 shell(32 位,x64 不起作用,因为我们在那里使用不同的 Value 格式),并将 –enable-arm-simulator 传递给 configure。例如,在 64 位 Linux 主机上,您可以使用以下 configure 命令获取 ARM 模拟器构建

AR=ar CC="gcc -m32" CXX="g++ -m32" ../configure --target=i686-pc-linux
--enable-debug --disable-optimize --enable-threadsafe --enable-simulator=arm

或在 OS X 上

$ AR=ar CC="clang -m32" CXX="clang++ -m32" ../configure --target=i686-apple-darwin10.0.0 --enable-debug --disable-optimize --enable-threadsafe --enable-arm-simulator

如果您想运行 jit-tests 或 jstests,建议使用 –enable-debug –enable-optimize 构建。

在模拟器中使用 VIXL 调试器 (arm64)

设置断点(请参阅上面有关在生成的代码中设置断点部分),并使用环境变量 USE_DEBUGGER=1 运行。然后,这将使您进入 VIXL 提供的简单调试器,VIXL 是用于 arm64 模拟的 ARM 模拟器技术。

使用模拟器调试器进行 arm32

上一节中 arm64 的相同说明适用,但环境变量不同:使用 ARM_SIM_DEBUGGER=1

使用 ARM 模拟器构建浏览器

您还可以使用 ARM 模拟器后端构建整个浏览器,例如在 ARM 上重现仅浏览器的 JS 故障。确保为 x86(32 位)构建浏览器,并将此选项添加到您的 mozconfig 文件中

ac_add_options –enable-arm-simulator

如果您在 Ubuntu 或 Debian 64 位发行版下并且想要构建 32 位浏览器,则可能很难找到相关的 32 位依赖项。您可以使用 padenot 的脚本,它将神奇地设置一个 chrooted 32 位环境并为您执行所有操作 (c)(您只需要修改 mozconfig 文件)。

在测试中使用 rr

使用 -s 获取测试运行的命令行

./jit_test.py -s $JS_SHELL saved-stacks/async.js

在 shell 调用之前插入“rr”

rr $JS_SHELL -f $JS_SRC/jit-test/lib/prolog.js --js-cache $JS_SRC/jit-test/.js-cache -e "const platform='linux2'; const libdir='$JS_SRC/jit-test/lib/'; const scriptdir='$JS_SRC/jit-test/tests/saved-stacks/'" -f $JS_SRC/jit-test/tests/saved-stacks/async.js

(请注意,以上只是一个示例;仅设置 JS_SHELL 和 JS_SRC 将不起作用)。或者,如果这是一个间歇性问题,请在循环中运行它,为每次运行捕获一个 rr 日志,直到它失败为止

n=1; while rr ...same.as.above...; do echo passed $n; n=$(( $n + 1 )); done

等到它出现故障。现在您可以运行 rr replay 以在 gdb 下重放上次(失败的)运行。

带有 reftest 的 rr

在不同的像素写入处中断

  1. 找到不同的像素的 X/Y

  2. 使用 run Z,其中 Z 是日志中 TEST-START 的标记。例如,在“ [rr 28496 607198]REFTEST TEST-START | file:///home/bgirard/mozilla-central/tree/image/test/reftest/bmp/bmpsuite/b/wrapper.html?badpalettesize.bmp”中,Z 将是 607198。

  3. break 'mozilla::dom::CanvasRenderingContext2D::DrawWindow(nsGlobalWindow&, double, double, double, double, nsAString_internal const&, unsigned int, mozilla::ErrorResult&)'

  4. cont

  5. break 'PresShell::RenderDocument(nsRect const&, unsigned int, unsigned int, gfxContext\*)'

  6. set print object on

  7. set $x = <YOUR X VALUE>

  8. set $y = <YOUR Y VALUE>

  9. print &((cairo_image_surface_t*)aThebesContext->mDT.mRawPtr->mSurface).data[$y * ((cairo_image_surface_t*)aThebesContext->mDT.mRawPtr->mSurface).stride + $x * ((cairo_image_surface_t\*)aThebesContext->mDT.mRawPtr->mSurface).depth / 8]

  10. watch *(char*)<ADDRESS OF PREVIOUS COMMAND>(注意:如果您在前面的表达式上设置了一个观察点,gdb 将监视该表达式并用完观察点)

带有 emacs 的 rr

在 emacs 中,执行 M-x gud-gdb 并将命令行替换为 rr replay。当 gdb 启动时,输入

set annot 1

以使其发出文件位置信息,以便 emacs 会弹出相应的源代码。请注意,如果您 reverse-continue 超过 SIGSEGV 并且您正在使用设置该信号的捕获点的标准 .gdbinit,您将在捕获点处获得额外的停止。只需 reverse-continue 再次继续到您的断点或任何其他内容。

[黑客] 替换一条指令

要替换特定的一条指令,您可以使用 JSScript 的 filenamelineno 字段中自定义指令的访问函数,以及 LIR/MIR 指令的 id()。JSScript 可以从 info().script() 获得。

bool
CodeGeneratorX86Shared::visitGuardShape(LGuardShape *guard)
{
    if (info().script()->lineno == 16934 && guard->id() == 522) {
        [… another impl only for this one …]
        return true;
    }
    [… old impl …]

[黑客] 输出所有已编译代码

我通常只是将其添加到相应的 executableCopy() 函数中。

if (getenv("INST_DUMP")) {
    char buf[4096];
    sprintf(buf, "gdb /proc/%d/exe %d -batch -ex 'set pagination off' -ex 'set arm force-mode arm' -ex 'x/%di %p' -ex 'set arm force-mode auto'", getpid(), getpid(), m_buffer.size() / 4, buffer);
    system(buf);
}

如果您没有运行在 arm 架构上,则应省略 -ex 'set arm force-mode arm'-ex 'set arm force-mode auto'。并且您应该将 size()/4 更改为更适合您架构的值。

使用毫秒级以下时间进行基准测试 (JS shell)

在 shell 中,我们有两种简单的方法来对脚本进行基准测试。我们可以使用 **-b** shell 选项(**–print-timing**),它将在命令行上执行给定的脚本,无需任何基准测试工具,并打印一行显示脚本运行时间的额外信息。另一种方法是用 **dateNow()** 函数调用包装要测量的部分,该函数返回毫秒数,并包含小数部分表示毫秒级以下的时间。

js> dateNow() - dateNow()
-0.0009765625

Firefox 61 开始,shell 也提供了 **performance.now()**。

使用毫秒级以下时间进行基准测试 (浏览器)

类似于在 JS shell 中使用 **dateNow()** 的方式,您可以在页面的 JavaScript 代码中使用 **performance.now()**。

转储 JavaScript 堆

在 shell 中,您可以调用 dumpHeap 函数来转储堆中所有 GC 对象(可到达的和不可到达的)。默认情况下,该函数写入标准输出,但可以指定文件名作为参数。

示例输出可能如下所示

0x1234abcd B global object

输出为文本格式。文件的第一个部分包含根节点列表,每行一个。每个根节点都具有以下格式:“0xabcd1234 <color> <description>”,其中 <color> 是给定 GC 对象的颜色(B 表示黑色,G 表示灰色,W 表示白色),<description> 是一个字符串。根节点列表以包含“=========="的行结尾。

在根节点之后是一系列区域。一个区域以几个以哈希开头的“注释行”开头。第一个注释声明区域。后面是列出区域内每个隔间的行。在所有隔间之后是 arena,GC 对象实际上存储在其中。每个 arena 后面都是 arena 中的所有 GC 对象。一个 GC 对象以一行开头,给出其地址、颜色和对象类型(对象、函数等)。在此之后是 GC 对象指向的一系列地址,每个地址都以“>”开头。

也可以使用 js::DumpHeap 函数从 C++ 代码(或 gdb)中转储 JavaScript 堆。它是 jsfriendapi.h 的一部分,在发布版本中可用。

在调试器中检查 MIR 对象

对于 MIRGraph、MBasicBlock 和 MDefinition 及其子类(MInstruction、MConstant 等),请调用 dump 成员函数。

(gdb) call graph->dump()
(gdb) call block->dump()
(gdb) call def->dump()

如何调试 oomTest() 失败

oomTest() 函数多次执行一段代码,在它进行的每次连续分配中模拟 OOM 错误。它旨在突出显示错误的 OOM 处理,这可能会在稍后的某个时间点显示为崩溃或断言失败。

在调试此类崩溃时,最有用的是找到最后一次模拟的分配失败,因为这通常是导致后续崩溃的原因。

我的工作流程如下

  1. 使用 --enable-debug--enable-oom-breakpoint 配置标志构建引擎版本。

  2. 设置环境变量 OOM_VERBOSE=1 并重现故障。这将在每次模拟失败时打印分配计数。记下最后一次分配的计数。

  3. 在调试器下运行引擎,并在函数 js_failedAllocBreakpoint 上设置断点。

  4. 运行程序并 continue 必要次数,直到到达最终分配。

    • 例如,在 lldb 中,如果显示的分配失败编号为 1500,则运行 continue -i 1498(减去 2,因为我们已经遇到过一次,并且不想跳过最后一次)。对于 gdb,请删除“-i”。

  5. 转储回溯。这应该会显示您错误处理 OOM 的位置,该位置将位于断点上方几帧。

注意:如果您使用的是 Linux,则使用 rr 可能更简单。

一些处理 OOM 的指导原则,如果不遵循这些原则会导致故障

  1. 检查分配失败!

    • 易出错的分配必须始终进行检查和处理,至少要向调用方返回一个指示失败的状态。

  2. 如果您有上下文,则向上下文报告 OOM

    • 如果一个函数具有 JSContext* 参数,通常它应该在分配失败时调用 js::ReportOutOfMemory(cx) 来向上下文报告此情况。

  3. 有时忽略 OOM是可以的

    • 例如,如果您正在执行推测性优化,您可能会放弃它并继续执行。在这种情况下,您可能需要调用 cx->recoverFromOutOfMemory(),如果堆栈中更下层的部分已经报告了失败。

调试 GC 标记/根节点

js::debug 命名空间包含一些函数,这些函数可用于监视单个 JSObject*(或任何 Cell*)的标记位。 js/src/gc/Heap.h 包含一个描述示例用法的注释。此处复制

// Sample usage from gdb:
//
//   (gdb) p $word = js::debug::GetMarkWordAddress(obj)
//   $1 = (uintptr_t *) 0x7fa56d5fe360
//   (gdb) p/x $mask = js::debug::GetMarkMask(obj, js::gc::GRAY)
//   $2 = 0x200000000
//   (gdb) watch *$word
//   Hardware watchpoint 7: *$word
//   (gdb) cond 7 *$word & $mask
//   (gdb) cont
//
// Note that this is *not* a watchpoint on a single bit. It is a watchpoint on
// the whole word, which will trigger whenever the word changes and the
// selected bit is set after the change.
//
// So if the bit changing is the desired one, this is exactly what you want.
// But if a different bit changes (either set or cleared), you may still stop
// execution if the $mask bit happened to already be set. gdb does not expose
// enough information to restrict the watchpoint to just a single bit.

大多数情况下,您将希望将 js::gc::BLACK(或者您可以只使用 0)作为 js::debug::GetMarkMask 的第二个参数。