Mas0n
to be reverse engineer🐧
翻车鱼

Ignition Bytecode for V8 Interpreter

Ignition Bytecode for V8 Interpreter

简要学习V8 Bytecode的组成以及执行方式。

纵览各个JavaScript引擎的实现,我们发现基于字节码的实现是主流。

  • 苹果公司的JavaScriptCore(JSC) 引擎实现了一个寄存器机(Register Machine)
  • Mozilla 公司的SpiderMonkey引擎实现了堆栈机(Stack Machine)
  • 微软的 Chakra 实现的是寄存器机(Register Machine)

字节码是机器代码的抽象。如果字节码采用和物理 CPU 相同的计算模型进行设计,则将字节码编译为机器代码更容易。这就是为什么解释器(Interpreter)常常是寄存器或堆栈。

但在早些年前,V8引擎选择了直接将 JS 代码编译到机器代码执行。虽然使其执行性能上登峰造极,但却带来了内存占用过大的问题。

2017年,V8引擎为解决由此引发的问题,引入了Ignition。

Ignition是一个具有累加器的寄存器机(Register Machine)。

从源码到解释器

v8引擎拥有众多子模块,这里列出其四大重要组成:

  • Parser:将JavaScript源码转换为Abstract Syntax Tree (AST)
  • Ignition:Interpreter,即解释器,将AST转换为Bytecode,解释执行Bytecode;同时收集TurboFan优化编译所需的信息
  • TurboFan:Compiler,即编译器,TurboFan利用Ignition所收集的类型信息,将Bytecode转换为优化的汇编代码
  • Orinoco:Garbage Collector,垃圾回收模块,负责将程序不再需要的内存空间回收;

Parser将JavaScript源码解析为AST,而后Ignition将AST转换为Bytecode,最后经TurboFan将Bytecode转换为经其优化的Machine Code。

其流水线图清晰的展示了引擎执行流程。

https://cdn.shi1011.cn/2022/11/1469aacbfb4ac613fd6c10988e82114f.png?imageMogr2/format/webp/interlace/0/quality/90|watermark/2/text/wqlNYXMwbg/font/bXN5aGJkLnR0Zg/fontsize/14/fill/IzMzMzMzMw/dissolve/80/gravity/southeast/dx/5/dy/5

Ignition Bytecode

最直观的,我们希望能够通过一个简单的方式来观察语句对应的字节码。

下面是一个极其简单的代码块。

undefined;

我们可以通过V8提供的d8添加--print-bytecode来打印出对应字节码。

编译d8

下载编译工具链

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

导入环境变量

export PATH=`pwd`/depot_tools:"$PATH"

拉取v8源码,这里使用版本为10.8.79

gclient sync 
fetch v8
git pull origin
git checkout 10.8.79

为方便查看字节码,编译时开启反汇编以及常量显示

tools/dev/v8gen.py x64.release -- \
>   v8_enable_disassembler=true \
>   v8_enable_object_print=true

ninja -C out.gn/x64.release d8

简单测试

out.gn/x64.release/d8 --print-bytecode -e "1;"

得到字节码输出

[generated bytecode for function:  (0x1bf600199c39 <SharedFunctionInfo>)]
Bytecode length: 4
Parameter count 1
Register count 1
Frame size 8
Bytecode age: 0
         0x1bf600199c9e @    0 : 0d 01             LdaSmi [1]
         0x1bf600199ca0 @    2 : c5                Star0
         0x1bf600199ca1 @    3 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

Accumulator Load Instruction

上文提到,Ignition是一个具有累加器的寄存器机,这意味Ignition中必然存在特殊的指令来操纵累加器。

Ignition拥有16个通用寄存器,这里将其称为r0r15

我们可以在https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h看到相关定义。

