JavaScript的字节码 - v8 Ignition指令

Posted Jtag特工

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript的字节码 - v8 Ignition指令相关的知识,希望对你有一定的参考价值。

javascript的字节码 - v8 Ignition指令

前面的文章我们介绍了在js的AST层次的各种操作手段。AST操练熟练了之后,就差一步就可以执行了,那就是转换成中间代码,或者是解释型的字节码,或者是为编译器准备的IR.

我们以v8为例,首先看下v8的运行架构:

这个图中有三个不熟悉的专有名词,ignition, crankshaft和Turbofan。

其中,Ignition是v8的解释器,crankshaft是老一代的编译器,turbofan是较新一代的编译器。所以我们所说的字节码就对应于igintion的字节码,编译的中间代码就是TurboFan IR.
本文我们集中讲解Ignition字节码。

通过v8的d8工具,我们可以方便地看到ignition bytecode的序列,只要加–print-bytecode参数:

./d8 --print-bytecode

如果你手头没有d8工具的话,可以使用你手头的node:

node --print-bytecode

下面我们就可以开始愉快的ignition字节码之旅啦。

累加器加载指令

我们先从最小的语句开始。

undefined;

这个真足够小了吧。我们来看其对应的三条字节码指令:

         0x63e081d407a @    0 : 0e                LdaUndefined 
         0x63e081d407b @    1 : c4                Star0 
         0x63e081d407c @    2 : a9                Return 

做为一条语句,是有其返回值的。这条语句自然就是返回undefined.

Ld*属于加载到累加器的指令集。
Star是寄存器操作指令。
Return是返回指令。

当我们再次输入undefined;的时候,因为已经转换过字节码了,则不会再次做转换,直接运行之前生成好的字节码。

同样,针对true,有LdaTrue字节码。

         0x63e081d42d6 @    0 : 11                LdaTrue 
         0x63e081d42d7 @    1 : c4                Star0 
         0x63e081d42d8 @    2 : a9                Return 

null对应LdaNull:

         0x63e081d43e6 @    0 : 0f                LdaNull 
         0x63e081d43e7 @    1 : c4                Star0 
         0x63e081d43e8 @    2 : a9                Return 

对于0,有一条专门的LdaZero指令:

         0x63e081d4772 @    0 : 0c                LdaZero 
         0x63e081d4773 @    1 : c4                Star0 
         0x63e081d4774 @    2 : a9                Return 

如果是1呢,会有一个LoadSmi指令:

         0x63e081d4856 @    0 : 0d 01             LdaSmi [1]
         0x63e081d4858 @    2 : c4                Star0 
         0x63e081d4859 @    3 : a9                Return

LoadSmi是个两字节的指令,指令代码0d之后接一个字节的立即数。
我们再看下-1的情况:

         0x63e081d493a @    0 : 0d ff             LdaSmi [-1]
         0x63e081d493c @    2 : c4                Star0 
         0x63e081d493d @    3 : a9                Return 

如果要加载两个字节的立即数,LdaSmi变成LdaSmi.Wide指令:

         0x2c11081d556a @    0 : 00 0d 10 27       LdaSmi.Wide [10000]

针对4个字节的情况,还有Lda.ExtraWide指令。
比如:

let n1 = 100_000_000;

对应的指令是

         0x2c11081d5456 @    0 : 01 0d 00 e1 f5 05 LdaSmi.ExtraWide [100000000]

如果是1.1的话会如何呢?这时候指令里放不下了,1.1这个数存放在堆上,LdaConstant指令根据后面一个字节的索引来从堆上读取这个值。

Bytecode Age: 0
         0x63e081d4a46 @    0 : 13 00             LdaConstant [0]
         0x63e081d4a48 @    2 : c4                Star0 
         0x63e081d4a49 @    3 : a9                Return 
Constant pool (size = 1)
0x63e081d4a0d: [FixedArray] in OldSpace
 - map: 0x063e08002209 <Map>
 - length: 1
           0: 0x063e081d4a19 <HeapNumber 1.1>

如果是1+1呢?

         0x63e081d4c1a @    0 : 0d 02             LdaSmi [2]
         0x63e081d4c1c @    2 : c4                Star0 
         0x63e081d4c1d @    3 : a9                Return 

生成字节码的时候,解释器已经就进行了立即数的计算,不会再浪费指令了。

有一个有趣的事情大家应该知道,就是0.0和-0.0的问题。
对于0.0,解释器就当0处理:

         0x63e081d4e0a @    0 : 0c                LdaZero 
         0x63e081d4e0b @    1 : c4                Star0 
         0x63e081d4e0c @    2 : a9                Return 

而-0.0就跟浮点常数一个待遇了,虽然堆上存放的是0.0而非-0.0:

         0x63e081d4d26 @    0 : 13 00             LdaConstant [0]
         0x63e081d4d28 @    2 : c4                Star0 
         0x63e081d4d29 @    3 : a9                Return 
Constant pool (size = 1)
0x63e081d4ced: [FixedArray] in OldSpace
 - map: 0x063e08002209 <Map>
 - length: 1
           0: 0x063e081d4cf9 <HeapNumber 0.0>

算术指令

自增和自减指令

