原文地址:https://liveoverflow.com/just-in-time-compiler-in-javascriptcore-browser-0x03/
Introduction
在上一篇文章中,我们探讨了JavaScriptCore(来自WebKit的JavaScript引擎)是如何在内存中存储对象和数值的。在这篇文章中,我们将跟大家一起来探索JIT,即Just-In-Time编译器。
The Just-In-Time compiler
说到Just-In-Time编译器,这可是一个复杂的主题。但简单地说,JIT编译器的作用就是将JavaScript字节码(由JavaScript虚拟机执行)编译为本机机器代码(程序集)。虽然这个过程与C代码的编译过程非常相似,但是对于JavaScriptCore中的JIT来说,还是有许多独特之处的。
为了深入学习JIT,我曾经专门就此向Linus请教,他给出的建议是,多看官方的WebKit资源,例如JavaScriptCore CSI:A Crash Site Investigation Story,这篇文章在如何调试崩溃并诊断错误以查找根本原因方面给出了具体的操作方法——当我们在JavaScriptCore中挖掘漏洞的时候,这方面的内容是非常有用的。除此之外,这篇文章还介绍了如何创建WebKit的地址过滤程序,以用于捕获潜在的堆溢出或UAF漏洞。
$ cd webkitDIr
$ ./Tools/Scripts/set-webkit-configuration --asan
$ ./Tools/Scripts/build-webkit --debug
这些内容虽然很酷,但我们对JIT方面的内容更感兴趣,所以,让我们找一些对我们的研究有用的东西。在上面提到的文章中,我们可以找到有关JIT编译器相关介绍。
JSC是一种支持多级优化的执行引擎。
实际上,它共有4级:
- 第1级:LLInt解释器
- 第2级:Baseline JIT编译器
- 第3级:DFG JIT
- 第4级:FTL JIT
第1级:LLInt解释器
这是常规的JavaScript解释器,实际上就是一个基本的JavaScript虚拟机。为了加深读者的立即,我们将通过源代码进行讲解。通过快速浏览LowLevelInterpreter.cpp源文件,我们会看到解释器的主循环语句,该循环用于遍历提供的JavaScript字节码,并执行各条指令。
//===================================================================================
// The llint C++ interpreter loop:
// LowLevelInteroreter.cpp
JSValue Cloop::execute(OpcodeID entry OpcodeID, void* executableAddress,
VM* vm, ProtoCallFrame* protoCallFrame, bool isInitializationPass)
{
// Loop Javascript bytecode and execute each instruction
// [...] snip
}
第2级:Baseline JIT编译器
当一个函数被多次调用的时候,我们就可以称这个函数时一个“热门的(hot)”函数。这个术语用于表示其执行次数较多,而JavaScriptCore会将这种类型的函数JIT化。如果我们查看JIT.cpp源文件,我们可以进一步获得更加详细的信息。
void JIT::privateCompileMainPass()
{
...
// When the LLInt determines it wants to do OSR entry into the baseline JIT in a loop,
// it will pass in the bytecode offset it was executing at when it kicked off our
// compilation. We only need to compile code for anything reachable from that bytecode
// offset.
...
}
当前栈替换(On Stack Replacement,OSR)是一种用于在同一函数的不同实现之间进行切换的技术。例如,OSR可以在执行期间从动态解释代码(未优化代码)切换为对应的JIT编译代码。关于OSR的更多介绍,请参阅这里。
然而,在这个阶段,机器代码仍然与原始字节码非常兼容的(我不知道使用这个术语进行描述是否恰当)的,并且还没有进行真正的优化处理。
第3级:DFG JIT
另一篇介绍WebKit FTL JIT的文章,曾经指出:
任何函数的第一次执行总是从解释器级开始的。不过,一旦函数中的某个语句执行次数超过了100次,或者某个函数被调用次数超过6次(以先到者为准),执行权就会转移给Baseline JIT编译的代码。这种做法虽然能够降低解释器的开销,但并没有进行真正意义上的编译器优化。一旦任何语句在Baseline代码中执行次数超过1000次,或者Baseline函数被调用次数超过66次,我们会再次将执行权转移给DFG JIT编译的代码。