// The list of single-byte Star variants, in the format of BYTECODE_LIST.
#define SHORT_STAR_BYTECODE_LIST(V)                              \
  V(Star15, ImplicitRegisterUse::kReadAccumulatorWriteShortStar) \
  V(Star14, ImplicitRegisterUse::kReadAccumulatorWriteShortStar) \
  V(Star13, ImplicitRegisterUse::kReadAccumulatorWriteShortStar) \
  V(Star12, ImplicitRegisterUse::kReadAccumulatorWriteShortStar) \
  V(Star11, ImplicitRegisterUse::kReadAccumulatorWriteShortStar) \
  V(Star10, ImplicitRegisterUse::kReadAccumulatorWriteShortStar) \
  V(Star9, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star8, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star7, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star6, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star5, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star4, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star3, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star2, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star1, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)  \
  V(Star0, ImplicitRegisterUse::kReadAccumulatorWriteShortStar)

以及一个累加器寄存器,这里简称为acc(Accumulator Rigister)。

从上面的测试中,我们就能发现三条指令,这里先给出指令解析。

Lda*

我们可以将其理解为Load * into accumulator register

例如:

  • LdaUndefined,即将undefined放入Accumulator Rigister
  • LdaSmi [10],即将10放入Accumulator Rigister

Star*

我们可以将此指令理解为Store accumulator register’s value into *

例如:

  • Star0,即将Accumulator Rigister上的值放入r0寄存器。
  • Star3,即将Accumulator Rigister上的值放入r3寄存器。

Return

Return返回acc中的内容。

下面详述部分累加器操作指令

Oddball

Oddball类型包含以下

  • null
  • undefined
  • true
  • false

我们从一个最小语句开始:

out.gn/x64.release/d8 --print-bytecode -e "undefined;"

得到以下输出:

         0x102300199c9e @    0 : 0e                LdaUndefined
         0x102300199c9f @    1 : c5                Star0
         0x102300199ca0 @    2 : aa                Return

在这样一条语句中,字节码表示的伪代码如下

acc = undefined;
r0 = acc;
return acc;

明显的,其返回值即undefined

null

out.gn/x64.release/d8 --print-bytecode -e "null;"

对应的字节码如下:

         0x1a3900199c9e @    0 : 0f                LdaNull

布尔类型

键入true

out.gn/x64.release/d8 --print-bytecode -e "true;"

得到以下输出:

         0x3c100199c9e @    0 : 11                LdaTrue
         0x3c100199c9f @    1 : c5                Star0
         0x3c100199ca0 @    2 : aa                Return

键入false

out.gn/x64.release/d8 --print-bytecode -e "false;"

得到以下输出:

         0x146f00199c9e @    0 : 12                LdaFalse
         0x146f00199c9f @    1 : c5                Star0
         0x146f00199ca0 @    2 : aa                Return

关键指令分别为LdaTrueLdaFalse,其返回值为truefalse

Small integers

首先尝试0

out.gn/x64.release/d8 --print-bytecode -e "0;"

得到以下输出:

         0x2c8f00199c9e @    0 : 0c                LdaZero
         0x2c8f00199c9f @    1 : c5                Star0
         0x2c8f00199ca0 @    2 : aa                Return

与Oddball类型相似,关键指令为LdaZero

当我们输入单字节大小以内且大于1的数字时

out.gn/x64.release/d8 --print-bytecode -e "10;"

得到以下输出:

         0x1f200199c9e @    0 : 0d 0a             LdaSmi [10]
         0x1f200199ca0 @    2 : c5                Star0
         0x1f200199ca1 @    3 : aa                Return

不同于0,关键指令变为LdaSmi,显然[10]中的10为立即数。

LdaSmi指令占用两字节,0d为操作码,0a为立即数。

当我们输入双字节大小的整数时

out.gn/x64.release/d8 --print-bytecode -e "1000;"

得到以下输出:

         0xd3b00199c9e @    0 : 00 0d e8 03       LdaSmi.Wide [1000]
         0xd3b00199ca2 @    4 : c5                Star0
         0xd3b00199ca3 @    5 : aa                Return

