V8 Ignition 解释器的内部实现探究
杰哥的{运维,编程,调板子}小笔记V8 Ignition 解释器的内部实现探究¶
背景¶
V8 是一个很常见的 JavaScript 引擎,运行在很多的设备上,因此想探究一下它内部的部分实现。本博客在 ARM64 Ubuntu 24.04 平台上针对 V8 12.8.374.31 版本进行分析。本博客主要分析了 V8 的 Ignition 解释器的解释执行部分。
编译 V8¶
首先简单过一下 v8 的源码获取以及编译流程,主要参考了 Checking out the V8 source code 和 Compiling on Arm64 Linux:
# setup depot_toolscd ~git clone https://chromium.googlesource.com/chromium/tools/depot_tools.gitexport PATH=$PWD/depot_tools:$PATH# clone v8 reposmkdir ~/v8cd ~/v8fetch v8cd v8# switch to specified taggit checkout 12.8.374.31gclient sync --verbose# install dependencies./build/install-build-deps.sh# install llvm 19wget https://mirrors.tuna.tsinghua.edu.cn/llvm-apt/llvm.shchmod +x llvm.shsudo ./llvm.sh 19 -m https://mirrors.tuna.tsinghua.edu.cn/llvm-aptrm llvm.shsudo apt install -y lld clang-19# fix incompatibilities with system clang 19sed -i "s/-Wno-missing-template-arg-list-after-template-kw//" build/config/compiler/BUILD.gn# compile v8 using system clang 19# for amd64: use x64.optdebug instead of arm64.optdebugtools/dev/gm.py arm64.optdebug --progress=verbose# d8 is compiled successfully at./out/arm64.optdebug/d8如果不想编译 V8,也可以直接用 Node.JS 来代替 d8。不过 Node.JS 会加载很多 JS 代码,使得输出更加复杂,此时就需要手动过滤一些输出,或者通过命令行设置一些打印日志的过滤器。另外,后面有一些深入的调试信息,需要手动编译 V8 才能打开,因此还是建议读者上手自己编译一个 V8。
在 AMD64 上,默认会使用 V8 自带的 LLVM 版本来编译,此时就不需要额外安装 LLVM 19,也不需要修改 v8/build/config/compiler/BUILD.gn。
解释器和编译器¶
通过 V8 的文档可以看到,V8 一共有这些解释器或编译器,按照其优化等级从小到大的顺序:
在 JS 的使用场景,不同代码被调用的次数以及对及时性的需求差别很大,为了适应不同的场景,V8 设计了这些解释器和编译器来提升整体的性能:执行次数少的代码,倾向于用更低优化等级的解释器或编译器,追求更低的优化开销;执行次数多的代码,当编译优化时间不再成为瓶颈,则倾向于用更高优化等级的编译器,追求更高的执行性能。
Ignition 解释器¶
分析样例 JS 代码¶首先来观察一下 Ignition 解释器的工作流程。写一段简单的 JS 代码:
function add(a, b) { return a + b;}add(1, 2);保存为 test.js,运行 ./out/arm64.optdebug/d8 --print-ast --print-bytecode test.js 以打印它的 AST 以及 Bytecode:
首先开始的是 top level 的 AST 以及 Bytecode,它做的事情就是:声明一个函数 add,然后以参数 (1, 2) 来调用它。
top level AST:
[generating bytecode for function: ]--- AST ---FUNC at 0. KIND 0. LITERAL ID 0. SUSPEND COUNT 0. NAME "". INFERRED NAME "". DECLS. . FUNCTION "add" = function add. EXPRESSION STATEMENT at 42. . kAssign at -1. . . VAR PROXY local[0] (0xc28698556308) (mode = TEMPORARY, assigned = true) ".result". . . CALL. . . . VAR PROXY unallocated (0xc28698556200) (mode = VAR, assigned = true) "add". . . . LITERAL 1. . . . LITERAL 2. RETURN at -1. . VAR PROXY local[0] (0xc28698556308) (mode = TEMPORARY, assigned = true) ".result"首先声明了一个 add 函数,然后以 1 和 2 两个参数调用 add 函数,把结果绑定给局部变量 .result,最后以 .result 为结果返回。接下来看它会被翻译成什么字节码:
[generated bytecode for function: (0x1f8e002988f5 <SharedFunctionInfo>)]Bytecode length: 28Parameter count 1Register count 4Frame size 32 0x304700040048 @ 0 : 13 00 LdaConstant [0] 0x30470004004a @ 2 : c9 Star1 0x30470004004b @ 3 : 19 fe f7 Mov <closure>, r2 0x30470004004e @ 6 : 68 63 01 f8 02 CallRuntime [DeclareGlobals], r1-r2 0x304700040053 @ 11 : 21 01 00 LdaGlobal [1], [0] 0x304700040056 @ 14 : c9 Star1 0x304700040057 @ 15 : 0d 01 LdaSmi [1] 0x304700040059 @ 17 : c8 Star2 0x30470004005a @ 18 : 0d 02 LdaSmi [2] 0x30470004005c @ 20 : c7 Star3 0x30470004005d @ 21 : 66 f8 f7 f6 02 CallUndefinedReceiver2 r1, r2, r3, [2] 0x304700040062 @ 26 : ca Star0 0x304700040063 @ 27 : af ReturnConstant pool (size = 2)0x304700040011: [TrustedFixedArray] - map: 0x1f8e00000595 <Map(TRUSTED_FIXED_ARRAY_TYPE)> - length: 2 0: 0x1f8e00298945 <FixedArray[2]> 1: 0x1f8e000041dd <String[3]: #add>Handler Table (size = 0)Source Position Table (size = 0)V8 的字节码采用的是基于寄存器的执行模型,而非其他很多字节码会采用的栈式。换句话说,每个函数有自己的若干个寄存器可供操作。每条字节码分为 Opcode(表示这条字节码要进行的操作)和操作数两部分。函数开头的 Register count 4 表明该函数有四个寄存器:r0-r3,此外还有一个特殊的 accumulator 寄存器,它一般不会出现在操作数列表中,而是隐含在 Opcode 内(Lda/Sta)。
完整的 Opcode 列表可以在 v8/src/interpreter/bytecodes.h 中找到,对应的实现可以在 v8/src/interpreter/interpreter-generator.cc 中找到。
上述字节码分为两部分,第一部分是声明 add 函数:
LdaConstant [0]: 把 Constant Pool 的第 0 项也就是FixedArray[2]写入accumulator寄存器当中Star1: 把accumulator寄存器的值拷贝到r1寄存器,结合上一条字节码,就是设置r1 = FixedArray[2]Mov <closure>, r2: 把<closure>拷贝到r2寄存器,猜测这里的<closure>对应的是add函数CallRuntime [DeclareGlobals], r1-r2: 调用运行时的DeclareGlobals函数,并传递两个参数,分别是r1和r2;有意思的是,CallRuntime的参数必须保存在连续的寄存器当中,猜测是为了节省编码空间
至此,add 函数就声明完成了。接下来,就要实现 add(1, 2) 的调用:
LdaGlobal [1], [0]: 把 Constant Pool 的第 1 项也就是"add"这个字符串写入accumulator,最后的[0]和 FeedBackVector 有关,目前先忽略Star1: 把accumulator寄存器的值拷贝到r1寄存器,结合上一条字节码,就是设置r1 = "add"LdaSmi [1]: 把小整数(Small integer, Smi)1写入accumulatorStar2: 把accumulator寄存器的值拷贝到r2寄存器,结合上一条字节码,就是设置r2 = 1LdaSmi [2]: 把小整数(Small integer, Smi)2写入accumulatorStar3: 把accumulator寄存器的值拷贝到r3寄存器,结合上一条字节码,就是设置r3 = 2CallUndefinedReceiver2 r1, r2, r3, [2]: 根据r1调用一个函数,并传递两个参数r2, r3(函数名称最后的2表示有两个参数),最后的[2]也和 FeedBackVector 有关
这样就完成了函数调用。
接下来观察 add 函数的 AST:
[generating bytecode for function: add]--- AST ---FUNC at 12. KIND 0. LITERAL ID 1. SUSPEND COUNT 0. NAME "add". INFERRED NAME "". PARAMS. . VAR (0xc50512445280) (mode = VAR, assigned = false) "a". . VAR (0xc50512445300) (mode = VAR, assigned = false) "b". DECLS. . VARIABLE (0xc50512445280) (mode = VAR, assigned = false) "a". . VARIABLE (0xc50512445300) (mode = VAR, assigned = false) "b". RETURN at 25. . kAdd at 34. . . VAR PROXY parameter[0] (0xc50512445280) (mode = VAR, assigned = false) "a". . . VAR PROXY parameter[1] (0xc50512445300) (mode = VAR, assigned = false) "b"add 函数的 AST 比较直接,a + b 直接对应了 kAdd 结点,直接作为返回值。
接下来观察 add 的 Bytecode:
[generated bytecode for function: add (0x10c700298955 <SharedFunctionInfo add>)]Bytecode length: 6Parameter count 3Register count 0Frame size 0 0x6ed0004008c @ 0 : 0b 04 Ldar a1 0x6ed0004008e @ 2 : 3b 03 00 Add a0, [0] 0x6ed00040091 @ 5 : af ReturnConstant pool (size = 0)Handler Table (size = 0)Source Position Table (size = 0)Ldar a1: 把第二个参数a1也就是b写入accumulator寄存器Add a0, [0]: 求第一个参数a0也就是a与accumulator寄存器的和,写入到accumulator寄存器当中,结合上一条 Bytecode,就是accumulator = a0 + a1;[0]和 FeedBackVector 有关Return: 把accumulator中的值作为返回值,结束函数调用
简单小结一下 V8 的字节码:
- 有若干个局部的寄存器,在操作数中以
rn的形式出现,n是寄存器编号 - 有
accumulator局部寄存器,作为部分字节码的隐含输入或输出(Add) - 有若干个参数,在操作数中以
an的形式出现,n是参数编号 - 操作数还可以出现立即数参数
[imm],可能是整数字面量(LdaSmi),可能是下标(LdaConstant),也可能是 FeedBackVector 的 slot
有了字节码以后,接下来观察 Ignition 具体是怎么解释执行这些字节码的。
解释执行¶为了实际执行这些字节码,Ignition 的做法是:
- 给每种可能的 Opcode 生成一段二进制代码,这段代码会实现这个 Opcode 的功能
- 在运行时维护一个 dispatch table,维护了 Opcode 到二进制代码地址的映射关系
- 在每段代码的结尾,找到下一个 Opcode 对应的代码的地址,然后跳转过去
- 调用函数时,先做一系列的准备,找到函数第一个字节码的 Opcode 对应的代码的地址,跳转过去
由于 Opcode 的种类是固定的,所以实际运行 V8 的时候,这些代码已经编译好了,只需要在运行时初始化对应的数据结构即可。这个代码的生成和编译过程,也不是由 C++ 编译器做的,而是有一个 mksnapshot 命令来完成初始化,你可以认为它把这些 Opcode 对应的汇编指令都预先生成好,运行时直接加载即可。
首先来看 Ignition 的怎么实现各种 Opcode 的,以 LdaSmi 为例,它的作用是小的把立即数(Smi=Small integer)写入到 accumulator 当中,这段在 v8/src/interpreter/interpreter-generator.cc 的代码实现了这个逻辑:
// LdaSmi <imm>//// Load an integer literal into the accumulator as a Smi.IGNITION_HANDLER(LdaSmi, InterpreterAssembler) { TNode<Smi> smi_int = BytecodeOperandImmSmi(0); SetAccumulator(smi_int); Dispatch();}可以看到,逻辑并不复杂,就是取了第一个立即数操作数,设置到了 accumulator,最后调用 Dispatch,也就是读取下一个 Opcode 对应的汇编指令然后跳转。接下来看这几个步骤在汇编上是怎么实现的。
为了查看 Ignition 对各种 Opcode 具体生成了什么样的汇编指令,可以用 ./out/arm64.optdebug/mksnapshot --trace-ignition-codegen --code-comments 命令查看,下面列出了 LdaSmi 这个 Opcode 对应的汇编,由于这段汇编有点长,具体做的事情和对应的源码已经通过注释标注出来:
kind = BYTECODE_HANDLERname = LdaSmicompiler = turbofanaddress = 0x31a000906fdInstructions (size = 324)# 在代码的开头,检查寄存器是否正确,即 x2 是否保存了当前代码段的开始地址,对应的源码:# v8/src/compiler/backend/arm64/code-generator-arm64.cc CodeGenerator::AssembleCodeStartRegisterCheck():# UseScratchRegisterScope temps(masm());# Register scratch = temps.AcquireX();# __ ComputeCodeStartAddress(scratch); // becomes x16 in the following code# __ cmp(scratch, kJavaScriptCallCodeStartRegister);# __ Assert(eq, AbortReason::kWrongFunctionCodeStart);# 其中 kJavaScriptCallCodeStartRegister 定义在 v8/src/codegen/arm64/register-arm64.h:# constexpr Register kJavaScriptCallCodeStartRegister = x2; [ Frame: MANUAL -- Prologue: check code start register -- - AssembleCode@../../src/compiler/backend/code-generator.cc:2320xc6ccfe4a8b00 0 10000010 adr x16, #+0x0 (addr 0xc6ccfe4a8b00)0xc6ccfe4a8b04 4 eb02021f cmp x16, x20xc6ccfe4a8b08 8 54000080 b.eq #+0x10 (addr 0xc6ccfe4a8b18) [ - Abort@../../src/codegen/arm64/macro-assembler-arm64.cc:4008 Abort message: - Abort@../../src/codegen/arm64/macro-assembler-arm64.cc:4010 Wrong value in code start register passed - Abort@../../src/codegen/arm64/macro-assembler-arm64.cc:40110xc6ccfe4a8b0c c d2801081 movz x1, #0x84 [ Frame: NO_FRAME_TYPE [ - EntryFromBuiltinAsOperand@../../src/codegen/arm64/macro-assembler-arm64.cc:2377 ]0xc6ccfe4a8b10 10 f96a3750 ldr x16, [x26, #21608]0xc6ccfe4a8b14 14 d63f0200 blr x16 ] ]# 栈对齐检查,定义在:# v8/src/codegen/arm64/macro-assembler-arm64.cc MacroAssembler::AssertSpAligned():# if (!v8_flags.debug_code) return;# ASM_CODE_COMMENT(this);# HardAbortScope hard_abort(this); // Avoid calls to Abort.# // Arm64 requires the stack pointer to be 16-byte aligned prior to address# // calculation.# UseScratchRegisterScope scope(this);# Register temp = scope.AcquireX(); // becomes x16 in the following code# Mov(temp, sp);# Tst(temp, 15);# Check(eq, AbortReason::kUnexpectedStackPointer); -- B0 start (construct frame) -- [ - AssertSpAligned@../../src/codegen/arm64/macro-assembler-arm64.cc:15900xc6ccfe4a8b18 18 910003f0 mov x16, sp [ - LogicalMacro@../../src/codegen/arm64/macro-assembler-arm64.cc:1970xc6ccfe4a8b1c 1c f2400e1f tst x16, #0xf ]0xc6ccfe4a8b20 20 54000080 b.eq #+0x10 (addr 0xc6ccfe4a8b30) [ - Abort@../../src/codegen/arm64/macro-assembler-arm64.cc:4008 Abort message: - Abort@../../src/codegen/arm64/macro-assembler-arm64.cc:4010 The stack pointer is not the expected value - Abort@../../src/codegen/arm64/macro-assembler-arm64.cc:4011 [ Frame: NO_FRAME_TYPE0xc6ccfe4a8b24 24 52800780 movz w0, #0x3c0xc6ccfe4a8b28 28 f94e7750 ldr x16, [x26, #7400]0xc6ccfe4a8b2c 2c d63f0200 blr x16 ] ] ]# 构建栈帧# 构建完成后会得到:sp = prev sp - 64, fp = sp + 48## 栈帧的示意图, 每一个方框表示 8 字节的内存:# sp + 64 +------------+# | lr | <= lr 是 link register 的缩写,表示返回地址# sp + 56 +------------+# | prev fp | <= 保存了调用前的 fp (frame pointer)# sp + 48 +------------+ <= 新的 fp (frame pointer) 指向这里# | x16 (0x22) |# sp + 40 +------------+# | x20 | <= bytecode array register# sp + 32 +------------+# | x21 | <= dispatch table register# sp + 24 +------------+# | x19 | <= bytecode offset register,记录当前正在执行的 bytecode 在 bytecode array 中的偏移# sp + 16 +------------+# | x0 | <= accumulator register# sp + 8 +------------+# | |# sp +------------+ <= 新的 sp (stack pointer) 指向这里## 这些寄存器定义在 v8/src/codegen/arm64/register-arm64.h 当中:# constexpr Register kInterpreterAccumulatorRegister = x0;# constexpr Register kInterpreterBytecodeOffsetRegister = x19;# constexpr Register kInterpreterBytecodeArrayRegister = x20;# constexpr Register kInterpreterDispatchTableRegister = x21;0xc6ccfe4a8b30 30 d2800450 movz x16, #0x220xc6ccfe4a8b34 34 a9be43ff stp xzr, x16, [sp, #-32]!0xc6ccfe4a8b38 38 a9017bfd stp fp, lr, [sp, #16]0xc6ccfe4a8b3c 3c 910043fd add fp, sp, #0x10 (16)0xc6ccfe4a8b40 40 d10083ff sub sp, sp, #0x20 (32)0xc6ccfe4a8b44 44 f90013f4 str x20, [sp, #32]0xc6ccfe4a8b48 48 f9000ff5 str x21, [sp, #24]0xc6ccfe4a8b4c 4c f90007e0 str x0, [sp, #8]0xc6ccfe4a8b50 50 f9000bf3 str x19, [sp, #16]# 调用了未知的 C 函数0xc6ccfe4a8b54 54 d28042c1 movz x1, #0x216 [ - LoadFromConstantsTable@../../src/codegen/arm64/macro-assembler-arm64.cc:2166 [ - LoadRoot@../../src/codegen/arm64/macro-assembler-arm64.cc:19540xc6ccfe4a8b58 58 f94d5f42 ldr x2, [x26, #6840] ] [ - DecompressTagged@../../src/codegen/arm64/macro-assembler-arm64.cc:34480xc6ccfe4a8b5c 5c d28bcef0 movz x16, #0x5e770xc6ccfe4a8b60 60 b8706842 ldr w2, [x2, x16]0xc6ccfe4a8b64 64 8b020382 add x2, x28, x2 ] ]0xc6ccfe4a8b68 68 aa1403e0 mov x0, x200xc6ccfe4a8b6c 6c f94ecf50 ldr x16, [x26, #7576] [ - CallCFunction@../../src/codegen/arm64/macro-assembler-arm64.cc:21060xc6ccfe4a8b70 70 10000068 adr x8, #+0xc (addr 0xc6ccfe4a8b7c)0xc6ccfe4a8b74 74 a93f235d stp fp, x8, [x26, #-16]0xc6ccfe4a8b78 78 d63f0200 blr x160xc6ccfe4a8b7c 7c f81f035f stur xzr, [x26, #-16] ]0xc6ccfe4a8b80 80 d2800001 movz x1, #0x0 [ - LoadFromConstantsTable@../../src/codegen/arm64/macro-assembler-arm64.cc:2166 [ - LoadRoot@../../src/codegen/arm64/macro-assembler-arm64.cc:19540xc6ccfe4a8b84 84 f94d5f42 ldr x2, [x26, #6840] ] [ - DecompressTagged@../../src/codegen/arm64/macro-assembler-arm64.cc:34480xc6ccfe4a8b88 88 d28bcf70 movz x16, #0x5e7b0xc6ccfe4a8b8c 8c b8706842 ldr w2, [x2, x16]0xc6ccfe4a8b90 90 8b020382 add x2, x28, x2 ] ]0xc6ccfe4a8b94 94 f94007e0 ldr x0, [sp, #8]0xc6ccfe4a8b98 98 f94ecf50 ldr x16, [x26, #7576] [ - CallCFunction@../../src/codegen/arm64/macro-assembler-arm64.cc:21060xc6ccfe4a8b9c 9c 10000068 adr x8, #+0xc (addr 0xc6ccfe4a8ba8)0xc6ccfe4a8ba0 a0 a93f235d stp fp, x8, [x26, #-16]0xc6ccfe4a8ba4 a4 d63f0200 blr x160xc6ccfe4a8ba8 a8 f81f035f stur xzr, [x26, #-16] ]# 从这里开始实现 LdaSmi 的语义# 从前面的分析可以看到 LdaSmi 由两个字节组成:# 1. 第一个字节是 0x0d,表示这是一条 LdaSmi# 2. 第二个字节就是要加载到 `accumulator` 的小整数# 如:0d 01 对应 LdaSmi [1],0d 02 对应 LdaSmi [2]# 所以,为了实现 LdaSmi,需要从 bytecode array 中读取 LdaSmi 字节码的第二个字节,# 保存到 `accumulator` 寄存器当中# 下面一条一条地分析指令在做的事情:# 1. 从 sp + 16 地址读取 bytecode offset 寄存器的值到 x3,# 它记录了 LdaSmi 相对 bytecode array 的偏移0xc6ccfe4a8bac ac f9400be3 ldr x3, [sp, #16]# 2. 计算 x3 + 1 的值并写入 x4,得到 LdaSmi 的第二个字节相对 bytecode array 的偏移0xc6ccfe4a8bb0 b0 91000464 add x4, x3, #0x1 (1)# 3. 从 sp + 32 地址读取 bytecode array 寄存器的值到 x200xc6ccfe4a8bb4 b4 f94013f4 ldr x20, [sp, #32]# 4. 从 x20 + x4 地址读取 LdaSmi 的第二个字节到 x4,也就是要加载到 `accumulator` 的值,# 之后 x4 的值会写入到 x0,也就是 `accumulator` 对应的寄存器0xc6ccfe4a8bb8 b8 38e46a84 ldrsb w4, [x20, x4]# Dispatch: 找到下一个 Opcode 对应的代码的入口,然后跳转过去 ========= Dispatch - Dispatch@../../src/interpreter/interpreter-assembler.cc:1278 - AssembleArchInstruction@../../src/compiler/backend/arm64/code-generator-arm64.cc:978# x3 是 LdaSmi 当前所在的 bytecode offset,加 2 是因为 LdaSmi 占用了两个字节# x19 = x3 + 2,就是 bytecode offset 前进两个字节,指向下一个字节码0xc6ccfe4a8bbc bc 91000873 add x19, x3, #0x2 (2)# x20 是 bytecode array,从 bytecode array 读取下一个字节码的第一个字节到 x3 寄存器0xc6ccfe4a8bc0 c0 38736a83 ldrb w3, [x20, x19]# 接下来检查在 x3 寄存器当中的字节码的第一个字节(Opcode),如果它:# 1. 小于 187(kFirstShortStar),说明它不是特殊的 Short Star (Star0-Star15) 字节码# 2. 介于 187(kFirstShortStar) 和 202(kLastShortStar) 之间,说明它是特殊的 Short Star (Star0-Star15) 字节码# 3. 如果大于 202(kLastShortStar),说明它是非法的字节码# 如果 x3 寄存器大于或等于 187,说明这个字节码可能是 Short Star 字节码,就跳转到后面的 B20xc6ccfe4a8bc4 c4 7102ec7f cmp w3, #0xbb (187)0xc6ccfe4a8bc8 c8 54000102 b.hs #+0x20 (addr 0xc6ccfe4a8be8) -- B1 start --# 此时 x3 小于 187# 从栈上读取 x21 即 dispatch table register0xc6ccfe4a8bcc cc f9400ff5 ldr x21, [sp, #24]# 从 dispatch table,以 x3 为下标,读取下一个字节码对应的代码的地址0xc6ccfe4a8bd0 d0 f8637aa2 ldr x2, [x21, x3, lsl #3]# 把之前 LdaSmi 计算得到的 x4 寄存器写到 `accumulator` 即 x0 寄存器当中# 这里 x0 = 2 * x4,是因为 v8 用最低位表示这是一个 Smi(用 0 表示)还是一个指针(用 1 表示)0xc6ccfe4a8bd4 d4 0b040080 add w0, w4, w4# 恢复调用函数前的旧 fp 和 lr0xc6ccfe4a8bd8 d8 a9407bbd ldp fp, lr, [fp]# 恢复调用函数前的旧 sp0xc6ccfe4a8bdc dc 910103ff add sp, sp, #0x40 (64)# 下一个字节码对应的代码的地址已经保存在 x2 寄存器当中,跳转过去0xc6ccfe4a8be0 e0 aa0203f1 mov x17, x20xc6ccfe4a8be4 e4 d61f0220 br x17 -- B2 start -- [ Assert: UintPtrGreaterThanOrEqual(opcode, UintPtrConstant(static_cast<int>( Bytecode::kFirstShortStar))) - StoreRegisterForShortStar@../../src/interpreter/interpreter-assembler.cc:310 - AssembleArchInstruction@../../src/compiler/backend/arm64/code-generator-arm64.cc:978 ] Assert - AssembleArchInstruction@../../src/compiler/backend/arm64/code-generator-arm64.cc:978 [ Assert: UintPtrLessThanOrEqual( opcode, UintPtrConstant(static_cast<int>(Bytecode::kLastShortStar))) - StoreRegisterForShortStar@../../src/interpreter/interpreter-assembler.cc:314 - AssembleArchInstruction@../../src/compiler/backend/arm64/code-generator-arm64.cc:978# 此时 x3 大于或等于 187# 进一步判断 x3 是否大于 202,如果大于,则跳转到后面的 B30xc6ccfe4a8be8 e8 7103287f cmp w3, #0xca (202)0xc6ccfe4a8bec ec 540001c8 b.hi #+0x38 (addr 0xc6ccfe4a8c24) -- B4 start --# 此时 x3 介于 187 和 202 之间,是一个 Short Star# Short Star 做的事情就是把 `accumulator` 寄存器的值复制到 r0-r15 当中指定的寄存器# 所以直接在这里实现了 Short Star 的语义,而不是单独跑一段代码去执行它# 由于 r0-r15 寄存器保存在栈上,所以通过 x3 计算出 Short Star 要写到哪个寄存器# 进而直接计算要写到的栈的地址的偏移# 寻找一个通项公式,找到 Star0-Star15 要写入的地址:# Star0(202): r0 的位置在栈顶再往下的 8 字节,即 fp 减去 56# Star0(187): r15 的位置在栈顶再往下的 8*16 字节,即 fp 减去 176# 相对 fp 的偏移量就等于 x3 * 8 - 1672# 从而得到下面的代码:# 计算 x3 = x3 * 80xc6ccfe4a8bf0 f0 d37df063 lsl x3, x3, #3# 把之前 LdaSmi 计算得到的 x4 寄存器写到 `accumulator` 即 x0 寄存器当中# 这里 x0 = 2 * x4,是因为 v8 用最低位表示这是一个 Smi(用 0 表示)还是一个指针(用 1 表示)0xc6ccfe4a8bf4 f4 0b040080 add w0, w4, w4 ] Assert - AssembleArchInstruction@../../src/compiler/backend/arm64/code-generator-arm64.cc:978# 计算 x3 = x3 - 1672,就得到了相对 fp 的偏移量0xc6ccfe4a8bf8 f8 d11a2063 sub x3, x3, #0x688 (1672)# 从 fp 的地址读取函数调用前的 fp0xc6ccfe4a8bfc fc f94003a4 ldr x4, [fp]# 把 `accumulator` 写入到相对函数调用前的 fp 的对应位置0xc6ccfe4a8c00 100 f8236880 str x0, [x4, x3]# 下面就是 Dispatch 逻辑,只不过这次是执行完 Short Star 字节码后的 Dispatch# x19 = x3 + 1,就是 bytecode offset 前进一个字节,指向下一个字节码0xc6ccfe4a8c04 104 91000673 add x19, x19, #0x1 (1)# x20 是 bytecode array,从 bytecode array 读取下一个字节码的第一个字节到 x3 寄存器0xc6ccfe4a8c08 108 38736a83 ldrb w3, [x20, x19]# 从栈上读取 x21 即 dispatch table register0xc6ccfe4a8c0c 10c f9400ff5 ldr x21, [sp, #24]# 从 dispatch table,以 x3 为下标,读取下一个字节码对应的代码的地址0xc6ccfe4a8c10 110 f8637aa2 ldr x2, [x21, x3, lsl #3]# 恢复调用函数前的旧 fp 和 lr0xc6ccfe4a8c14 114 a9407bbd ldp fp, lr, [fp]# 恢复调用函数前的旧 sp0xc6ccfe4a8c18 118 910103ff add sp, sp, #0x40 (64)# 下一个字节码对应的代码的地址已经保存在 x2 寄存器当中,跳转过去0xc6ccfe4a8c1c 11c aa0203f1 mov x17, x20xc6ccfe4a8c20 120 d61f0220 br x17 -- B5 start (no frame) -- -- B3 start (deferred) --# 此时 x3 大于 202,为非法字节码,跳转到错误处理的逻辑 [ - LoadFromConstantsTable@../../src/codegen/arm64/macro-assembler-arm64.cc:2166 [ - LoadRoot@../../src/codegen/arm64/macro-assembler-arm64.cc:19540xc6ccfe4a8c24 124 f94d5f41 ldr x1, [x26, #6840] ] [ - DecompressTagged@../../src/codegen/arm64/macro-assembler-arm64.cc:34480xc6ccfe4a8c28 128 d28bd170 movz x16, #0x5e8b0xc6ccfe4a8c2c 12c b8706821 ldr w1, [x1, x16]0xc6ccfe4a8c30 130 8b010381 add x1, x28, x1 ] ] [ Frame: NO_FRAME_TYPE [ Inlined Trampoline for call to AbortCSADcheck - CallBuiltin@../../src/codegen/arm64/macro-assembler-arm64.cc:23910xc6ccfe4a8c34 134 96a4e10b bl #-0x56c7bd4 (addr 0xc6ccf8de1060) ;; code: Builtin::AbortCSADcheck ] ]0xc6ccfe4a8c38 138 d4200000 brk #0x00xc6ccfe4a8c3c 13c d4200000 brk #0x00xc6ccfe4a8c40 140 d503201f nop ;;; Safepoint table. - Emit@../../src/codegen/safepoint-table.cc:187 ]External Source positions: pc offset fileid line b4 380 72Safepoints (entries = 2, byte size = 12)0xc6ccfe4a8b7c 7c slots (sp->fp): 010010000xc6ccfe4a8ba8 a8 slots (sp->fp): 00001000RelocInfo (size = 3)0xc6ccfe4a8c34 code target (BUILTIN AbortCSADcheck) (0xc6ccf8de1060)为了简化代码,关闭了 control flow integrity 相关的代码生成,具体方法是运行 gn args out/arm64.optdebug,追加一行 v8_control_flow_integrity = false,再重新 autoninja -C out/arm64.optdebug d8。
以上是 debug 模式下生成的代码,多了很多检查;如果在 release 模式下,可以观察到更优的指令:
kind = BYTECODE_HANDLERname = LdaSmicompiler = turbofanaddress = 0x31a000462bdInstructions (size = 80)# 从这里开始实现 LdaSmi 的语义# 计算 x19 + 1 的值并写入 x1,得到 LdaSmi 的第二个字节相对 bytecode array 的偏移0xc903f8193400 0 91000661 add x1, x19, #0x1 (1)# 从 x20 + x1 地址读取 LdaSmi 的第二个字节到 x1,也就是要加载到 `accumulator` 的值,# 之后 x1 的值会写入到 x0,也就是 `accumulator` 对应的寄存器0xc903f8193404 4 38e16a81 ldrsb w1, [x20, x1]# Dispatch: 找到下一个 Opcode 对应的代码的入口,然后跳转过去# x19 = x19 + 2,就是 bytecode offset 前进两个字节,指向下一个字节码0xc903f8193408 8 91000a73 add x19, x19, #0x2 (2)# x20 是 bytecode array,从 bytecode array 读取下一个字节码的第一个字节到 x3 寄存器0xc903f819340c c 38736a83 ldrb w3, [x20, x19]# 计算 x4 = x3 * 8,也就是 dispatch table 中下一个字节码对应的代码地址的字节偏移0xc903f8193410 10 d37df064 lsl x4, x3, #3# 把之前 LdaSmi 计算得到的 x1 寄存器写到 `accumulator` 即 x0 寄存器当中# 这里 x0 = 2 * x1,是因为 v8 用最低位表示这是一个 Smi(用 0 表示)还是一个指针(用 1 表示)0xc903f8193414 14 0b010020 add w0, w1, w1# 如果 x3 寄存器大于或等于 187,说明这个字节码可能是 Short Star 字节码,就跳转到后面的 0xc903f819342c 地址0xc903f8193418 18 7102ec7f cmp w3, #0xbb (187)0xc903f819341c 1c 54000082 b.hs #+0x10 (addr 0xc903f819342c)# 如果没有跳转,此时 x3 寄存器小于 187# 从 dispatch table,以 x3 为下标(x4 = x3 * 8),读取下一个字节码对应的代码的地址0xc903f8193420 20 f8646aa2 ldr x2, [x21, x4]# 跳转到下一个字节码对应的代码的地址0xc903f8193424 24 aa0203f1 mov x17, x20xc903f8193428 28 d61f0220 br x17# 实现 Short Star 字节码# 计算出要写入的 r0-r15 寄存器相对 fp 的偏移量 x3 * 8 - 1672# 这个偏移量的计算公式在前面推导过,此时 x4 等于 x3 * 80xc903f819342c 2c d11a2081 sub x1, x4, #0x688 (1672)0xc903f8193430 30 aa1d03e3 mov x3, fp# 把 `accumulator` 写入到相对 fp 的对应位置0xc903f8193434 34 f8216860 str x0, [x3, x1]# 下面就是 Dispatch 逻辑,只不过这次是执行完 Short Star 字节码后的 Dispatch# x19 = x19 + 1,就是 bytecode offset 前进一个字节,指向下一个字节码0xc903f8193438 38 91000673 add x19, x19, #0x1 (1)# x20 是 bytecode array,从 bytecode array 读取下一个字节码的第一个字节到 x1 寄存器0xc903f819343c 3c 38736a81 ldrb w1, [x20, x19]# 从 dispatch table,以 x1 为下标,读取下一个字节码对应的代码的地址0xc903f8193440 40 f8617aa2 ldr x2, [x21, x1, lsl #3]# 跳转到下一个字节码对应的代码的地址0xc903f8193444 44 aa0203f1 mov x17, x20xc903f8193448 48 d61f0220 br x170xc903f819344c 4c d503201f nop可见 release 模式下的代码还是简单了许多,保证了性能。
有的 Opcode 后面不会紧接着出现 Short Star,此时 Dispatch 会减少一次特判,代码更加简单,以 Ldar 为例:
kind = BYTECODE_HANDLERname = Ldarcompiler = turbofanaddress = 0x31a00046245Instructions (size = 44)# Ldar 的语义是,把指定参数寄存器的值写入到 `accumulator` 当中# 参数寄存器的位置记录在 Ldar 字节码的第二个字节中# 如:0b 04 对应 Ldar a1# 计算 x19 + 1 的值并写入 x1,得到 Ldar 的第二个字节相对 bytecode array 的偏移0xc903f8193320 0 91000661 add x1, x19, #0x1 (1)# 从 x20 + x1 地址读取 Ldar 的第二个字节到 x1,也就是参数寄存器相对 fp 的下标0xc903f8193324 4 38a16a81 ldrsb x1, [x20, x1]# 相对 fp 以 x1 为下标,读取参数寄存器的值到 x1 寄存器0xc903f8193328 8 aa1d03e3 mov x3, fp0xc903f819332c c f8617861 ldr x1, [x3, x1, lsl #3]# Dispatch: 找到下一个 Opcode 对应的代码的入口,然后跳转过去# x19 = x19 + 2,就是 bytecode offset 前进两个字节,指向下一个字节码0xc903f8193330 10 91000a73 add x19, x19, #0x2 (2)# x20 是 bytecode array,从 bytecode array 读取下一个字节码的第一个字节到 x3 寄存器0xc903f8193334 14 38736a83 ldrb w3, [x20, x19]# 从 dispatch table,以 x3 为下标,读取下一个字节码对应的代码的地址0xc903f8193338 18 f8637aa2 ldr x2, [x21, x3, lsl #3]# 把参数寄存器的值写入到 `accumulator` 也就是 x0 当中0xc903f819333c 1c aa0103e0 mov x0, x1# 跳转到下一个字节码对应的代码的地址0xc903f8193340 20 aa0203f1 mov x17, x20xc903f8193344 24 d61f0220 br x170xc903f8193348 28 d503201f nop小结一下:
- Ignition 给每种可能的 Opcode 类型生成一段代码
- 这段代码会进行一些检查(仅 Debug 模式下),然后在汇编里实现这个字节码的功能
- 执行完字节码后,进入 Dispatch 逻辑,寻找下一个字节码对应的代码的地址
- 特别地,如果下一个字节码是 Short Star (Star0-Star15),因为它比较简单和常见,就直接执行它,执行完再重新寻找再下一个字节码对应的代码的地址
- 这些 Opcode 对应的代码会在 v8 编译过程中通过
mksnapshot命令一次性生成好,运行时直接复用,不用重新生成 - V8 的值的最低位标识了它的类型:0 表示 Smi,1 表示指针,因此在存储 Smi 的时候,寄存器里保存的是实际值的两倍,这样最低位就是 0
参考¶
- Firing up the Ignition interpreter
- How to get Node.js to trace ignition within v8? with --trace-ignition
- Ignition: Jump-starting an Interpreter for V8
- Ignition: V8 Interpreter
- Introduction to TurboFan
- JavaScript Bytecode – v8 Ignition Instructions
- Understanding V8’s Bytecode
- V8 Documentation
- V8 Ignition
- V8 TurboFan
- V8 Turbolizer v13.4
- V8: Behind the Scenes (February Edition feat. A tale of TurboFan)
- danbev/learning-v8
- V8 Internals: How Small is a “Small Integer?”
Generated by RSStT. The copyright belongs to the original author.