v8学习笔记

v8的组成

v8主要由这四个模块组成:
1.Parser:解析器将javascript源码转换为抽象的语法树(Abstract Syntax Tree/AST)
2.Ignition:解释器,将AST转换为Bytecode,并解释执行Bytecode;此外还会同时收集TurboFan优化编译所需要的信息,比如函数参数的类型
3.TurboFan:编译器,利用Ignitio收集的类型信息,将Bytecode转换为优化后的汇编代码(Optimized Machine Code/v8里将汇编代码称作machine code)
4.Orinoco:Garbager Coollector(GC),垃圾回收部分,负责将程序不再需要的内存空间回收
原理如下

1
2
3
4
5
6
7
8
9
javascript ----> Parser ----> AST -----> Ignition --------------> Bytecode ------> 运行结果(Ignition直接运行Bytecode)
|
|
|
----------------------> TurboFan -----> Machine code -----> 运行结果(经过TurboFan优化)
|
|(deoptimization)
|
------------> Bytecode ------> 运行结果

Parser

Parser的部分源码如下

1
2
3
4
5
6
7
8
9
10
11
FunctionLiteral* Parser::ParseProgram(Isolate* isolate, ParseInfo* info) {
...
scanner_.Initialize();
scanner_.SkipHashBang();
FunctionLiteral* result = DoParseProgram(isolate, info);
MaybeResetCharacterStream(info, result);
MaybeProcessSourceRanges(info, result, stack_limit_);
HandleSourceURLComments(isolate, info->script());
...
return result;
}

parser在生成AST之前,会调用scanner_.Initialize进行词法分析将javascript转化为有意义的代码块(Token),然后parser再将Token解析成AST
比如var answer = 6 * 7;进行词法分析后的token如下(在线网站:https://esprima.org/demo/parse.html#)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "answer"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "6"
},
{
"type": "Punctuator",
"value": "*"
},
{
"type": "Numeric",
"value": "7"
},
{
"type": "Punctuator",
"value": ";"
}
]

生成的AST如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	type: Program
- body
- #1
type: VariableDeclaration(申明变量)
- declarations
- #1
type: VariableDeclarator
- id
type: Identifier
name: answer
- init
type: BinaryExpression
operator: *
- left
type: Literal
value: 6
raw: 6
- right
type: Literal
value: 7
raw: 7
kind: var
sourceType: script

Pre-parser

例子如下

1
2
3
4
5
6
7
function func1() {
console.log('aaa');
}
function func2() {
console.log('bbb');
}
func1();

v8并不会直接解析代码中的所有函数,对于函数申明,会使用pre-parser进行预解析,而遇到立即调用的函数表达式(IIFE)
会调用parser完全解析函数,并生成AST。
pre-parser需要做的事情有:验证它跳过的函数在语法上是否有效,并生成正确编译外部函数所需的所有信息。当稍后调用预先解析的函数时,它会根据需要
进行完全
解析和编译。
关于惰性parser例子如下

1
2
3
4
5
6
7
8
9
// This is the top-level scope.
function outer() {
// preparsed
function inner() {
// preparsed
}
}

outer(); // Fully parses and compiles `outer`, but not `inner`.

outer直接指向外部context,该context包含内部函数使用变量申明的值。为了允许函数的惰性编译(并支持调试器),上下文指向一个名为ScopeInfo。
ScopeInfo对象描述了上下文中列出的变量。这意味着在编译内部函数时,我们可以计算变量在上下文链中的位置。
关于per-parser更详细的信息见:https://v8.dev/blog/preparser

Parser

javascritp代码中,立即调用的函数表达式(IIFE)由parser将其进行完全解析,并生成AST

Ignition

Ignition 解释器本身由一系列字节码处理程序代码片段组成,每个片段都处理一个特定的字节码,然后调度给下一个字节码的处理程序。这些字节码处理程序使用高
层级的、机器架构无关的汇编代码写成,由 CodeStubAssembler 类实现,并由 TurboFan 编译。
在2017之前v8使用full-codegen+Crankshaft 生成machine code,不生成bytecode,之后设计了Ignition,通过生成bytecode的方式节省空间与时间
AST到bytecode的流程大致如下:

1
2
Parser --> AST --> Bytecode Generator --> Register Optimizer --> Peephole Optimizer
--> Dead-code Elimination --> Bytecode Array Writer --> Bytecode --> Interpreter

1.Bytecode Generator:作用是遍历AST,并为每个子节点生成Bytecode
补充:Bytecode Generator本质是一系列字节码处理片段,是用与机器架构无关的高级别的汇编代码写成的,由CodeStubAssembler 实现并由Turbofan编译
更多细节参考:https://www.youtube.com/watch?v=r5OWCtuKiAk&t=1062s 以及https://zhuanlan.zhihu.com/p/41496446

TurboFan

TODO:https://paper.seebug.org/1936/ (简单理解)

object

关于V8对象的部分继承关系如下