指令由LdaSmi变为了LdaSmi.Wide,占用了4字节,00 0d为操作码,e8 03为立即数。

同样的,当我们输入四字节大小的整数时

out.gn/x64.release/d8 --print-bytecode -e "100000;"

得到以下输出:

         0x16cc00199c9e @    0 : 01 0d 40 42 0f 00 LdaSmi.ExtraWide [1000000]
         0x16cc00199ca4 @    6 : c5                Star0
         0x16cc00199ca5 @    7 : aa                Return

指令为LdaSmi.ExtraWide,占用了6字节,01 0d为操作码,40 42 0f 00为立即数。

可以看到,Ignition字节码的操作数宽度能够自由伸缩,下图展示了其指令内存排布。

https://cdn.shi1011.cn/2022/11/70ac4019d0ddf874d9f9e0c1fd9bf7e4.png?imageMogr2/format/webp/interlace/0/quality/90|watermark/2/text/wqlNYXMwbg/font/bXN5aGJkLnR0Zg/fontsize/14/fill/IzMzMzMzMw/dissolve/80/gravity/southeast/dx/5/dy/5

HeapNumber

浮点数如何表示呢?

out.gn/x64.release/d8 --print-bytecode -e "10.03;"

得到以下输出:

[generated bytecode for function:  (0x2ea900199c39 <SharedFunctionInfo>)]
Bytecode length: 4
Parameter count 1
Register count 1
Frame size 8
Bytecode age: 0
         0x2ea900199cb6 @    0 : 13 00             LdaConstant [0]
         0x2ea900199cb8 @    2 : c5                Star0
         0x2ea900199cb9 @    3 : aa                Return
Constant pool (size = 1)
0x2ea900199c7d: [FixedArray] in OldSpace
 - map: 0x2ea900002229 <Map(FIXED_ARRAY_TYPE)>
 - length: 1
           0: 0x2ea900199c89 <HeapNumber 10.03>
Handler Table (size = 0)
Source Position Table (size = 0)

不同于整数类型,在Ignition字节码中,浮点数被放置在堆(Heap)上,指令为LdaConstant

LdaConstant [0]即意味着从Constant pool读取索引为0的元素,即<HeapNumber 10.03>

下图展示了LdaConstant指令的取指过程

https://cdn.shi1011.cn/2022/11/eb9c86feb62fb503b196677eabce6d61.png?imageMogr2/format/webp/interlace/0/quality/90|watermark/2/text/wqlNYXMwbg/font/bXN5aGJkLnR0Zg/fontsize/14/fill/IzMzMzMzMw/dissolve/80/gravity/southeast/dx/5/dy/5

简单的总结一下:

  • String以及HeapNumber总是存储在Constant pool中
  • Small integers和Oddball由字节码直接加载

Closure

关于闭包的构成,如下所示:

+-------------+
|   Closure   |-------+-------------------+--------------------+
+-------------+       |                   |                    |
                      ↓                   ↓                    ↓
               +-------------+  +--------------------+  +-----------------+
               |   Context   |  | SharedFunctionInfo |  | Feedback Vector |
               +-------------+  +--------------------+  +-----------------+
                                          |             | Invocation Count|
                                          |             +-----------------+
                                          |             | Optimized Code  |
                                          |             +-----------------+
                                          |             |    Binary Op    |
                                          |             +-----------------+
                                          |
                                          |             +-----------------+
                                          +-----------> |    Byte Code    |
                                                        +-----------------+
  • Closure:函数闭包包含 Context, SharedFunctionInfoFeedbackVector
  • Context:包含函数的自由变量,并提供对全局对象的访问

自由变量是既不是函数的局部变量也不是函数的参数变量,即它们在函数的作用域中,但在函数之外声明。

  • SharedFunctionInfo:存储函数的一般信息,如源位置和字节码
  • FeedbackVector: 存储编译器优化所需的信息

