17 KiB
因收到Google相关通知,网站将会择期关闭。相关通知内容
39 源码解读:V8 执行 JS 代码的全过程
你好,我是LMOS。
前面我们学习了现代浏览器架构,也大致了解了浏览器内核的工作原理。在浏览器的内核中,V8 是一个绕不开的话题。在浏览器中,Chrome 的重要地位不用赘述,而V8不仅是 Chrome 的核心组件,还是 node.js 等众多软件的核心组件,所以,V8的重要程度亦不用多言。
不过,V8涉及到的技术十分广泛,包括操作系统、编译技术、计算机体系结构等多方面知识,为了带你先从宏观角度系统学习和了解V8项目,这节课我会从源码理解讲起,带你了解了V8 执行 JS 代码的全过程。
如何阅读 V8 源码和搭建 V8 开发环境
前面两节课,我带你简单了解了 Chromium 和 Webkit 项目的目录结构,在这里我们继续看一下如何学习 V8 源码。
Chromium 项目中包含了可运行的 V8 源码,但是从调试的角度看,我们一般使用 depot_tools来编译调试 V8 源码,它是V8的编译工具链,下载和编译代码都需要用到它,你可以直接点击 depot_tools bundle 下载。
解压后,我们需要将 depot_tools 工具添加到环境变量,注意这些操作需要你保证本机可以访问 Google 浏览器。
我们以 Mac 系统为例,添加到环境变量的代码命令如下:
export PATH=pwd
/depot_tools:"$PATH"
然后,你可以在命令行中测试 depot_tools 是否可以使用:
gclient sync
下载完 depot_tools 后,我们就可以下载 V8 代码进行编译调试了“
mkdir v8 cd v8 fetch v8 cd v8/src
下载好 V8 源码后,我们需要使用 GN 来配置工程文件。下面是我们用到的几个编译的参数:
is_component_build = true // 编译成动态链接库以减少体积 is_debug = true // 开启调试 v8_optimized_debug = true // 关闭一些代码优化 symbol_level = 0 将所有的debug符号放在一起,加速二次编译和链接过程; ide=vs2022 / ide=xcode // 选择编译 IDE
我们这节课就不展开讲解 gn 命令了,如果你有兴趣了解更多内容,可以自行查阅资料。说回正题,我们继续聊配置工作。
Windows 的配置情况如下 :
gn gen out.gn/x64.release --args='is_debug=true target_cpu="x64" v8_target_cpu="arm64" use_goma=true is_component_build=true v8_optimized_debug = true symbol_level = 0'
我们再来看看 Mac 下的情况,Mac 下我们需要更新 xcode 依赖,代码如下:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer gn gen out/gn --ide=xcode
执行完成后,我们可以通过 IDE 进入相应的工程文件下,后面是我的操作截图,供你参考:
我们看到,在工程文件下有一个名为 samples 的目录,上图中打开的文件 hello-world.cc 也是这个目录下的一个文件,它是 V8 项目中的一个实例文件,我们后面的学习也会从hello-world.cc文件入手。
我们来看一下这个文件的具体代码:
int main(int argc, char* argv[]) { // Initialize V8. v8::V8::InitializeICUDefaultLocation(argv[0]); v8::V8::InitializeExternalStartupData(argv[0]); std::unique_ptrv8::Platform platform = v8::platform::NewDefaultPlatform(); v8::V8::InitializePlatform(platform.get()); v8::V8::Initialize(); // Create a new Isolate and make it the current one. v8::Isolate::CreateParams create_params; create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); v8::Isolate* isolate = v8::Isolate::New(create_params); { v8::Isolate::Scope isolate_scope(isolate); // Create a stack-allocated handle scope. v8::HandleScope handle_scope(isolate); // Create a new context. v8::Localv8::Context context = v8::Context::New(isolate); // Enter the context for compiling and running the hello world script. v8::Context::Scope context_scope(context); { // Create a string containing the JavaScript source code. v8::Localv8::String source = v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'"); // Compile the source code. v8::Localv8::Script script = v8::Script::Compile(context, source).ToLocalChecked(); // Run the script to get the result. v8::Localv8::Value result = script->Run(context).ToLocalChecked(); // Convert the result to an UTF8 string and print it. v8::String::Utf8Value utf8(isolate, result); printf("%s\n", *utf8);
我们简单看看 hello-world.cc 这个文件,它是用 C++ 程序编写的,主要做了下面几件事:
初始化了 V8 程序;- 运行了一段基于 JavaScript 语言程序的 “hello world” 并输出;- 运行了一段基于 JavaScript 语言程序的加法运算并输出;- 执行完成后卸载了 V8。
上节课我们有提到,V8 是一个 JS 的执行引擎,在这个 helloworld 的代码中,除去运行JS 代码的两部分,其它的代码都是为 JS 代码运行提供的准备工作。
我们现在就看一下运行时都做了哪些基本的准备工作。
V8 在运行时的表现
上面代码是 hello-world 代码的主函数,也是核心的部分。我们梳理一下关键过程有哪些:首先hello- world代码的主函数调用了 v8::V8::Initialize() 方法对 V8 进行初始化;然后,调用了 v8::Isolate::New 来创建 Isolate;接着,创建完成后调用了 v8::Script::Compile 来进行编译;最后,调用 script->Run 用来执行 JS 代码。
我们后面会围绕上述关键过程做分析。你可以结合下面这张图,看看 hello-world.cc 的执行过程,还有这个过程里涉及到的核心方法和重要数据结构。
好,让我们进入具体分析环节,先从内存申请开始说起。
V8启动时的内存申请
申请内存从 InitReservation 方法开始,它主要处理的操作就是为 V8 引擎向 OS 申请内存,代码在 src/utils/allocation.cc 这个目录中:
// Reserve a region of twice the size so that there is an aligned address
// within it that's usable as the cage base.
VirtualMemory padded_reservation(params.page_allocator,
params.reservation_size * 2,
reinterpret_cast<void*>(hint));
if (!padded_reservation.IsReserved()) return false;
// Find properly aligned sub-region inside the reservation.
Address address =
VirtualMemoryCageStart(padded_reservation.address(), params);
CHECK(padded_reservation.InVM(address, params.reservation_size));
申请内存的时候,InitReservation 会先申请两倍的内存,保证内存对齐,再从两倍内存中找到一个适合对齐地址,这是 V8 真正使用的内存地址。这块申请出来的内存后面的工作里用得上。完成申请后,还会再调用 padded_reservation.Free() 方法,将刚开始申请的内存释放掉。
下面我带你看看 VirtualMemoryCage 数据结构,它是 V8 内存管理的主要数据结构。V8的内存方式采用的段页式,和 OS 的内存数据结构比较类似,但区别是 V8 只有一个段,OS 会有多段,但是 V8 会有很多页。
VirtualMemeoryCage 的数据结构位于allocation.h 文件中,如下所示:
// +------------+-----------+----------- ~~~ -+ // | ... | ... | ... | // +------------+-----------+------------ ~~~ -+ // ^ ^ ^ // start cage base allocatable base // // <------------> <-------------------> // base bias size allocatable size // <--------------------------------------------> // reservation size
reservation size 是 V8 实际申请的内存,start 是内存基址,cage base 是页表的位置,allocatable 是 V8 可分配内存的开始,用来创建 Isolate。
Isolate
Isolate是一个完整的V8实例,有着完整的堆和栈。V8是虚拟机,Isolate才是运行JavaScript的宿主。一个Isolate是一个独立的运行环境,包括但不限于堆管理器(heap)、垃圾回收器(GC)等。
在同一个时间,有且只有一个线程能在Isolate中运行代码,也就是说同一时刻,只有一个线程能进入Iisolate,而多个线程可以通过切换来共享同一个Isolate。
Isolate 对外的接口是 V8_EXPORT ,定义在 include/v8.h 文件中,其他程序可以调用它。这个接口也可以理解为JavaScript的运行单元,多个线程也就是多个任务,它们可以共享一个运行单元,主要涉及到几个 V8 的概念:
Context:上下文,所有的JS代码都是在某个V8 Context中运行的。 Handle:一个指定JS对象的索引,它指向此JS对象在V8堆中的位置。 Handle Scope:包含很多handle的集合,用来统一管理多个handle,当Scope被移出堆时,它所管理的handle集合也会被移除。
Isolate 还有一个对内的数据结构 V8_EXPORT_PRIVATE,也是一个核心的数据结构,内部的很多重要的结构都会用到它,后面编译流程我还会讲到。
编译
V8 的编译流程也是 V8 的核心流程,我们先简单看下编译的大概流程:
tokenize (分词):将 JS 代码解析为 Token 流,Token 是语法上的不可拆分的最小单位; parse (解析):语法分析,将上一步生成的 token 流转化为 AST 结构,AST 被称为抽象语法树; ignite (解释):通过解释器,生成字节码。
接着,我们再看看这个过程的关键数据结构 V8_EXPORT_PRIVATE ParseInfo,代码在 src/parsing/parse-info.cc 目录下:
ParseInfo 这个数据结构就是JS 代码生成token,再生成 AST 的过程,AST 的数据结构位置在 src/ast/ast.h。
生成 AST后,解释器会根据 AST生成字节码,并解释执行字节码。字节码是介入 AST和机器码之间的一种数据结构,你先留个印象,我们后面再详细说。
代码执行
经过编译,最终生成了字节码。我们继续来看 Exectuion 这个数据结构,这个结构承载着 JS 代码运行过程前后的相关信息:
class Execution final : public AllStatic { public: // Whether to report pending messages, or keep them pending on the isolate. enum class MessageHandling { kReport, kKeepPending }; enum class Target { kCallable, kRunMicrotasks };
// Call a function, the caller supplies a receiver and an array // of arguments. // // When the function called is not in strict mode, receiver is // converted to an object. // V8_EXPORT_PRIVATE V8_WARN_UNUSED_RESULT static MaybeHandle