认识 V8 引擎(2)- 编译跑通 12 年前的 V8

我认为,学习开源代码最好的方式,是编译过代码后,可以进行单步调试,一步一步洞悉内部所发生的一切。从上篇我们知道,V8 从 2008 年就已经面世,到目前为止已经有 13 年之久。这 13 年以来,科技经历的变革是巨大的,同样,V8 代码的复杂度变化也是巨大的。如果直接从现在的 V8 代码入手,学习,无疑是非常困难的。

困难的不是把 V8 的代码编过,而是 V8 复杂的代码结构,难以一眼看清它本质的核心脉络。V8 最早的开源版本是 2008 年的 0.1.5 版本,那时的代码结构还相对简单,从代码阅读和学习的角度,这是一个非常好的版本。于是一开始我的努力方向,就是把 0.1.5 版本在 Mac 上编过,并且能将简单的 Hello World 跑起来。

早期的 V8 编译脚本是用 SCons 写的,而不是现在的 GN + Ninja 的系统。我尝试也安装 SCons 看能不能编过,然后发现根本就不过了。原因是今天的 SCons 版本和 V8 团队当年用的 SCons 版本已经发生了巨大的改变。比如早期的 Options 函数已经废弃,甚至连当年的文档都难以追溯。同时当年的 SCons 编译脚本是用 Python 2.X 的版本写的,用今天的 Python3 来跑,也遇到各种兼容性的问题。当然,这些都是可以解决的。然而我并不想解决 SCons 编译的问题。因为我想用现在更好的方式将它编过,就是 CMake

参考了源码中 SConscript 里的编译过程,我重写了一份 CMakeLists.txt。在经历了一个又一个编译错误之后,我终于把 V8 0.1.5 版本在 Mac 上编过了(我平常几乎只用 Mac,所以没管其他的平台)。然后写了个简单的 Hello World 代码,用当时 V8 的 API。

// test/main.cc
#include <stdio.h>
#include "v8.h"

int main(int argc, char* argv[]) {
  printf("hello v8\n");
  bool init_result = v8::V8::Initialize();
  if (!init_result) {
    printf("v8 initialize failed\n");
    return -1;
  }
  auto ctx = v8::Context::New();
  v8::HandleScope hs;
  v8::Local<v8::String> source = v8::String::New("1+1");
  v8::Local<v8::Value> result = v8::Script::Compile(source)->Run();
  v8::String::AsciiValue str(result);
  printf("1+1=%s\n", *str);
  return 0;
}

从上面的代码可以看出,早期的 V8 API 和现在的几乎是接近的。首先需要通过 V8::Initialize() 对 V8 进行全局初始化。然后有 V8::Context 的概念,甚至已经有了 HandleScope 自动管理 Local 变量。当然从上面代码也可以看出区别,今天非常重要的 v8::Isolate 概念还没有,以及 v8::Context::Scope 也还没有出现。

上面代码运行之后,发现 v8::V8::Initialize() 就失败了。因为可以调试,定位到了这么这个函数:

bool VirtualMemory::Commit(void* address, size_t size) {
  if (MAP_FAILED == mmap(address, size, PROT_READ | PROT_WRITE | PROT_EXEC,
                         MAP_PRIVATE | MAP_ANON | MAP_FIXED,
                         kMmapFd, kMmapFdOffset)) {
    fprintf (stderr, "mmap size=%ld fd=%d failed %s\n",
                (long)size, kMmapFd, strerror(errno));
    // 这里失败了,打印的错误是:
    // mmap size=262144 fd=-1 failed Cannot allocate memory
    return false;
  }
  UpdateAllocatedSpaceLimits(address, size);
  return true;
}

遇到 mmap 内存分配失败时,我是有点一脸懵的。在仔细查看了 Commit 函数上下文,以及了解 mmap 每个参数之后,我发现的竟然是另一个严肃的问题。我编的这份代码是 08 年所写的,只支持编译 32位的版本。而我编译的时候,选择的是编译的 64位。于是我想,在 CMakeLists 里指定编 -m32 版本吧。然后发现切到 32位之后更是更多的编译不过。如今在 PC 上已经很少有编 32位的程序了,我不是很想去解决 32位编译的问题。我不如去看看 V8 的代码历史记录,看看从哪个版本开始支持了 64位。

于是我找到了最早支持 64位的版本:1.2.14.20。我重新整理了 CMakeLists 编译脚本,解决了几个代码的编译问题之后,It works! Yes!

1.2 版本的 V8 是 09 年的代码,距今也有 12 年,代码还保持了简单的结构。相对前面 0.1.5 版本,有了 v8::Context::Scope 的概念。多了很多测试代码,甚至,连强大的 d8 工具也面世了。在 tools 目录下,甚至有 Xcode 工程文件以及 Visual Studio 工程文件,当然还包括了 gyp 文件。我简单尝试了 Xcode 工程编译,似乎编的还是 32位代码,没有成功。我不想继续尝试了,也包括 Visual Studio。因为我已经通过 CMakeLists 编过了 64位版本,并且可以愉快的调试了。

我想现在这份 12 年前的 V8 代码,应该是最适合学习和了解 V8 的。毕竟,它只有几十个文件,编译一次只要几十秒钟。

最后,我也将这份可以编过跑过的代码放到了 GitHub 上。有兴趣的小伙伴,可以一起来学习交流:

https://github.com/coderzh/v8-1.2

git clone 之后,用 VSCode 打开,通过我配置好的编译设置,按下 Cmd+Shift+b 就可以编译,按下 F5 就可以进入调试,岂不快哉。

微信扫一扫交流

作者:CoderZh
微信关注:hacker-thinking (程序员思考者)
本文出处:https://blog.coderzh.com/2021/05/03/v8-2/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。