# V8 Ignition 解释器的内部实现探究

## 背景

V8 是一个很常见的 JavaScript 引擎，运行在很多的设备上，因此想探究一下它内部的部分实现。本博客在 ARM64 Ubuntu 24.04 平台上针对 [V8 12.8.374.31](https://chromium.googlesource.com/v8/v8.git/+/6f774f929205be0a49cf861b8d73a92655e1dd36) 版本进行分析。本博客主要分析了 V8 的 Ignition 解释器的解释执行部分。

## 编译 V8

首先简单过一下 v8 的源码获取以及编译流程，主要参考了 [Checking out the V8 source code](https://v8.dev/docs/source-code) 和 [Compiling on Arm64 Linux](https://v8.dev/docs/compile-arm64):

```shell
# setup depot_tools
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=$PWD/depot_tools:$PATH

# clone v8 repos
mkdir ~/v8
cd ~/v8
fetch v8
cd v8

# switch to specified tag
git checkout 12.8.374.31
gclient sync --verbose

# install dependencies
./build/install-build-deps.sh

# install llvm 19
wget https://mirrors.tuna.tsinghua.edu.cn/llvm-apt/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 19 -m https://mirrors.tuna.tsinghua.edu.cn/llvm-apt
rm llvm.sh
sudo apt install -y lld clang-19

# fix incompatibilities with system clang 19
sed -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.optdebug
tools/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 一共有这些解释器或编译器，按照其优化等级从小到大的顺序：

1. [Ignition](https://v8.dev/docs/ignition): 解释器
1. [SparkPlug](https://v8.dev/blog/sparkplug): 不优化的快速编译器，追求快的编译速度
1. [Maglev](https://v8.dev/blog/maglev)：做优化的编译器，寻求编译速度和编译质量的平衡
1. [TurboFan](https://v8.dev/docs/turbofan)：做优化的编译器，寻求更好的编译质量

在 JS 的使用场景，不同代码被调用的次数以及对及时性的需求差别很大，为了适应不同的场景，V8 设计了这些解释器和编译器来提升整体的性能：执行次数少的代码，倾向于用更低优化等级的解释器或编译器，追求更低的优化开销；执行次数多的代码，当编译优化时间不再成为瓶颈，则倾向于用更高优化等级的编译器，追求更高的执行性能。

## Ignition 解释器

### 分析样例 JS 代码

首先来观察一下 Ignition 解释器的工作流程。写一段简单的 JS 代码：

```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:

```text
[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` 为结果返回。接下来看它会被翻译成什么字节码：

```text
[generated bytecode for function:  (0x1f8e002988f5 <SharedFunctionInfo>)]
Bytecode length: 28
Parameter count 1
Register count 4
Frame 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                Return
Constant 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` 函数：

1. `LdaConstant [0]`: 把 Constant Pool 的第 0 项也就是 `FixedArray[2]` 写入 `accumulator` 寄存器当中
1. `Star1`: 把 `accumulator` 寄存器的值拷贝到 `r1` 寄存器，结合上一条字节码，就是设置 `r1 = FixedArray[2]`
1. `Mov <closure>, r2`: 把 `<closure>` 拷贝到 `r2` 寄存器，猜测这里的 `<closure>` 对应的是 `add` 函数
1. `CallRuntime [DeclareGlobals], r1-r2`: 调用运行时的 `DeclareGlobals` 函数，并传递两个参数，分别是 `r1` 和 `r2`；有意思的是，`CallRuntime` 的参数必须保存在连续的寄存器当中，猜测是为了节省编码空间

至此，`add` 函数就声明完成了。接下来，就要实现 `add(1, 2)` 的调用：

1. `LdaGlobal [1], [0]`: 把 Constant Pool 的第 1 项也就是 `"add"` 这个字符串写入 `accumulator`，最后的 `[0]` 和 FeedBackVector 有关，目前先忽略
1. `Star1`: 把 `accumulator` 寄存器的值拷贝到 `r1` 寄存器，结合上一条字节码，就是设置 `r1 = "add"`
1. `LdaSmi [1]`: 把小整数（Small integer, Smi）`1` 写入 `accumulator`
1. `Star2`: 把 `accumulator` 寄存器的值拷贝到 `r2` 寄存器，结合上一条字节码，就是设置 `r2 = 1`
1. `LdaSmi [2]`: 把小整数（Small integer, Smi）`2` 写入 `accumulator`
1. `Star3`: 把 `accumulator` 寄存器的值拷贝到 `r3` 寄存器，结合上一条字节码，就是设置 `r3 = 2`
1. `CallUndefinedReceiver2 r1, r2, r3, [2]`: 根据 `r1` 调用一个函数，并传递两个参数 `r2, r3`（函数名称最后的 `2` 表示有两个参数），最后的 `[2]` 也和 FeedBackVector 有关

这样就完成了函数调用。

接下来观察 `add` 函数的 AST：

```text
[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：

```text
[generated bytecode for function: add (0x10c700298955 <SharedFunctionInfo add>)]
Bytecode length: 6
Parameter count 3
Register count 0
Frame size 0
         0x6ed0004008c @    0 : 0b 04             Ldar a1
         0x6ed0004008e @    2 : 3b 03 00          Add a0, [0]
         0x6ed00040091 @    5 : af                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
```

1. `Ldar a1`: 把第二个参数 `a1` 也就是 `b` 写入 `accumulator` 寄存器
1. `Add a0, [0]`: 求第一个参数 `a0` 也就是 `a` 与 `accumulator` 寄存器的和，写入到 `accumulator` 寄存器当中，结合上一条 Bytecode，就是 `accumulator = a0 + a1`；`[0]` 和 FeedBackVector 有关
1. `Return`: 把 `accumulator` 中的值作为返回值，结束函数调用

简单小结一下 V8 的字节码：

1. 有若干个局部的寄存器，在操作数中以 `rn` 的形式出现，`n` 是寄存器编号
1. 有 `accumulator` 局部寄存器，作为部分字节码的隐含输入或输出（`Add`）
1. 有若干个参数，在操作数中以 `an` 的形式出现，`n` 是参数编号
1. 操作数还可以出现立即数参数 `[imm]`，可能是整数字面量（`LdaSmi`），可能是下标（`LdaConstant`），也可能是 FeedBackVector 的 slot

有了字节码以后，接下来观察 Ignition 具体是怎么解释执行这些字节码的。

### 解释执行

为了实际执行这些字节码，Ignition 的做法是：

1. 给每种可能的 Opcode 生成一段二进制代码，这段代码会实现这个 Opcode 的功能
1. 在运行时维护一个 dispatch table，维护了 Opcode 到二进制代码地址的映射关系
1. 在每段代码的结尾，找到下一个 Opcode 对应的代码的地址，然后跳转过去
1. 调用函数时，先做一系列的准备，找到函数第一个字节码的 Opcode 对应的代码的地址，跳转过去

由于 Opcode 的种类是固定的，所以实际运行 V8 的时候，这些代码已经编译好了，只需要在运行时初始化对应的数据结构即可。这个代码的生成和编译过程，也不是由 C++ 编译器做的，而是有一个 `mksnapshot` 命令来完成初始化，你可以认为它把这些 Opcode 对应的汇编指令都预先生成好，运行时直接加载即可。

首先来看 Ignition 的怎么实现各种 Opcode 的，以 `LdaSmi` 为例，它的作用是小的把立即数（Smi=Small integer）写入到 `accumulator` 当中，这段在 `v8/src/interpreter/interpreter-generator.cc` 的代码实现了这个逻辑：

```cpp
// 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 对应的汇编，由于这段汇编有点长，具体做的事情和对应的源码已经通过注释标注出来：

```text
kind = BYTECODE_HANDLER
name = LdaSmi
compiler = turbofan
address = 0x31a000906fd

Instructions (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:232
0xc6ccfe4a8b00     0  10000010       adr x16, #+0x0 (addr 0xc6ccfe4a8b00)
0xc6ccfe4a8b04     4  eb02021f       cmp x16, x2
0xc6ccfe4a8b08     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:4011
0xc6ccfe4a8b0c     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:1590
0xc6ccfe4a8b18    18  910003f0       mov x16, sp
                      [  - LogicalMacro@../../src/codegen/arm64/macro-assembler-arm64.cc:197
0xc6ccfe4a8b1c    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_TYPE
0xc6ccfe4a8b24    24  52800780       movz w0, #0x3c
0xc6ccfe4a8b28    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, #0x22
0xc6ccfe4a8b34    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:1954
0xc6ccfe4a8b58    58  f94d5f42       ldr x2, [x26, #6840]
                      ]
                      [  - DecompressTagged@../../src/codegen/arm64/macro-assembler-arm64.cc:3448
0xc6ccfe4a8b5c    5c  d28bcef0       movz x16, #0x5e77
0xc6ccfe4a8b60    60  b8706842       ldr w2, [x2, x16]
0xc6ccfe4a8b64    64  8b020382       add x2, x28, x2
                      ]
                    ]
0xc6ccfe4a8b68    68  aa1403e0       mov x0, x20
0xc6ccfe4a8b6c    6c  f94ecf50       ldr x16, [x26, #7576]
                    [  - CallCFunction@../../src/codegen/arm64/macro-assembler-arm64.cc:2106
0xc6ccfe4a8b70    70  10000068       adr x8, #+0xc (addr 0xc6ccfe4a8b7c)
0xc6ccfe4a8b74    74  a93f235d       stp fp, x8, [x26, #-16]
0xc6ccfe4a8b78    78  d63f0200       blr x16
0xc6ccfe4a8b7c    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:1954
0xc6ccfe4a8b84    84  f94d5f42       ldr x2, [x26, #6840]
                      ]
                      [  - DecompressTagged@../../src/codegen/arm64/macro-assembler-arm64.cc:3448
0xc6ccfe4a8b88    88  d28bcf70       movz x16, #0x5e7b
0xc6ccfe4a8b8c    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:2106
0xc6ccfe4a8b9c    9c  10000068       adr x8, #+0xc (addr 0xc6ccfe4a8ba8)
0xc6ccfe4a8ba0    a0  a93f235d       stp fp, x8, [x26, #-16]
0xc6ccfe4a8ba4    a4  d63f0200       blr x16
0xc6ccfe4a8ba8    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 寄存器的值到 x20
0xc6ccfe4a8bb4    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 字节码，就跳转到后面的 B2
0xc6ccfe4a8bc4    c4  7102ec7f       cmp w3, #0xbb (187)
0xc6ccfe4a8bc8    c8  54000102       b.hs #+0x20 (addr 0xc6ccfe4a8be8)
                  -- B1 start --
# 此时 x3 小于 187
# 从栈上读取 x21 即 dispatch table register
0xc6ccfe4a8bcc    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 和 lr
0xc6ccfe4a8bd8    d8  a9407bbd       ldp fp, lr, [fp]
# 恢复调用函数前的旧 sp
0xc6ccfe4a8bdc    dc  910103ff       add sp, sp, #0x40 (64)
# 下一个字节码对应的代码的地址已经保存在 x2 寄存器当中，跳转过去
0xc6ccfe4a8be0    e0  aa0203f1       mov x17, x2
0xc6ccfe4a8be4    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，如果大于，则跳转到后面的 B3
0xc6ccfe4a8be8    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 * 8
0xc6ccfe4a8bf0    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 的地址读取函数调用前的 fp
0xc6ccfe4a8bfc    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 register
0xc6ccfe4a8c0c   10c  f9400ff5       ldr x21, [sp, #24]
# 从 dispatch table，以 x3 为下标，读取下一个字节码对应的代码的地址
0xc6ccfe4a8c10   110  f8637aa2       ldr x2, [x21, x3, lsl #3]
# 恢复调用函数前的旧 fp 和 lr
0xc6ccfe4a8c14   114  a9407bbd       ldp fp, lr, [fp]
# 恢复调用函数前的旧 sp
0xc6ccfe4a8c18   118  910103ff       add sp, sp, #0x40 (64)
# 下一个字节码对应的代码的地址已经保存在 x2 寄存器当中，跳转过去
0xc6ccfe4a8c1c   11c  aa0203f1       mov x17, x2
0xc6ccfe4a8c20   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:1954
0xc6ccfe4a8c24   124  f94d5f41       ldr x1, [x26, #6840]
                      ]
                      [  - DecompressTagged@../../src/codegen/arm64/macro-assembler-arm64.cc:3448
0xc6ccfe4a8c28   128  d28bd170       movz x16, #0x5e8b
0xc6ccfe4a8c2c   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:2391
0xc6ccfe4a8c34   134  96a4e10b       bl #-0x56c7bd4 (addr 0xc6ccf8de1060)    ;; code: Builtin::AbortCSADcheck
                      ]
                    ]
0xc6ccfe4a8c38   138  d4200000       brk #0x0
0xc6ccfe4a8c3c   13c  d4200000       brk #0x0
0xc6ccfe4a8c40   140  d503201f       nop
                  ;;; Safepoint table. - Emit@../../src/codegen/safepoint-table.cc:187
                  ]

External Source positions:
 pc offset  fileid  line
        b4       380        72


Safepoints (entries = 2, byte size = 12)
0xc6ccfe4a8b7c     7c  slots (sp->fp): 01001000
0xc6ccfe4a8ba8     a8  slots (sp->fp): 00001000

RelocInfo (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 模式下，可以观察到更优的指令：

```text
kind = BYTECODE_HANDLER
name = LdaSmi
compiler = turbofan
address = 0x31a000462bd

Instructions (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, x2
0xc903f8193428    28  d61f0220       br x17

# 实现 Short Star 字节码
# 计算出要写入的 r0-r15 寄存器相对 fp 的偏移量 x3 * 8 - 1672
# 这个偏移量的计算公式在前面推导过，此时 x4 等于 x3 * 8
0xc903f819342c    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, x2
0xc903f8193448    48  d61f0220       br x17
0xc903f819344c    4c  d503201f       nop
```

可见 release 模式下的代码还是简单了许多，保证了性能。

有的 Opcode 后面不会紧接着出现 Short Star，此时 Dispatch 会减少一次特判，代码更加简单，以 `Ldar` 为例：

```text
kind = BYTECODE_HANDLER
name = Ldar
compiler = turbofan
address = 0x31a00046245

Instructions (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, fp
0xc903f819332c     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, x2
0xc903f8193344    24  d61f0220       br x17
0xc903f8193348    28  d503201f       nop
```

小结一下：

1. Ignition 给每种可能的 Opcode 类型生成一段代码
1. 这段代码会进行一些检查（仅 Debug 模式下），然后在汇编里实现这个字节码的功能
1. 执行完字节码后，进入 Dispatch 逻辑，寻找下一个字节码对应的代码的地址
1. 特别地，如果下一个字节码是 Short Star (Star0-Star15)，因为它比较简单和常见，就直接执行它，执行完再重新寻找再下一个字节码对应的代码的地址
1. 这些 Opcode 对应的代码会在 v8 编译过程中通过 `mksnapshot` 命令一次性生成好，运行时直接复用，不用重新生成
1. V8 的值的最低位标识了它的类型：0 表示 Smi，1 表示指针，因此在存储 Smi 的时候，寄存器里保存的是实际值的两倍，这样最低位就是 0

## 参考

- [Firing up the Ignition interpreter](https://v8.dev/blog/ignition-interpreter)
- [How to get Node.js to trace ignition within v8? with --trace-ignition](https://stackoverflow.com/questions/73337772/how-to-get-node-js-to-trace-ignition-within-v8-with-trace-ignition)
- [Ignition: Jump-starting an Interpreter for V8](https://dynamic-languages-symposium.org/dls-16/program/media/McIlroy_2016_IgnitionJumpStartingAnInterpreterForV8_Dls.pdf)
- [Ignition: V8 Interpreter](https://docs.google.com/document/d/11T2CRex9hXxoJwbYqVQ32yIPMh0uouUZLdyrtmMoL44)
- [Introduction to TurboFan](https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/)
- [JavaScript Bytecode – v8 Ignition Instructions](https://www.alibabacloud.com/blog/javascript-bytecode-v8-ignition-instructions_599188)
- [Understanding V8’s Bytecode](https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775)
- [V8 Documentation](https://v8.dev/docs)
- [V8 Ignition](https://v8.dev/docs/ignition)
- [V8 TurboFan](https://v8.dev/docs/turbofan)
- [V8 Turbolizer v13.4](https://v8.github.io/tools/v13.4/turbolizer/index.html)
- [V8: Behind the Scenes (February Edition feat. A tale of TurboFan)](https://benediktmeurer.de/2017/03/01/v8-behind-the-scenes-february-edition)
- [danbev/learning-v8](https://github.com/danbev/learning-v8)
- [V8 Internals: How Small is a “Small Integer?”](https://medium.com/fhinkel/v8-internals-how-small-is-a-small-integer-e0badc18b6da)