我们现在开始引入变量吧。
如果是局部作用域,这跟我们之前只使用立即数是差不多的。
比如:

let a2=1;

转换成字节码如下:

         0x63e081d500e @    0 : 0d 01             LdaSmi [1]
         0x63e081d5010 @    2 : c3                Star1 
         0x63e081d5011 @    3 : 0e                LdaUndefined 
         0x63e081d5012 @    4 : a9                Return 

如果是在全局定义变量:

let a1 = 0;

则会引入一条新指令StaCurrentContextSlot:

         0x63e081d4f06 @    0 : 0c                LdaZero 
         0x63e081d4f07 @    1 : 25 02             StaCurrentContextSlot [2]
         0x63e081d4f09 @    3 : 0e                LdaUndefined 
         0x63e081d4f0a @    4 : a9                Return

好,下面我们开始进行一些算术运算。
首先我们针对自增运算符

let a3 = 1; a3 ++;

对应的指令是Inc:

         0x63e081d53ca @    0 : 0d 01             LdaSmi [1]
         0x63e081d53cc @    2 : c3                Star1 
         0x63e081d53cd @    3 : 75 00             ToNumeric [0]
         0x63e081d53cf @    5 : c2                Star2 
         0x63e081d53d0 @    6 : 51 00             Inc [0]
         0x63e081d53d2 @    8 : c3                Star1 
         0x63e081d53d3 @    9 : 19 f8 fa          Mov r2, r0
         0x63e081d53d6 @   12 : 0b fa             Ldar r0
         0x63e081d53d8 @   14 : a9                Return 

值得注意的是,在进行算术运算之前,需要使用ToNumberic指令将当前值转换成数值类型。

对于自减运算符

let a4 = 100; a4--;

也只是换成了Dec指令:

         0x63e081d54da @    0 : 0d 64             LdaSmi [100]
         0x63e081d54dc @    2 : c3                Star1 
         0x63e081d54dd @    3 : 75 00             ToNumeric [0]
         0x63e081d54df @    5 : c2                Star2 
         0x63e081d54e0 @    6 : 52 00             Dec [0]
         0x63e081d54e2 @    8 : c3                Star1 
         0x63e081d54e3 @    9 : 19 f8 fa          Mov r2, r0
         0x63e081d54e6 @   12 : 0b fa             Ldar r0
         0x63e081d54e8 @   14 : a9                Return 

二元算术运算符

我们先看一个加法的:

let a5 = 2; a5 = a5 + 2;

翻译成AddSmi指令:

         0x63e081d5712 @    0 : 0d 02             LdaSmi [2]
         0x63e081d5714 @    2 : c3                Star1 
         0x63e081d5715 @    3 : 45 02 00          AddSmi [2], [0]
         0x63e081d5718 @    6 : c3                Star1 
         0x63e081d5719 @    7 : c4                Star0 
         0x63e081d571a @    8 : a9                Return 

如果不是针对立即数,而是两个变量操作:

let a6 = 1; let a7=2; let a8 = a6 + a7;

则会换成Add指令:

         0x63e081d583a @    0 : 0d 01             LdaSmi [1]
         0x63e081d583c @    2 : c3                Star1 
         0x63e081d583d @    3 : 0d 02             LdaSmi [2]
         0x63e081d583f @    5 : c2                Star2 
         0x63e081d5840 @    6 : 0b f8             Ldar r2
         0x63e081d5842 @    8 : 39 f9 00          Add r1, [0]
         0x63e081d5845 @   11 : c1                Star3 
         0x63e081d5846 @   12 : 0e                LdaUndefined 
         0x63e081d5847 @   13 : a9                Return 

常量与变量

如果我们针对常量进行操作,会生成什么样的指令呢:

const a11 = 0; a11 +=1;

我们保存常量的时候,还是用的StaCurrentContextSlot指令,但是加载时用的是LdaImmutableCurrentContextSlot, 针对AddSmi指令,会调用CallRuntime去调用运行时的ThrowConstAssignError异常。

         0x63e081d5b7a @    0 : 0c                LdaZero 
         0x63e081d5b7b @    1 : 25 02             StaCurrentContextSlot [2]
         0x63e081d5b7d @    3 : 17 02             LdaImmutableCurrentContextSlot [2]
         0x63e081d5b7f @    5 : 45 01 00          AddSmi [1], [0]
         0x63e081d5b82 @    8 : 65 66 01 fa 00    CallRuntime [ThrowConstAssignError], r0-r0
         0x63e081d5b87 @   13 : c4                Star0 
         0x63e081d5b88 @   14 : a9                Return 

而我们在顶层写一个var声明的变量的时候,会被转化为全局变量:

var a13 = 0;

这其中仍然有运行时的DeclareGlobals的参与,最后通过StaGlobal保存为全局变量:

         0x63e081d5cda @    0 : 13 00             LdaConstant [0]
         0x63e081d5cdc @    2 : c3                Star1 
         0x63e081d5cdd @    3 : 19 fe f8          Mov <closure>, r2
         0x63e081d5ce0 @    6 : 65 54 01 f9 02    CallRuntime [DeclareGlobals], r1-r2
         0x63e081d5ce5 @   11 : 0c                LdaZero 
         0x63e081d5ce6 @   12 : 23 01 00          StaGlobal [1], [0]
         0x63e081d5ce9 @   15 : 0e                LdaUndefined 
         0x63e081d5cea @   16 : a9                Return 

