V8 Pwn Basics 0: Environment Build

怎么有人写完两个题了才来环境搭建…

v8 是 Google 用 C++ 开发的一个开源 JavaScript 引擎. 简单来说, 就是执行 js 代码的一个程序. Chromium, Node.js 都使用 v8 解析并运行 js.

v8 的开源仓库在 这里

JavaScript 是解释语言, 需要先翻译成字节码后在 VM 上运行. V8 中实现了一个 VM. 出于性能考虑, 目前的引擎普遍采用一种叫做 Just-in-time (JIT) 的编译技术, V8 也是. JIT 的思想在于, 如果一段代码反复执行, 那么将其编译成机器代码运行, 会比每次都解释要快得多.

/v8-pwn-basics-0-environment-build/img/v8-compiler-pipeline.webp
V8’s compiler pipeline

v8 解释和编译 JS 代码的流程如上图所示.

  • 解析器Parser 将 JS 代码转换为 抽象语法树Abstract Syntax Tree (AST)
  • 解释器Interpreter 将 AST 转换成 字节码Bytecode, 并在 VM 中执行
  • 编译器Compiler 将一些字节码优化编译成二进制机器码并执行

v8 的解释器叫 Ignition, 是一个 VM, 编译器叫 TurboFan. 新版还中间还有一个非优化编译器, 叫 Sparkplug.

首先需要 depot_tools. 这个工具库是专门搞 Chromium 开发用的, 里面有一堆程序和脚本.

1
2
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH

接着 clone v8, 并且下载工具. 可以用 depot_tools 中的 fetch, 它会在当前目录下生成一些文件如 .cipd, .gclient, .gclient_entries, .gclient_previous_sync_commits clone 完成后进入 v8 目录, 使用 gclient sync 下载工具.

1
2
3
fetch v8
cd v8
gclient sync

每个 commit 的工具可能是不同的, 当切换了分支后, 记得重新 gclient sync 一下.

下载完工具后, 运行 ./build/install-build-deps.sh 脚本安装依赖.

执行 ./tools/dev/gm.py x64.release 可以使用预设的选项编译 release 版本, 将 release 换成 debug 可以编译 debug 版本. 这样编译出来的文件在 ./out/x64.release 或者 ./out/x64.debug 下.

也可以自行设置编译选项, 然后编译. 用 ./tools/dev/v8gen.py $target.$version -- options 来生成 $target 架构的 $version 版本的配置文件. 如 ./tools/dev/v8gen.py x64.release. 生成的文件会在 ./out.gn/ 下的对应目录里. 更多用法可以看 官方文档.

无论是用 gm 还是 v8gen, 生成的文件中包含一个编译选项. 在 ./out/ 或者 ./out.gn/ 对应目录下的 args.gn. 比如 gm 默认生成的 release 版本选项如下:

is_component_build = false
is_debug = false
target_cpu = "x64"
use_goma = false
goma_dir = "None"
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

一般来说用默认的选项就可以. v8_enable_object_print 是支持打印 JSObject 在 v8 中的内存表示, 调试的时候会用到. (不过一般还是先用 debug 版本调).

is_debug = true 编译选项会设置 DCHECK 宏, 它负责一些简单的安全检查, 如判断数组是否越界. 而题目往往编译的 release 版本, 如果在利用中有这种行为, 不会有什么影响. 但是用 debug 版本调试时会直接 assert. 不幸的是没有选项能够取消设置 DCHECK. 如果还需要在 debug 版本下调试以获得良好体验的话, 可以手动 patch 一下. 在 src/base/logging.h 中找到 DCHECK 定义的地方:

1
2
3
4
5
6
7
8
9
#ifdef DEBUG

#define DCHECK_WITH_MSG(condition, message)   \
  do {                                        \
    if (V8_UNLIKELY(!(condition))) {          \
      V8_Dcheck(__FILE__, __LINE__, message); \
    }                                         \
  } while (0)
#define DCHECK(condition) DCHECK_WITH_MSG(condition, #condition)

直接把 do while 中的代码给删掉就行.

d8 的一些启动参数可以支持打印信息. 比如 --trace-opt 可以打印编译优化时的信息.

d8 带 --allow-natives-syntax 参数启动的话, 可以在 js 脚本中写一些调试用的函数, 这些函数通常以 % 开头, 如 %DebugPrint() 显示对象信息, %DebugPrintPtr() 显示指针指向的对象信息, %SystemBreak() 下断点等. 在 src/runtime/runtime.h 中可以找到所有的 natives syntax.

debug 版本下输出的内容较多, release 版本只有少部分信息.

d8 内实现了一些辅助调试的函数, 并且提供了 gdb 脚本, 以供在 gdb 中调用. 在 .gdbinit source 一下 tools/gdbinittools/gdb-v8-support.py 即可.

用的较多的是 job $value 这个命令. 如果 $value 是个 带标记的指针, 则会显示 JSObject 在内存中的表示, 输出与 %DebugPrint() 类似.

其他命令可以在 tools/gdbinit 中查看.