1
2
3
4
5
6
7
TaggedImpl ------> Object -----> Smi
|
|
-----> HeapObject ------> JsReceiver ------> JSOBJECT
|
|
-----> JSProxy

avatar
这个图非常的直观

TaggedImpl

TaggedImpl的作用是识别Pointer和Smi(为了GC的准确垃圾回收+节省空间),通过利用地址按字长对齐的特性,v8中相关处理如下:
1.整数往左移1位,最后一位为0,则表示数值
2.指针由于最后一位本来就是0不用改变,即设置为1

HeapObject

heapobject 的编码方式如下
avatar
其中Object包括的内容有:
Object::KHeaderSize = 0
HeapObject:
增加了kMapOfset、此时kHeaderSize=8
JSReceiver:
增加了KProperiesOrHashOffset,此时KHeaderSize = 16
JSObject:
增加了KElementsOffset,此时KeaderSize = 24
此时JSObject拥有三个内置的属性:
1.map
2.propertiesOrHash
3.elements

JSReceiver

JSReceiver在堆上的形式,如下
avatar
JS中数组和字典在使用上区别不大,当时根据v8实现的角度,在其内部位数组和字典选择不同的数据结构可以优化它们
的访问速度,所以分别使用propertiesOrHash和elements这两个属性,这两个都是指针,v8会根据实际情况将它们
连接到堆上的不同的数据结构

Map

Map的内存拓扑图,如图
avatar
map称作hidden class,用于描述对象的元信息,比如instance size
如图
avatar

ArrayObject

js中数组的类型很多有:
1.PACKED_SMI_ELEMENTS
2.PACKED_DOUBLE_ELEMENTS
3.PACKED_ELEMENTS
4.HOLEY_SMI_ELEMENTS
5.HOLEY_DOUBLE_ELEMENTS
n…
各类型可以相互转换如图
avatar
packed的实际意思是连续的数组,HOLEY表示不连续的数组
编写test.js测试

1
2
3
4
var a = [1,2,3,4,5,6,7,8];
%DebugPrint(a);
%SystemBreak();
var b = [111,333,444];

jsarray如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DebugPrint: 0x1d66b9d4ddc9: [JSArray]
- map: 0x0071b8cc2d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x2fcad4011111 <JSArray[0]>
- elements: 0x1d66b9d4dd31 <FixedArray[8]> [PACKED_SMI_ELEMENTS (COW)]
- length: 8
- properties: 0x3cb756300c71 <FixedArray[0]> {
#length: 0x35cea10c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x1d66b9d4dd31 <FixedArray[8]> {
0: 1
1: 2
2: 3
3: 4
4: 5
5: 6
6: 7
7: 8
}

此时看一下elements,如下

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> job 0x1d66b9d4dd31
0x1d66b9d4dd31: [FixedArray]
- map: 0x3cb756300851 <Map>
- length: 8
0: 1
1: 2
2: 3
3: 4
4: 5
5: 6
6: 7
7: 8

内存中的组成为:map + properties + elements + length +

inobject、fast、slow

inobject、fast、slow是对象的三种命名属性类型:
1.inobject直接储存在对象本身
2.fast属性储存在属性中,需要通过map中的描述信息访问对应的信息
3.slow属性储存在self-contained属性子弹中,元信息不通过map共享
inobject和fast适用于静态场景(不会发生比较多的属性添加),slow适用于动态场景(比如频繁添加属性、以及delete等操作),并且inobject和fast
都有着一定的配额当创建对象时,会为对象分配额外的配额,添加属性会根据配额设置位对应的属性。
inobject和fast的形式如图
avatar
slow形式如图
avatar
inobject和fast以及slow这三种形式,v8引擎会根据情况切换(执行了delete等操作)
补充:创建对象时会为对象分配额外的inobject配额,如果此时分配的inobject没有被适用,会造成空间的浪费,此时v8使用了名叫slack tracking的技术

slack tracking

slack tracking实现方式如下:
1.构造函数对象的 map 中有一个 initial_map() 属性,该属性就是那些由该构造函数对象创建的模板,即它们的 map
slack tracking 会修改 initial_map() 属性中的 instance_size 属性值,该值是 GC 分配内存空间时使用的
2.当第一次使用某个构造函数 C 创建对象时,它的 initial_map() 是未设置的,因此初次会设置该值,简单来说就是创建一个新的 map
对象,并设置该对象的 construction_counter 属性
3.construction_counter 其实是一个递减的计数器,初始值是 kSlackTrackingCounterStart 即 7随后每次(包括当次)使用该构造函数创建对象,
都会对 construction_counter 递减,当计数为 0 时,就会汇总当前的属性数(包括动态添加的),然后得到最终的 instance_size
slack tracking 完成后,后续动态添加的属性都是 fast 型的。
construct_counter计数形式如下图:
avatar

Others

指针压缩

v8 8.0中为提高64位机器内存利用率而引入的机制。
引入指针压缩前的对象布局,如图
avatar
引入指针压缩后,如图
avatar
此时实际的指针位R13 + half_point