字面量

Ignition指令集提供三种字面量创建的指令:

  • CreateArrayLiteral
  • CreateObjectLateral
  • CreateRegExpLiteral

这个容易理解,针对的是[], , // 三种值。

我们看几个例子.

  1. 空数组:
let a1 = [];

作为常用操作,Ignition为其提供了一个专门指令:CreateEmptyArrayLiteral

         0x24b2081d3386 @    0 : 7b 00             CreateEmptyArrayLiteral [0]
         0x24b2081d3388 @    2 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d338a @    4 : 0e                LdaUndefined 
         0x24b2081d338b @    5 : a9                Return 
  1. 空对象
let a2=;

Ignition为其提供了一个专门指令:CreateEmptyObjectLiteral

         0x24b2081d411e @    0 : 7d                CreateEmptyObjectLiteral 
         0x24b2081d411f @    1 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d4121 @    3 : 0e                LdaUndefined 
         0x24b2081d4122 @    4 : a9                Return 
  1. 正则表达式
let a3 = /a*/;

正则搞个空的就没意思了,我们搞个有值的。可以看到,字面量最终转换成堆上的字符串。

Bytecode Age: 0
         0x24b2081d42e2 @    0 : 78 00 00 00       CreateRegExpLiteral [0], [0], #0
         0x24b2081d42e6 @    4 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d42e8 @    6 : 0e                LdaUndefined 
         0x24b2081d42e9 @    7 : a9                Return 
Constant pool (size = 1)
0x24b2081d42b5: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 1
           0: 0x24b2081d424d <String[2]: #a*>

属性的访问

Igintion提供LdaNamedProperty和StaNamedProperty指令来按名字访问属性。

我们来看个例子:

let a4 = ; a4.a = 1;

生成的指令如下:

         0x24b2081d4412 @    0 : 7d                CreateEmptyObjectLiteral 
         0x24b2081d4413 @    1 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d4415 @    3 : 16 02             LdaCurrentContextSlot [2]
         0x24b2081d4417 @    5 : c3                Star1 
         0x24b2081d4418 @    6 : 0d 01             LdaSmi [1]
         0x24b2081d441a @    8 : c2                Star2 
         0x24b2081d441b @    9 : 32 f9 00 00       StaNamedProperty r1, [0], [0]
         0x24b2081d441f @   13 : 19 f8 fa          Mov r2, r0
         0x24b2081d4422 @   16 : 0b fa             Ldar r0
         0x24b2081d4424 @   18 : a9                Return 
Constant pool (size = 1)
0x24b2081d43e5: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 1
           0: 0x24b208008265 <String[1]: #a>

注意a4没出场是因为现在用的是Current Context Slot.

再读出来:

a4['a'];

这时候a4要作为参数传给LdaNamedProperty.

Bytecode Age: 0
         0x24b2081d466a @    0 : 21 00 00          LdaGlobal [0], [0]
         0x24b2081d466d @    3 : c3                Star1 
         0x24b2081d466e @    4 : 2d f9 01 02       LdaNamedProperty r1, [1], [2]
         0x24b2081d4672 @    8 : c4                Star0 
         0x24b2081d4673 @    9 : a9                Return 
Constant pool (size = 2)
0x24b2081d4639: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 2
           0: 0x24b2081d437d <String[2]: #a4>
           1: 0x24b208008265 <String[1]: #a>

闭包的创建与调用

函数通过CreateClosure指令来创建。

我们来个箭头函数的例子:

let f1 = () => 0;

生成的指令如下:

Bytecode Age: 0
         0x24b2081d481a @    0 : 80 00 00 00       CreateClosure [0], [0], #0
         0x24b2081d481e @    4 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d4820 @    6 : 0e                LdaUndefined 
         0x24b2081d4821 @    7 : a9                Return 
Constant pool (size = 1)
0x24b2081d47ed: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 1
           0: 0x24b2081d47b9 <SharedFunctionInfo f1>

普通的函数也是CreateClosure:

let f2 = function(x)  return x * x;

字节码跟上面的一模一样:

Bytecode Age: 0
         0x24b2081d4996 @    0 : 80 00 00 00       CreateClosure [0], [0], #0
         0x24b2081d499a @    4 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d499c @    6 : 0e                LdaUndefined 
         0x24b2081d499d @    7 : a9                Return 
Constant pool (size = 1)
0x24b2081d4969: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 1
           0: 0x24b2081d4935 <SharedFunctionInfo f2>

函数的代码呢?在调用时才能见到。调用的指令是CallUndefinedReceiver*,没有参数的就是CallUndefinedReceiver0.

f1();

f1的字节码在调用的后面会单独输出。

[generated bytecode for function:  (0x24b2081d4a51 <SharedFunctionInfo>)]
...
Bytecode Age: 0
         0x24b2081d4ac2 @    0 : 21 00 00          LdaGlobal [0], [0]
         0x24b2081d4ac5 @    3 : c3                Star1 
         0x24b2081d4ac6 @    4 : 61 f9 02          CallUndefinedReceiver0 r1, [2]
         0x24b2081d4ac9 @    7 : c4                Star0 
         0x24b2081d4aca @    8 : a9                Return 