Variable Definition

let

我们首先在代码块内定义一个变量

out.gn/x64.release/d8 --print-bytecode -e "{let a=1;}"

有下列输出:

         0x202a00199c9e @    0 : 0d 01             LdaSmi [1]
         0x202a00199ca0 @    2 : c4                Star1
         0x202a00199ca1 @    3 : 0e                LdaUndefined
         0x202a00199ca2 @    4 : aa                Return

其伪代码表示为:

acc = 1;
r1 = acc;
acc = undefined;
return acc;

可以看到在代码块内,使用let定义变量,其值存储在寄存器中。

但上文提到,通用寄存器仅16个,当变量定义超过16个时,其字节码又如何表示呢?

out.gn/x64.release/d8 --print-bytecode -e "{let a=1,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q;}"

有下列输出:

         0x2aa500199c9e @    0 : 0d 01             LdaSmi [1]
         0x2aa500199ca0 @    2 : c4                Star1
         0x2aa500199ca1 @    3 : 0e                LdaUndefined
         0x2aa500199ca2 @    4 : c3                Star2
···
         0x2aa500199cbb @   29 : 0e                LdaUndefined
         0x2aa500199cbc @   30 : b6                Star15
         0x2aa500199cbd @   31 : 0e                LdaUndefined
         0x2aa500199cbe @   32 : 18 ea             Star r16
         0x2aa500199cc0 @   34 : 0e                LdaUndefined
         0x2aa500199cc1 @   35 : 18 e9             Star r17
         0x2aa500199cc3 @   37 : 0e                LdaUndefined
         0x2aa500199cc4 @   38 : aa                Return

可以看到,当变量数超出通用寄存器的数量时,Ignition转而申请一个临时寄存器r16,并使用Star指令将值放置在临时寄存器中。

如果我们在全局环境下let定义变量,其表现又与闭包不同。

out.gn/x64.release/d8 --print-bytecode -e "let a=1, b=2;"

输出有以下:

         0x39b300199cae @    0 : 0d 01             LdaSmi [1]
         0x39b300199cb0 @    2 : 25 02             StaCurrentContextSlot [2]
         0x39b300199cb2 @    4 : 0d 02             LdaSmi [2]
         0x39b300199cb4 @    6 : 25 03             StaCurrentContextSlot [3]
         0x39b300199cb6 @    8 : 0e                LdaUndefined
         0x39b300199cb7 @    9 : aa                Return

这里出现了一条新的指令StaCurrentContextSlot,意为将变量定义放置在当前上下文的作用域内。

var

首先来观察下列

out.gn/x64.release/d8 --print-bytecode -e "var a=2;"

得到以下:

[generated bytecode for function:  (0x06d600199c39 <SharedFunctionInfo>)]
Bytecode length: 18
Parameter count 1
Register count 3
Frame size 24
Bytecode age: 0
         0x6d600199cba @    0 : 13 00             LdaConstant [0]
         0x6d600199cbc @    2 : c4                Star1
         0x6d600199cbd @    3 : 19 fe f8          Mov <closure>, r2
         0x6d600199cc0 @    6 : 66 5e 01 f9 02    CallRuntime [DeclareGlobals], r1-r2
         0x6d600199cc5 @   11 : 0d 02             LdaSmi [2]
         0x6d600199cc7 @   13 : 23 01 00          StaGlobal [1], [0]
         0x6d600199cca @   16 : 0e                LdaUndefined
         0x6d600199ccb @   17 : aa                Return
Constant pool (size = 2)
0x6d600199c89: [FixedArray] in OldSpace
 - map: 0x06d600002229 <Map(FIXED_ARRAY_TYPE)>
 - length: 2
           0: 0x06d600199c7d <FixedArray[1]>
           1: 0x06d600004075 <String[1]: #a>
