文/ 阿里淘系 F(x)Team - 旭伦
前面的文章我们介绍了在 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
这个容易理解,针对的是[], {}, // 三种值。
我们看几个例子.
- 空数组:
let a1 = [];
作为常用操作,Ignition为其提供了一个专门指令:CreateEmptyArrayLiteral
0x24b2081d3386 @ 0 : 7b 00 CreateEmptyArrayLiteral [0] 0x24b2081d3388 @ 2 : 25 02 StaCurrentContextSlot [2] 0x24b2081d338a @ 4 : 0e LdaUndefined 0x24b2081d338b @ 5 : a9 Return
- 空对象
let a2={};
Ignition为其提供了一个专门指令:CreateEmptyObjectLiteral
0x24b2081d411e @ 0 : 7d CreateEmptyObjectLiteral 0x24b2081d411f @ 1 : 25 02 StaCurrentContextSlot [2] 0x24b2081d4121 @ 3 : 0e LdaUndefined 0x24b2081d4122 @ 4 : a9 Return
- 正则表达式
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);
因为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++) {con(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这样的为内部状态服务的指令
- 如果运行时会出错,会生成抛出错误之类的指令,出错信息是在运行时获取