...
[generated bytecode for function: f1 (0x24b2081d47b9 <SharedFunctionInfo f1>)]
...
Bytecode Age: 0
         0x24b2081d4b52 @    0 : 0c                LdaZero 
         0x24b2081d4b53 @    1 : a9                Return 

我们再看f2的:

f2(1.1);

因为f2有一个参数,所以调用的指令是CallUndefinedReceiver1:

[generated bytecode for function:  (0x24b2081d4c49 <SharedFunctionInfo>)]
...
Bytecode Age: 0
         0x24b2081d4cca @    0 : 21 00 00          LdaGlobal [0], [0]
         0x24b2081d4ccd @    3 : c3                Star1 
         0x24b2081d4cce @    4 : 13 01             LdaConstant [1]
         0x24b2081d4cd0 @    6 : c2                Star2 
         0x24b2081d4cd1 @    7 : 62 f9 f8 02       CallUndefinedReceiver1 r1, r2, [2]
         0x24b2081d4cd5 @   11 : c4                Star0 
         0x24b2081d4cd6 @   12 : a9                Return 
...
[generated bytecode for function: f2 (0x24b2081d4935 <SharedFunctionInfo f2>)]
...
Bytecode Age: 0
         0x24b2081d4d5e @    0 : 0b 03             Ldar a0
         0x24b2081d4d60 @    2 : 3b 03 00          Mul a0, [0]
         0x24b2081d4d63 @    5 : a9                Return 

分支跳转指令

分支跳转指令分为两部分,一部分是Test类设置标志位,一部分是根据标志位跳转。

我们来看个例子:

let f3 = (x) => if (x>0) return 0; else return -1;; f3(0);

我们只摘取函数体这部分,由TestGreaterThan和JumpIfFalse两条指令构成。JumpIfFalse是如果为真继续执行,为假则跳转。

         0x24b2081d561a @    0 : 0c                LdaZero 
         0x24b2081d561b @    1 : 6e 03 00          TestGreaterThan a0, [0]
         0x24b2081d561e @    4 : 99 04             JumpIfFalse [4] (0x24b2081d5622 @ 8)
         0x24b2081d5620 @    6 : 0c                LdaZero 
         0x24b2081d5621 @    7 : a9                Return 
         0x24b2081d5622 @    8 : 0d ff             LdaSmi [-1]
         0x24b2081d5624 @   10 : a9                Return 

除了if语句之外,像"?."等运算符也会产生分支判断。
我们看例子:

let f5 = (x) => return x?.length;; f5(null);

判空运算符对应的是JumpIfUndefinedOrNull指令。

Bytecode Age: 0
         0x24b2081d6766 @    0 : 0b 03             Ldar a0
         0x24b2081d6768 @    2 : 19 03 fa          Mov a0, r0
         0x24b2081d676b @    5 : 9e 08             JumpIfUndefinedOrNull [8] (0x24b2081d6773 @ 13)
         0x24b2081d676d @    7 : 2d fa 00 00       LdaNamedProperty r0, [0], [0]
         0x24b2081d6771 @   11 : 8a 03             Jump [3] (0x24b2081d6774 @ 14)
         0x24b2081d6773 @   13 : 0e                LdaUndefined 
         0x24b2081d6774 @   14 : a9                Return 
Constant pool (size = 1)
0x24b2081d6739: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 1
           0: 0x24b208004cb1 <String[6]: #length>

我们自己写的判空反而没法生成这一条指令:

let f6 = (x) => return x===null || x===undefined; f6(0);

生成的是直译成TestNull和TestUndefined指令:

         0x24b2081d695a @    0 : 0b 03             Ldar a0
         0x24b2081d695c @    2 : 1e                TestNull 
         0x24b2081d695d @    3 : 98 05             JumpIfTrue [5] (0x24b2081d6962 @ 8)
         0x24b2081d695f @    5 : 0b 03             Ldar a0
         0x24b2081d6961 @    7 : 1f                TestUndefined 
         0x24b2081d6962 @    8 : a9                Return 

同样,nullish运算符也是个分支语句:

let a2 = a1 ?? 100;

还是个JumpIfUndefinedOrNull的实现,看起来是同一个作者:

         0x2c11081d409e @    0 : 21 00 00          LdaGlobal [0], [0]
         0x2c11081d40a1 @    3 : 9e 04             JumpIfUndefinedOrNull [4] (0x2c11081d40a5 @ 7)
         0x2c11081d40a3 @    5 : 8a 04             Jump [4] (0x2c11081d40a7 @ 9)
         0x2c11081d40a5 @    7 : 0d 64             LdaSmi [100]
         0x2c11081d40a7 @    9 : 25 02             StaCurrentContextSlot [2]

处理异常

异常是另外一种意义上的分支指令,但是涉及到异常上下文。

try a4.a = 1; catch(e) ; 

CreateCatchContext提供创建异常上下文,然后通过PushContext和PopContext去切换上下文。