Handler Table (size = 0)
Source Position Table (size = 0)

其伪代码可以表示为:

acc = Constant[0];
r1 = acc;
r2 = <closure>;
DeclareGlobals(r1, r2);
acc = 2;
global[1] = acc;
acc = undefined;
return acc;
  • LdaConstant [0]:从Constant pool取出下标为0的常量放入累加器中
  • Star1:将累加器中的内容存放到r1寄存器中
  • Mov <closure>, r2:将<closure>的上下文内容放到r2寄存器中
  • CallRuntime [DeclareGlobals], r1-r2:调用Runtime函数DeclareGlobals,参数为r1r2寄存器
  • StaGlobal [1], [0]:将acc中的内容放到全局定义中,这里需要注意[0]代表反馈向量槽(FeedbackVector Slot)中相应的位置

当我们在代码块中使用var定义时,其字节码与上述无异。

out.gn/x64.release/d8 --print-bytecode -e "{var a=1}"

有以下输出:

[generated bytecode for function:  (0x269900199c39 <SharedFunctionInfo>)]
Bytecode length: 18
Parameter count 1
Register count 3
Frame size 24
Bytecode age: 0
         0x269900199cba @    0 : 13 00             LdaConstant [0]
         0x269900199cbc @    2 : c4                Star1
         0x269900199cbd @    3 : 19 fe f8          Mov <closure>, r2
         0x269900199cc0 @    6 : 66 5e 01 f9 02    CallRuntime [DeclareGlobals], r1-r2
         0x269900199cc5 @   11 : 0d 01             LdaSmi [1]
         0x269900199cc7 @   13 : 23 01 00          StaGlobal [1], [0]
         0x269900199cca @   16 : 0e                LdaUndefined
         0x269900199ccb @   17 : aa                Return
Constant pool (size = 2)
0x269900199c89: [FixedArray] in OldSpace
 - map: 0x269900002229 <Map(FIXED_ARRAY_TYPE)>
 - length: 2
           0: 0x269900199c7d <FixedArray[1]>
           1: 0x269900004075 <String[1]: #a>
Handler Table (size = 0)
Source Position Table (size = 0)

但当我们在闭包内定义,就有所差异了。

这里要注意v8采用了懒加载技术(lazy loading),简单的来讲当我们定义一个函数而不去调用它,那么这个函数并不会加载。

因此为达到我们的实验目的,我们需要主动调用它,使其正常加载:

out.gn/x64.release/d8 --print-bytecode -e "function test(){var a=1} test();"

省略外部调用字节码,函数内部字节码有以下:

···
[generated bytecode for function: test (0x15a600199c91 <SharedFunctionInfo test>)]
Bytecode length: 5
Parameter count 1
Register count 1
Frame size 8
Bytecode age: 0
         0x15a600199e12 @    0 : 0d 01             LdaSmi [1]
         0x15a600199e14 @    2 : c5                Star0
         0x15a600199e15 @    3 : 0e                LdaUndefined
         0x15a600199e16 @    4 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

可以发现在闭包内,var定义等同于let在闭包内的定义。

这恰恰证明了在ES6标准中新增的letvar的区别。

  • var声明限定于函数作用域和全局作用域
  • letvar的基础上添加了块级作用域声明

const

当然,const也是ES6标准中新增的内容,用于常量定义。和let一样,const也能声明在块级作用域。

在块级作用域中声明:

out.gn/x64.release/d8 --print-bytecode -e "{const a=1}"

有以下输出:

[generated bytecode for function:  (0x327f00199c39 <SharedFunctionInfo>)]
Bytecode length: 5
Parameter count 1
Register count 2
Frame size 16
Bytecode age: 0
         0x327f00199c9e @    0 : 0d 01             LdaSmi [1]
         0x327f00199ca0 @    2 : c4                Star1
         0x327f00199ca1 @    3 : 0e                LdaUndefined
         0x327f00199ca2 @    4 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

