mksnapshot是v8编译过程中的一个中间产物,看名字平平无奇,也甚少文章着重介绍它,但实际上它并不是它名字表述那样只是生成个快照,而是内藏玄机:
我们以Array.isArray的实现来讲解下mksnapshot扮演的角色。
首先, Array.isArray是用一个叫torque的语言来写的,有点类似js的语法,只在v8中使用,Array.isArray的实现如下:
namespace runtime {
extern runtime ArrayIsArray(implicit context: Context)(JSAny): JSAny;
} // namespace runtime
namespace array {
// ES #sec-array.isarray
javascript builtin ArrayIsArray(js-implicit context: NativeContext)(arg: JSAny):
JSAny {
// 1. Return ? IsArray(arg).
typeswitch (arg) {
case (JSArray): {
return True;
}
case (JSProxy): {
// TODO(verwaest): Handle proxies in-place
return runtime::ArrayIsArray(arg);
}
case (JSAny): {
return False;
}
}
}
} // namespace array
经过torque编译器后,会生成一段很复杂的C++代码,我截取一个片段
TNode<JSProxy> Cast_JSProxy_1(compiler::CodeAssemblerState* state_, TNode<Context> p_context, TNode<Object> p_o, compiler::CodeAssemblerLabel* label_CastError) {
// other code ...
if (block0.is_used()) {
ca_.Bind(&block0);
ca_.SetSourcePosition("../../src/builtins/cast.tq", 162);
compiler::CodeAssemblerLabel label1(&ca_);
tmp0 = CodeStubAssembler(state_).TaggedToHeapObject(TNode<Object>{p_o}, &label1);
ca_.Goto(&block3);
if (label1.is_used()) {
ca_.Bind(&label1);
ca_.Goto(&block4);
}
}
// other code ...
}
这是跑在运行时的Array.isArray的C++实现?
错了!这段代码只跑在mksnapshot里头,这段代码的产物是turbofan的IR。IR经过turbofan的优化编译后生成目标机器指令,然后dump到embedded.S汇编文件,如下才是跑在运行时的Array.isArray:
Builtins_ArrayIsArray:
.type Builtins_ArrayIsArray, %function
.size Builtins_ArrayIsArray, 214
.octa 0xd10043ff910043fda9017bfda9be6fe1,0x540003a9eb2263fff8560342f81e83a0
.octa 0x7840b063f85ff04336000182f9401be2,0x14000007d2800003540000607110907f
.octa 0x910043ffa8c17bfd910003bff85b8340,0x35000163d2800020d2800023d65f03c0
.octa 0x540000e17102d47f7840b063f85ff043,0xf94da741f90003e2f90007ffd10043ff
.octa 0x17ffffeef85c034017fffff097ffb480,0xaa1b03e2f9501f41d2800000f90003fb
.octa 0x17ffffddf94003fb97ffb477aa0003e3,0x840000000100000002d503201f
.octa 0xffffffff000000a8ffffffffffffffff
.byte 0xff,0xff,0xff,0xff,0x0,0x1,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc
在这个过程中,jit编译器turbofan干的是AOT的活。可以类比llvm之类的编译器架构,turbofan就类似llvm后端(ps,llvm我记得也支持jit)。
在一般的库,所谓交叉编译就是调用改目标平台指定的工具链直接编译源码生成目标平台的文件。比如一个C文件要给android用,调用ndk包的gcc、clang编译即可。
但我们前面说过,builtin实际用的是v8自己的工具链体系编译成目标平台的代码,所以并不能套用上面的方式,它是怎么实现呢?
首先,turbofan生成机器指令的部分是可以替换的,比如链接http://builtins-x64.cc生成的是x64指令,链接http://builtins-arm64.cc生成的arm64指令。
以在linux x64上交叉编译android arm64的builtin为例,步骤如下:
在embedded.S里的builtins是怎么起作用的呢?
首先embedded.S声明了四个全局变量,分别是:
在http://isolate.cc声明几个extern变量,于是链接后http://isolate.cc就能引用到那几个变量:
extern "C" const uint8_t* v8_Default_embedded_blob_code_;
extern "C" uint32_t v8_Default_embedded_blob_code_size_;
extern "C" const uint8_t* v8_Default_embedded_blob_data_;
extern "C" uint32_t v8_Default_embedded_blob_data_size_;
前面也说过,v8_Default_embedded_blob_data_包含了各builtin的偏移,这些偏移组成一个数组,放在isolate的builtin_entry_table,数组下标是该builtin的枚举值。调用某builtin就是builtin_entry_table通过枚举值获取起始地址调用。
如果不是交叉编译,snapshot生成还是挺容易理解的:v8对各种对象有做了序列化和反序列化的支持,所谓生成snapshot,就是序列化,通常会以context作为根来序列化。
生成snapshot前允许执行一段代码(以及其warnup代码),这段代码调用到的函数的编译结果也会序列化下来,后续反序列化后,就免去了编译过程,大大加快的启动的速度,以nodejs为例,采用snapshot能数倍加快其启动速度。
结合交叉编译时就会有个很费解的地方:我们前面提到mksnapshot在交叉编译时,jit生成的builtin是目标机器指令,而js的运行得通过跑builtin来实现(Ignition解析器每个指令就是一个builtin),这目标机器指令(比如arm64)怎么在本地(linux 的x64)跑起来呢?
通过调试才知道交叉编译时,mksnapshot会用一个目标机器的模拟器来跑这些builtin:
//src\common\globals.h
#if !defined(USE_SIMULATOR)
#if (V8_TARGET_ARCH_ARM64 && !V8_HOST_ARCH_ARM64)
#define USE_SIMULATOR 1
#endif
// ...
#endif
//src\execution\simulator.h
#ifdef USE_SIMULATOR
Return Call(Args... args) {
// other code ...
return Simulator::current(isolate_)->template Call<Return>(
reinterpret_cast<Address>(fn_ptr_), args...);
}
#else
DISABLE_CFI_ICALL Return Call(Args... args) {
// other code ...
}
#endif // USE_SIMULATOR
如果交叉编译,将会走USE_SIMULATOR分支。arm64将会调用到simulator-arm64.h, http://simulator-arm64.cc实现的模拟器里头。上面Call的处理是把指令首地址赋值到模拟器的_pc寄存器,参数放寄存器,执行完指令从寄存器获取返回值。
mksnapshot制作快照可以输入一个额外的脚本,加载这快照后等同于执行过了这脚本了,只可惜mksnapshot工具提供的是一个纯净的v8环境,于是你输入的脚本也必需是一个纯es规范的js。
但我们js虚拟机往往是嵌入到一个程序中使用,会有很多宿主的扩展api(比如nodejs的文件、网络api,puerts导出的引擎api等等),如果需要包含这些扩展的初始化,mksnapshot是不可用的,所以mksnapshot也只能作为v8编译的一个中间环节。没法作为一个通用工具使用。
mksnapshot的快照制作是调用v8::SnapshotCreator完成,而v8::SnapshotCreator是提供了我们输入这些外部数据的机会。
一个原生扩展方法的函数指针,或者v8::External,都是外部数据,需要在SnapshotCreator的构造时输入,构造函数如下:
SnapshotCreator(Isolate* isolate,
const intptr_t* external_references = nullptr,
StartupData* existing_blob = nullptr);
还有另外一种用户自定义数据是 InternalField,如果有使用到这种数据,需要通过提供序列化(v8::SerializeInternalFieldsCallback)和反序列化(v8::DeserializeInternalFieldsCallback)逻辑帮助v8生成快照时保存/恢复你所需数据,对于上述的两个Callback,可以看snapshots里的介绍。
如果你只有一个Context需要保存,用SnapshotCreator::SetDefaultContext就可以了,恢复时直接v8::Context::New即可。
如果有多于一个Context,还可以通过SnapshotCreator::AddContext添加,它会返回一个索引,恢复时输入索引即可恢复到指定的存档
//保存
size_t context_index = snapshot_creator.AddContext(context, si_cb);
//恢复
v8::Local<v8::Context> context = v8::Context::FromSnapshot(isolate, context_index, di_cb).ToLocalChecked();
如果保存Context之外的数据,可以调用SnapshotCreator::AddData,然后通过Isolate或者Context的GetDataFromSnapshot接口恢复。