Bytecode Age: 0
         0x24b2081d60c2 @    0 : 0e                LdaUndefined 
         0x24b2081d60c3 @    1 : c4                Star0 
         0x24b2081d60c4 @    2 : 19 ff f9          Mov <context>, r1
         0x24b2081d60c7 @    5 : 21 00 00          LdaGlobal [0], [0]
         0x24b2081d60ca @    8 : c2                Star2 
         0x24b2081d60cb @    9 : 0d 01             LdaSmi [1]
         0x24b2081d60cd @   11 : c1                Star3 
         0x24b2081d60ce @   12 : 32 f8 01 02       StaNamedProperty r2, [1], [2]
         0x24b2081d60d2 @   16 : 19 f7 fa          Mov r3, r0
         0x24b2081d60d5 @   19 : 0b f7             Ldar r3
         0x24b2081d60d7 @   21 : 8a 0f             Jump [15] (0x24b2081d60e6 @ 36)
         0x24b2081d60d9 @   23 : c2                Star2 
         0x24b2081d60da @   24 : 82 f8 02          CreateCatchContext r2, [2]
         0x24b2081d60dd @   27 : c3                Star1 
         0x24b2081d60de @   28 : 10                LdaTheHole 
         0x24b2081d60df @   29 : a6                SetPendingMessage 
         0x24b2081d60e0 @   30 : 0b f9             Ldar r1
         0x24b2081d60e2 @   32 : 1a f8             PushContext r2
         0x24b2081d60e4 @   34 : 1b f8             PopContext r2
         0x24b2081d60e6 @   36 : 0b fa             Ldar r0
         0x24b2081d60e8 @   38 : a9                Return 
Constant pool (size = 3)
0x24b2081d608d: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 3
           0: 0x24b2081d437d <String[2]: #a4>
           1: 0x24b208008265 <String[1]: #a>
           2: 0x24b2081d603d <ScopeInfo CATCH_SCOPE>
Handler Table (size = 16)
   from   to       hdlr (prediction,   data)
  (   5,  19)  ->    23 (prediction=1, data=1)

这其中还让我们第一次见到LdaTheHole这样涉及到内部机制的指令。按照v8团队某同学的说法,这个洞是一个哨兵,供系统在此做一些检查的工作,有几种可能,要区分出来具体是哪种情况。后面的文章讲源码的时候再仔细分析。

如果我们使用ES2019中新增的可选catch功能的话,则不会生成CreateCatchContext.
生成的代码如下:

Bytecode Age: 0
         0x2c11081d5b3a @    0 : 0e                LdaUndefined 
         0x2c11081d5b3b @    1 : c4                Star0 
         0x2c11081d5b3c @    2 : 19 ff f9          Mov <context>, r1
         0x2c11081d5b3f @    5 : 21 00 00          LdaGlobal [0], [0]
         0x2c11081d5b42 @    8 : c2                Star2 
         0x2c11081d5b43 @    9 : 0d 01             LdaSmi [1]
         0x2c11081d5b45 @   11 : c1                Star3 
         0x2c11081d5b46 @   12 : 32 f8 01 02       StaNamedProperty r2, [1], [2]
         0x2c11081d5b4a @   16 : 19 f7 fa          Mov r3, r0
         0x2c11081d5b4d @   19 : 0b f7             Ldar r3
         0x2c11081d5b4f @   21 : 8a 06             Jump [6] (0x2c11081d5b55 @ 27)
         0x2c11081d5b51 @   23 : 10                LdaTheHole 
         0x2c11081d5b52 @   24 : a6                SetPendingMessage 
         0x2c11081d5b53 @   25 : 0b f9             Ldar r1
         0x2c11081d5b55 @   27 : 0b fa             Ldar r0
         0x2c11081d5b57 @   29 : a9                Return 
Constant pool (size = 2)
0x2c11081d5b09: [FixedArray] in OldSpace
 - map: 0x2c1108002209 <Map>
 - length: 2
           0: 0x2c11081d413d <String[2]: #a4>
           1: 0x2c1108008265 <String[1]: #a>
Handler Table (size = 16)
   from   to       hdlr (prediction,   data)
  (   5,  19)  ->    23 (prediction=1, data=1)

循环指令

我们先看传统的类C语言的for循环:

let f4 = (x) => for(let i=0;i<x;i++) console.log(i);; f4(10);

它的实现方式跟分支语句基本类似,就是多了一条JumpLoop指令。

         0x24b2081d582a @    0 : 0c                LdaZero 
         0x24b2081d582b @    1 : c4                Star0 
         0x24b2081d582c @    2 : 0b 03             Ldar a0
         0x24b2081d582e @    4 : 6d fa 00          TestLessThan r0, [0]
         0x24b2081d5831 @    7 : 99 18             JumpIfFalse [24] (0x24b2081d5849 @ 31)
         0x24b2081d5833 @    9 : 21 00 01          LdaGlobal [0], [1]
         0x24b2081d5836 @   12 : c2                Star2 
         0x24b2081d5837 @   13 : 2d f8 01 03       LdaNamedProperty r2, [1], [3]
         0x24b2081d583b @   17 : c3                Star1 
         0x24b2081d583c @   18 : 5e f9 f8 fa 05    CallProperty1 r1, r2, r0, [5]
         0x24b2081d5841 @   23 : 0b fa             Ldar r0
         0x24b2081d5843 @   25 : 51 07             Inc [7]
         0x24b2081d5845 @   27 : c4                Star0 
         0x24b2081d5846 @   28 : 89 1a 00          JumpLoop [26], [0] (0x24b2081d582c @ 2)