其字节码与let定义无异;在全局作用域中,与let仍旧无异,这里不再赘述。

既然字节码无异,继而产生了一个问题:如何区分常量与变量?

首先来观察变量:

out.gn/x64.release/d8 --print-bytecode -e "let a=1; a=2"

有以下输出:

[generated bytecode for function:  (0x355b00199c39 <SharedFunctionInfo>)]
Bytecode length: 10
Parameter count 1
Register count 1
Frame size 8
Bytecode age: 0
         0x355b00199ca6 @    0 : 0d 01             LdaSmi [1]
         0x355b00199ca8 @    2 : 25 02             StaCurrentContextSlot [2]
         0x355b00199caa @    4 : 0d 02             LdaSmi [2]
         0x355b00199cac @    6 : 25 02             StaCurrentContextSlot [2]
         0x355b00199cae @    8 : c5                Star0
         0x355b00199caf @    9 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

再来观察常量:

out.gn/x64.release/d8 --print-bytecode -e "const a=1; a=2"

有以下输出:

[generated bytecode for function:  (0x08ae00199c39 <SharedFunctionInfo>)]
Bytecode length: 13
Parameter count 1
Register count 1
Frame size 8
Bytecode age: 0
         0x8ae00199ca6 @    0 : 0d 01             LdaSmi [1]
         0x8ae00199ca8 @    2 : 25 02             StaCurrentContextSlot [2]
         0x8ae00199caa @    4 : 0d 02             LdaSmi [2]
         0x8ae00199cac @    6 : 66 70 01 fa 00    CallRuntime [ThrowConstAssignError], r0-r0
         0x8ae00199cb1 @   11 : c5                Star0
         0x8ae00199cb2 @   12 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

可以发现为常量再次赋值时,Ignition将会调用运行时方法ThrowConstAssignError抛出错误。

显而易见,在Js代码转换为字节码的过程中,其对常量便做了预处理。

Arithmetic Instruction

Unary Operators

指令实例
Inca++
Deca--
Negate-a
BitwiseNot~a
ToBooleanLogicalNot!a
LogicalNot!a
TypeOftypeof a
DeletePropertyStrictdelete a[0](严格模式)
DeletePropertySloppydelete a[0]

Binary Operators

指令实例
Adda + b
Suba - b
Mula * b
Diva / b
Moda % b
BitwiseOra | b
BitwiseXora ^ b
BitwiseAnda & b
ShiftLefta << b
ShiftRighta >> b
ShiftRightLogicala >>> b

More

  • Branch Jump Instruction
    • Effectful Test Operators
    • Control Flow
  • Creation and Invocation of Closures

······

更多关于字节码可以参考https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h,其定义给出了较为详细的字节码operand以及operandType

https://cdn.shi1011.cn/2022/11/ca4310a4fc3aeaf9d32bcd55def50ba4.png?imageMogr2/format/webp/interlace/0/quality/90|watermark/2/text/wqlNYXMwbg/font/bXN5aGJkLnR0Zg/fontsize/14/fill/IzMzMzMzMw/dissolve/80/gravity/southeast/dx/5/dy/5

Simple v8 Bytecode Interpreter

这是一个简易的v8字节码的执行器(非反序列化),有助于我们理解字节码简要的执行流程。

References

本文链接:https://blog.shi1011.cn/learn/2412
本文采用 CC BY-NC-SA 4.0 Unported 协议进行许可

Mas0n

文章作者

发表回复

textsms
account_circle
email

翻车鱼

Ignition Bytecode for V8 Interpreter
简要学习V8 Bytecode的组成以及执行方式。 纵览各个JavaScript引擎的实现,我们发现基于字节码的实现是主流。 苹果公司的JavaScriptCore(JSC) 引擎实现了一个寄存器机(Register …
扫描二维码继续阅读
2022-11-27