learn-tech/专栏/计算机基础实战课/39源码解读:V8执行JS代码的全过程.md
2024-10-16 10:18:29 +08:00

17 KiB
Raw Blame History

                        因收到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