下面,我们观赏一下一个针对数组进行迭代背后所做的事情:

let a21 = [1,2,3,4,5];
let sum=0;
for(let i of a21) sum+=i;

针对迭代器模式,Ignition提供了GetIterator指令,然后就是各种异常的处理:

         0x24b2081d5dc2 @    0 : 0e                LdaUndefined 
         0x24b2081d5dc3 @    1 : c3                Star1 
         0x24b2081d5dc4 @    2 : 21 00 00          LdaGlobal [0], [0]
         0x24b2081d5dc7 @    5 : be                Star6 
         0x24b2081d5dc8 @    6 : b1 f4 02 04       GetIterator r6, [2], [4]
         0x24b2081d5dcc @   10 : 9f 07             JumpIfJSReceiver [7] (0x24b2081d5dd3 @ 17)
         0x24b2081d5dce @   12 : 65 c3 00 fa 00    CallRuntime [ThrowSymbolIteratorInvalid], r0-r0
         0x24b2081d5dd3 @   17 : bf                Star5 
         0x24b2081d5dd4 @   18 : 2d f5 01 06       LdaNamedProperty r5, [1], [6]
         0x24b2081d5dd8 @   22 : c0                Star4 
         0x24b2081d5dd9 @   23 : 12                LdaFalse 
         0x24b2081d5dda @   24 : be                Star6 
         0x24b2081d5ddb @   25 : 19 ff f1          Mov <context>, r9
         0x24b2081d5dde @   28 : 11                LdaTrue 
         0x24b2081d5ddf @   29 : be                Star6 
         0x24b2081d5de0 @   30 : 5d f6 f5 08       CallProperty0 r4, r5, [8]
         0x24b2081d5de4 @   34 : ba                Star10 
         0x24b2081d5de5 @   35 : 9f 07             JumpIfJSReceiver [7] (0x24b2081d5dec @ 42)
         0x24b2081d5de7 @   37 : 65 bb 00 f0 01    CallRuntime [ThrowIteratorResultNotAnObject], r10-r10
         0x24b2081d5dec @   42 : 2d f0 02 0a       LdaNamedProperty r10, [2], [10]
         0x24b2081d5df0 @   46 : 96 27             JumpIfToBooleanTrue [39] (0x24b2081d5e17 @ 85)
         0x24b2081d5df2 @   48 : 2d f0 03 0c       LdaNamedProperty r10, [3], [12]
         0x24b2081d5df6 @   52 : ba                Star10 
         0x24b2081d5df7 @   53 : 12                LdaFalse 
         0x24b2081d5df8 @   54 : be                Star6 
         0x24b2081d5df9 @   55 : 19 f0 fa          Mov r10, r0
         0x24b2081d5dfc @   58 : 19 fa f7          Mov r0, r3
         0x24b2081d5dff @   61 : 21 04 0e          LdaGlobal [4], [14]
         0x24b2081d5e02 @   64 : b9                Star11 
         0x24b2081d5e03 @   65 : 0b fa             Ldar r0
         0x24b2081d5e05 @   67 : 39 ef 10          Add r11, [16]
         0x24b2081d5e08 @   70 : b8                Star12 
         0x24b2081d5e09 @   71 : 23 04 11          StaGlobal [4], [17]
         0x24b2081d5e0c @   74 : 19 ee f9          Mov r12, r1
         0x24b2081d5e0f @   77 : 19 f7 f0          Mov r3, r10
         0x24b2081d5e12 @   80 : 0b f9             Ldar r1
         0x24b2081d5e14 @   82 : 89 36 00          JumpLoop [54], [0] (0x24b2081d5dde @ 28)
         0x24b2081d5e17 @   85 : 0d ff             LdaSmi [-1]
         0x24b2081d5e19 @   87 : bc                Star8 
         0x24b2081d5e1a @   88 : bd                Star7 
         0x24b2081d5e1b @   89 : 8a 05             Jump [5] (0x24b2081d5e20 @ 94)
         0x24b2081d5e1d @   91 : bc                Star8 
         0x24b2081d5e1e @   92 : 0c                LdaZero 
         0x24b2081d5e1f @   93 : bd                Star7 
         0x24b2081d5e20 @   94 : 10                LdaTheHole 
         0x24b2081d5e21 @   95 : a6                SetPendingMessage 
         0x24b2081d5e22 @   96 : bb                Star9 
         0x24b2081d5e23 @   97 : 0b f4             Ldar r6
         0x24b2081d5e25 @   99 : 96 23             JumpIfToBooleanTrue [35] (0x24b2081d5e48 @ 134)
         0x24b2081d5e27 @  101 : 19 ff ef          Mov <context>, r11
         0x24b2081d5e2a @  104 : 2d f5 05 13       LdaNamedProperty r5, [5], [19]
         0x24b2081d5e2e @  108 : 9e 1a             JumpIfUndefinedOrNull [26] (0x24b2081d5e48 @ 134)
         0x24b2081d5e30 @  110 : b8                Star12 
         0x24b2081d5e31 @  111 : 5d ee f5 15       CallProperty0 r12, r5, [21]
         0x24b2081d5e35 @  115 : 9f 13             JumpIfJSReceiver [19] (0x24b2081d5e48 @ 134)
         0x24b2081d5e37 @  117 : b7                Star13 
         0x24b2081d5e38 @  118 : 65 bb 00 ed 01    CallRuntime [ThrowIteratorResultNotAnObject], r13-r13
         0x24b2081d5e3d @  123 : 8a 0b             Jump [11] (0x24b2081d5e48 @ 134)
         0x24b2081d5e3f @  125 : b9                Star11 
         0x24b2081d5e40 @  126 : 0c                LdaZero 
         0x24b2081d5e41 @  127 : 1c f3             TestReferenceEqual r7
         0x24b2081d5e43 @  129 : 98 05             JumpIfTrue [5] (0x24b2081d5e48 @ 134)
         0x24b2081d5e45 @  131 : 0b ef             Ldar r11
         0x24b2081d5e47 @  133 : a8                ReThrow 
         0x24b2081d5e48 @  134 : 0b f1             Ldar r9
         0x24b2081d5e4a @  136 : a6                SetPendingMessage 
         0x24b2081d5e4b @  137 : 0c                LdaZero 
         0x24b2081d5e4c @  138 : 1c f3             TestReferenceEqual r7
         0x24b2081d5e4e @  140 : 99 05             JumpIfFalse [5] (0x24b2081d5e53 @ 145)
         0x24b2081d5e50 @  142 : 0b f2             Ldar r8
         0x24b2081d5e52 @  144 : a8                ReThrow 
         0x24b2081d5e53 @  145 : 0b f9             Ldar r1
         0x24b2081d5e55 @  147 : a9                Return 

