认识v8编译的字节码(bytecode)

2020年7月25日 ... ☕️ 5 min read

js代码在v8的编译过程

代码需要编译为机器代码,才能最终在目标机器上执行。v8为了做到这一点,设计了中间层-字节码(bytecode)。

为了方便转译为机器代码,字节码采用了和物理CPU类似的带寄存器-累加器的计算模型。它的理解难度介于高级语言和汇编语言中间。

这个代码到bytecode的转译过程发生在Ignition里;而从bytecode到机器代码,则在TurboFan里进行。

v8-compile-code v8引擎编译代码的过程

了解bytecode

bytecode更偏向底层一些,比console.log更直观,一步一步检查很方便。不过语法更像汇编,所以理解上需要一些时间。

举个例子

function assignTest() {
  var str = 'string';
  var obj = {a: 0};
}

assignTest();

其中的字符串'string'和对象{a: 0}会保存在堆,保留一个内存地址。

字节码输出如下

./d8 --print-bytecode --print-bytecode-filter="assignTest" /path/to/dest.js 
[generated bytecode for function: assignTest (0x17f0082520ad <SharedFunctionInfo assignTest>)]
Parameter count 1
Register count 2
Frame size 16
         0x17f00825226e @    0 : 12 00             LdaConstant [0]
         0x17f008252270 @    2 : 26 fb             Star r0
         0x17f008252272 @    4 : 7d 01 00 29       CreateObjectLiteral [1], [0], #41
         0x17f008252276 @    8 : 26 fa             Star r1
         0x17f008252278 @   10 : 0d                LdaUndefined 
         0x17f008252279 @   11 : aa                Return 
Constant pool (size = 2)
0x17f00825223d: [FixedArray] in OldSpace
 - map: 0x17f0080424ad <Map>
 - length: 2
           0: 0x17f008044c4d <String[6]: #string>
           1: 0x17f008252221 <ObjectBoilerplateDescription[3]>
Handler Table (size = 0)
Source Position Table (size = 0)

我们只取中间执行部分出来看看,关于语法的定义,直接去 v8的js-graph.cc 查找

LdaConstant [0] #从常量池里取[0]的部分,即'string'的地址
Star r0         #保存到r0寄存器: var str = 'string'
CreateObjectLiteral [1], [0] #创建一个字面量对象
Star r1         #把这个对象保存r1寄存器: var obj = {a: 0} 
LdaUndefined 
Return 

有了上面的例子,我们立刻写一个经典的变量提升的问题

function assignTest() {
  console.log(str);
  var str = 'string';
  var obj = {a: 0};
}

assignTest()

对应的字节码

LdaGlobal [0], [0]Star r3LdaNamedProperty r3, [1], [2]Star r2CallProperty1 r2, r3, r0, [4]LdaConstant [2]
Star r0
CreateObjectLiteral [3], [6], #41
Star r1
LdaUndefined 
Return 
Constant pool (size = 4)
 - length: 4
           0: 0x37d6081c8d21 <String[7]: #console>
           1: 0x37d6081c8d95 <String[3]: #log>
           2: 0x37d608044c4d <String[6]: #string>
           3: 0x37d608252221 <ObjectBoilerplateDescription[3]>

可以看出,除了高亮的部分,余下的部分是一样的。

LdaGlobal [0], [0] #设置全局变量window.console
Star r3            #保存在r3寄存器
LdaNamedProperty r3, [1], [2] #取console.log
Star r2            #保存在r2
CallProperty1 r2, r3, r0, [4] #调用window.console.log(r0)
LdaConstant [2]    #取"string"
Star r0            #保存到r0

可以看出,编译器预留了r0寄存器,来保存'string'地址,调用console.log的当下,r0还没有赋值。所以我们看到的结果就是输出了'undefined'

那么如果用let声明会发生什么?自己试试吧,会遇到坑(hole)。

如何输出字节码

发行版的node环境,执行node --print-bytecode /path/to/target.js就会输出字节码。

但是release版本constant pool默认不显示,需要手动编译debug版本的v8才能看到。编译的方法在这里:Building V8 with GNV8 bytecode 系列文介紹

附:堆栈、寄存器、累加器

堆(heap)是不连续的内存区域,主要存放对象等。

栈(stack)是一块连续的内存区域,按照一定顺序存放,栈中主要存放基本类型变量的值以及指向堆中的数组或者对象的地址。

寄存器(register)有很多个,用于保存基本变量和引用变量的地址。

累加寄存器(accumulator register)用于保存中间计算的结果。

参考页面:

#what-is#bytecode

SideEffect is a blog for front-end web development.
Code by Axiu / rss