这里面的细节我们以后再说,大家先有个感性认识。

我们先定义个空类,看看发生什么:

class A ;

从生成了CreateClosure指令可以看到,系统默认还是帮我们生成了一个构造函数。

Bytecode Age: 0
         0x24b2081d6b32 @    0 : 81 00             CreateBlockContext [0]
         0x24b2081d6b34 @    2 : 1a f9             PushContext r1
         0x24b2081d6b36 @    4 : 10                LdaTheHole 
         0x24b2081d6b37 @    5 : bf                Star5 
         0x24b2081d6b38 @    6 : 80 02 00 00       CreateClosure [2], [0], #0
         0x24b2081d6b3c @   10 : c2                Star2 
         0x24b2081d6b3d @   11 : 13 01             LdaConstant [1]
         0x24b2081d6b3f @   13 : c1                Star3 
         0x24b2081d6b40 @   14 : 19 f8 f6          Mov r2, r4
         0x24b2081d6b43 @   17 : 65 25 00 f7 03    CallRuntime [DefineClass], r3-r5
         0x24b2081d6b48 @   22 : c1                Star3 
         0x24b2081d6b49 @   23 : 1b f9             PopContext r1
         0x24b2081d6b4b @   25 : 0b f6             Ldar r4
         0x24b2081d6b4d @   27 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d6b4f @   29 : 0e                LdaUndefined 
         0x24b2081d6b50 @   30 : a9                Return 
Constant pool (size = 3)
0x24b2081d6afd: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 3
           0: 0x24b2081d6a11 <ScopeInfo CLASS_SCOPE>
           1: 0x24b2081d6ad9 <FixedArray[7]>
           2: 0x24b2081d6a25 <SharedFunctionInfo A>

我们再new一个对象看看:

let a100 = new A();

可以看到,new运算符被翻译成了Construct指令:

Bytecode Age: 0
         0x24b2081d6ee2 @    0 : 21 00 00          LdaGlobal [0], [0]
         0x24b2081d6ee5 @    3 : c3                Star1 
         0x24b2081d6ee6 @    4 : 69 f9 fa 00 02    Construct r1, r0-r0, [2]
         0x24b2081d6eeb @    9 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d6eed @   11 : 0e                LdaUndefined 
         0x24b2081d6eee @   12 : a9                Return 
Constant pool (size = 1)
0x24b2081d6eb5: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 1
           0: 0x24b2081d69a5 <String[1]: #A>

一些新特性的实现

BigInt

前面介绍过LdaSmi最大接受4个字节的立即数,再多了就变成浮点数了。ES2020为我们提供了BigInt.

我们用末尾加"n"的方式看看,解释器是如何帮我们实现的:

let la = 123n;

系统直接帮我们处理成了FixedArray:

Bytecode Age: 0
         0x24b2081d7ada @    0 : 13 00             LdaConstant [0]
         0x24b2081d7adc @    2 : 25 02             StaCurrentContextSlot [2]
         0x24b2081d7ade @    4 : 0e                LdaUndefined 
         0x24b2081d7adf @    5 : a9                Return 
Constant pool (size = 1)
0x24b2081d7a9d: [FixedArray] in OldSpace
 - map: 0x24b208002209 <Map>
 - length: 1
           0: 0x24b2081d7aa9 <BigInt 123>

类私有变量

我们知道,从ES2020开始,我们可以通过#定义私有变量:

class A3
        #priv_data = 100
        get_data()
                return this.#priv_data;
        

let a3 = new A3();
a3.get_data();

v8的解法是,交给Runtime处理,调用Runtime的CreatePrivateNameSymbol:

Bytecode Age: 0
         0x27b1081d352e @    0 : 81 00             CreateBlockContext [0]
         0x27b1081d3530 @    2 : 1a f9             PushContext r1
         0x27b1081d3532 @    4 : 13 02             LdaConstant [2]
         0x27b1081d3534 @    6 : c1                Star3 
         0x27b1081d3535 @    7 : 13 02             LdaConstant [2]
         0x27b1081d3537 @    9 : c1                Star3 
         0x27b1081d3538 @   10 : 65 78 01 f7 01    CallRuntime [CreatePrivateNameSymbol], r3-r3
         0x27b1081d353d @   15 : 25 02             StaCurrentContextSlot [2]
         0x27b1081d353f @   17 : 10                LdaTheHole 
         0x27b1081d3540 @   18 : bf                Star5 
         0x27b1081d3541 @   19 : 80 03 00 00       CreateClosure [3], [0], #0
         0x27b1081d3545 @   23 : c2                Star2 
         0x27b1081d3546 @   24 : 13 01             LdaConstant [1]
         0x27b1081d3548 @   26 : c1                Star3 
         0x27b1081d3549 @   27 : 80 04 01 00       CreateClosure [4], [1], #0
         0x27b1081d354d @   31 : be                Star6 
         0x27b1081d354e @   32 : 19 f8 f6          Mov r2, r4
         0x27b1081d3551 @   35 : 65 25 00 f7 04    CallRuntime [DefineClass], r3-r6
         0x27b1081d3556 @   40 : c1                Star3 
         0x27b1081d3557 @   41 : 80 05 02 00       CreateClosure [5], [2], #0
         0x27b1081d355b @   45 : c0                Star4 
         0x27b1081d355c @   46 : 32 f8 06 00       StaNamedProperty r2, [6], [0]
         0x27b1081d3560 @   50 : 1b f9             PopContext r1
         0x27b1081d3562 @   52 : 0b f8             Ldar r2
         0x27b1081d3564 @   54 : 25 02             StaCurrentContextSlot [2]
         0x27b1081d3566 @   56 : 16 02             LdaCurrentContextSlot [2]
         0x27b1081d3568 @   58 : c3                Star1 
         0x27b1081d3569 @   59 : 69 f9 fa 00 02    Construct r1, r0-r0, [2]
         0x27b1081d356e @   64 : 25 03             StaCurrentContextSlot [3]
         0x27b1081d3570 @   66 : 16 03             LdaCurrentContextSlot [3]
         0x27b1081d3572 @   68 : c2                Star2 
         0x27b1081d3573 @   69 : 2d f8 07 04       LdaNamedProperty r2, [7], [4]
         0x27b1081d3577 @   73 : c3                Star1 
         0x27b1081d3578 @   74 : 5d f9 f8 06       CallProperty0 r1, r2, [6]
         0x27b1081d357c @   78 : c4                Star0 
         0x27b1081d357d @   79 : a9                Return 
Constant pool (size = 8)
0x27b1081d34e5: [FixedArray] in OldSpace
 - map: 0x27b108002209 <Map>
 - length: 8
           0: 0x27b1081d3365 <ScopeInfo CLASS_SCOPE>
           1: 0x27b1081d34c1 <FixedArray[7]>
           2: 0x27b1081d3291 <String[10]: ##priv_data>
           3: 0x27b1081d33a9 <SharedFunctionInfo A3>
           4: 0x27b1081d33dd <SharedFunctionInfo get_data>
           5: 0x27b1081d3411 <SharedFunctionInfo <instance_members_initializer>>
           6: 0x27b108005895 <Symbol: (class_fields_symbol)>
           7: 0x27b1081d32a9 <String[8]: #get_data>

TurboFan IR

Ignition的字节码并非是v8中唯一的中间码,另外还有TurboFan的IR.

篇幅所限我们就贴一张TurboFan IR的图:

以算术IR JSAdd为例,我们看下TurboFan IR的结构:

小结

本文我们简单扫了下Ignition的盲。

从中我们可以看到,跟JVM bytecode比起来:

  • Ignition指令集非常丰富,比如判空指令丰富,比如GetIterator这种操作都有指令
  • 对运行时有较强依赖,较多功能依赖CallRuntime. 随着ES的升级,运行时功能也不断拓展。比如ES2019增加了Dynamic Import,v8就增加了一个DynamicImportCall的Runtime函数,而没有修改指令集
  • 有LdaTheHole这样的为内部状态服务的指令
  • 如果运行时会出错,会生成抛出错误之类的指令,出错信息是在运行时获取

如果对于v8如何将源代码编译成字节码的过程感兴趣的同学,可以继续观看 v8字节码的编译过程

以上是关于JavaScript的字节码 - v8 Ignition指令的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript的字节码 - v8 Ignition指令

字节码

图解 Google V8 # 13:字节码:V8为什么又重新引入字节码?

v8字节码的编译过程

v8字节码的编译过程

v8字节码的编译过程