V8 javascript engine代码阅读

v8代码组成

目录结构概要

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
src ---+
|
+---arm
+---arm64
+---mips
+---mips64
A +---ia32
+---x64
+---ppc
+---s390
+---wasm
+---asmjs
|
+---ast
+---compiler
B +---compiler-dispatcher
+---interpreter
+---parsing
|
+---js
+---builtins
C +---runtime
+---snapshot
+---regexp
+---profiler
|
D +---ic
|
+---heap
E +---heap-symbols.h
+---zone
+---objects
|
F +---inspector
|
+---base
+---debug
+---tracing
+---extensions
G +---libplatform
+---libsampler
+---third_party
+---trap-handler
|
+---*.cc/*.h
.
.
.
  • A:存储汇编代码,反汇编程序,宏汇编程序,模拟器等,对于不同CPU不同。
  • B:code generation系统,例如parse, compile, interpreter, etc.
  • C:JS built-in function和runtime helper function
  • D:Inline Cache code
  • E:object model(对象模型)和memory(内存)相关代码
  • F:Inspector
  • G:Debugging and platform abstraction layer codes are stored.

必读代码

  • api.h/api.cc
    An API for Embedder is defined.
  • objects.h/objects.cc
    定义了v8的所有对象模型
  • compiler/compiler.cc
    编译的入口点
  • compiler/pipeline.cc
    和compiler.cc关联,放置TurboFan
  • runtime/runtime-*.cc
    A runtime function is defined.
  • builtins/builtin-*.cc
    A faster runtime function group. It is described in CodeStubAssembler (commentary) or Assembler.
  • interpreter/*.cc
    Ignition解释器
  • ic/*.cc
    Inline Caching的实现
    存储Runtime(?)

v8的内部实现

公开API

  • v8::HandleScope
    生成一个虚拟的作用域,监视(绑定)从v8的GC分配的对象
  • v8::Local
    v8有GC,但c++没有GC
    相反,它通过RAII (Resource Acquisition Is Initialization)分配和释放资源
    在C++中有一个称为析构函数的函数,当分配到stack上的类超出作用域并被丢弃时,该函数被调用。
    通过v8::Local,当超出了类的作用域时,会自动调用析构函数,析构函数会自动释放资源,实现一种所谓的智能指针功能。
    v8::Local是一个包装类,用于监视在c++中分配给堆的对象,并在调用析构函数时与当前的HandleScope一起删除。
    例如:
    1
    2
    3
    4
    5
    6
    7
    void test() {
    v8::Isolate* isolate = v8::Isolate::GetCurrent();
    v8::HandleScope handle_scope;

    v8::Local<v8::Array> array = v8::Array::New(isolate, 3);
    ...
    }

创建v8::HandleScope后,所有v8::Local都将分配给该v8::HandleScope。
因此,当在测试函数结束时调用handle_scope析构函数时,也会删除与v8 :: HandleScope相关的所有v8::Local。

  • v8::Handle
    被v8::Local包装的类,但实际上链接到v8::HandleScope。
    有些api会返回这个v8::Handle,但基本上它就像v8::Local一样使用。
  • v8::Isolate
    v8::Isolate是v8代码库底层部分的一个非常特殊的部分。
    最初v8有很多静态方法,对多线程没有太多考虑。
    嗯,这是有问题的,因为Chromium必须分离进程并启动v8。
    顺便说一句,事实证明,在Embedder端尝试多线程会导致相当大的问题。
    出于这个原因,构建了v8::Isolate机制。
    v8::Isolate是一个存储在线程本地存储(TLS)中的巨大对象
    几乎存储了与执行上下文链接的所有全局信息。
    由于它存储在Tls中,因此可以透明地为每个线程提供不同的v8::Isolate,因此Embedder端可以在对其他线程不了解的情况下编写代码。
    内部使用的各种对象(FixedArray)和表示隐藏类等的Map类也是从这个v8::Isolate生成的
    几乎所有地方都传递了这个类,没有v8::Isolate就很难编写代码。
    再次使用上面的示例代码
    1
    2
    3
    4
    5
    6
    7
    void test() {
    v8::Isolate* isolate = v8::Isolate::GetCurrent();
    v8::HandleScope handle_scope;

    v8::Local<v8::Array> array = v8::Array::New(isolate, 3);
    ...
    }

你还可以看到此函数也传递了v8::Isolate。
它是v8::Array::New等等,但是v8::Isoalte实际上生成了这个数组。
因此,在v8中,没有太多需要考虑线程冲突,所以这是一个相当方便的机制。

v8::internal

除了外部公共API之外的所有类,都在v8::internal命名空间中定义。

对象模型

v8非常特殊,它在C++中创建自己的对象模型。
该对象模型在src/objects.h的开头注释中描述,
当它被简化和提取时,就会变成这样。

  • Object
    • Smi (immediate small integer)
    • HeapObject (superclass for everything allocated in the heap)
      • JSReceiver (suitable for property access)
        • JSObject
        • JSProxy
      • FixedArrayBase
        • ByteArray
        • BytecodeArray
        • FixedArray
        • FixedDoubleArray
      • Name
        • String
        • Symbol
      • HeapNumber
      • BigInt
      • Cell
      • PropertyCell
      • PropertyArray
      • Code
      • AbstractCode, a wrapper around Code or BytecodeArray
      • Map
      • Oddball
      • Foreign
      • SmallOrderedHashTable
      • SharedFunctionInfo
      • Struct
      • WeakCell
      • FeedbackVector

我们创建了一个以v8::i::Object为基类的对象树。
几乎所有在v8中使用的类都继承自v8::i::Object,这看起来像java。
v8不遵循c++方式使这个对象模型运行良好。
由于某些原因,这些类不通过c++类来创建字段。
这些类仅用于表示c++中的内存布局,并且所有字段都是通过直接为此指针指定偏移量来获得的。
换句话说,忽略c++对象布局,我们自己完全控制内存布局。
当以伪代码表示时,看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class SomeObject {
Value* get_field1() {
char* self = reinterpret_cast<char*>(this);
self += header_offset;
return Value::Cast(self);
}
void Initialize() {
char* self = reinterpret_cast<char*>(this);
self += header_offset;
*self = Smi::Cast(1);
}
};
static const size_t OBJECT_SIZE = sizeof(char) * 32;
SomeObject* object = reinterpret_cast<SomeObject*>(malloc(OBJECT_SIZE));
object->Initialize();
object->get_filed1(); // 1

通过这种方式,你可以自己控制字段的偏移。
让我们了解一下层次结构顶端的两个分支:

HeapObject

首先是v8::i::HeapObject。
由于v8::i::Object建立了如上所述的直接通过偏移的内存布局
在访问字段时,继承HeapObject的对象使用以下宏。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#define FIELD_ADDR(p, offset) \
(reinterpret_cast<byte*>(p) + offset - kHeapObjectTag)

#define READ_FIELD(p, offset) \
(*reinterpret_cast<Object* const*>(FIELD_ADDR_CONST(p, offset)))

// 这是在GC并发标记为ON时以原子方式更新字段
#ifdef v8_CONCURRENT_MARKING
#define WRITE_FIELD(p, offset, value) \
base::Relaxed_Store( \
reinterpret_cast<base::AtomicWord*>(FIELD_ADDR(p, offset)), \
reinterpret_cast<base::AtomicWord>(value));
#else
#define WRITE_FIELD(p, offset, value) \
(*reinterpret_cast<Object**>(FIELD_ADDR(p, offset)) = value)
#endif

SMI_ACCESSORS(FixedArrayBase, length, kLengthOffset)

#define SMI_ACCESSORS_CHECKED(holder, name, offset, condition) \
int holder::name() const { \
DCHECK(condition); \
Object* value = READ_FIELD(this, offset); \
return Smi::ToInt(value); \
} \
void holder::set_##name(int value) { \
DCHECK(condition); \
WRITE_FIELD(this, offset, Smi::FromInt(value)); \
}

// 实际上它扩展如下。

int FixedArrayBase::length() const {
DCHECK(condition);
Object* value = (*reinterpret_cast<Object* const*>(
reinterpret_cast<const byte*>(this) + kLengthOffset - kHeapObjectTag)
return Smi::ToInt(value);
}

int FixedArrayBase::set_length(int value) const {
DCHECK(condition);
base::Relaxed_Store(
reinterpret_cast<base::AtomicWord*>(
reinterpret_cast<byte*>(this) + kLengthOffset - kHeapObjectTag);
reinterpret_cast<base::AtomicWord>(Smi::FromInt(value)));
}

重要的是
reinterpret_cast<const byte*>(this) + kLengthOffset - kHeapObjectTag
在这部分中,我们看到在将特定字段的偏移量添加到此指针后减去kHeapObjectTag。
顺便说一句,kHeapObjectTag的定义如下。
const int kHeapObjectTag = 1
只有1,也就是说,只需在指针地址的末尾设置1即可。

以下是示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
include <stdio.h>
#include <stdlib.h>
#include <iostream>

const int kHeapObjectTag = 1;
const int kHeapObjectTagSize = 2;
const intptr_t kHeapObjectTagMask = (1 << kHeapObjectTagSize) - 1;

inline static bool HasHeapObjectTag(const char* value) {
return ((reinterpret_cast<intptr_t>(value) & kHeapObjectTagMask) ==
kHeapObjectTag);
}

int main() {
auto allocated = reinterpret_cast<char*>(
malloc(sizeof(char) * (2 + kHeapObjectTag)));
auto heap_object = allocated + kHeapObjectTag;
heap_object[0] = 'm';
heap_object[1] = 'v';
printf("%ld %ld %p %p %d\n", reinterpret_cast<intptr_t>(allocated),
reinterpret_cast<intptr_t>(heap_object), allocated, heap_object,
HasHeapObjectTag(heap_object));
free(allocated);
}

运行结果如下:
140289524108464 140289524108465 0x7f97b3400cb0 0x7f97b3400cb1 1
地址以1结尾。
另外,v8::i::HeapObject在开头有一个v8::Map对象来表示隐藏类,以便识别它自己的类型。
所以v8::i::HeapObject的内存布局如下。


由于我们总是有一个表示类型的v8::Map,我们可以通过查看它来看到v8::i ::HeapObject的类型。
此外,写为Derived Object Header的部分根据继承的对象而不同(如果它是v8::i::FixedArray则是长度字段)。
下面是Map和JSObject C ++代码的简化表示

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <stdio.h>
#include <stdlib.h>
#include <iostream>

const int kHeapObjectTag = 1;
const int kHeapObjectTagSize = 2;
const intptr_t kHeapObjectTagMask = (1 << kHeapObjectTagSize) - 1;

inline static bool HasHeapObjectTag(const char* value) {
return ((reinterpret_cast<intptr_t>(value) & kHeapObjectTagMask) ==
kHeapObjectTag);
}

class Map {
public:
enum InstanceType {
JS_OBJECT,
JS_ARRAY,
JS_STRING
};

void set_instance_type(InstanceType instance_type) {
instance_type_ = instance_type;
}

InstanceType instance_type() {
return instance_type_;
}
private:
InstanceType instance_type_;
};

const int kHeaderSize = sizeof(Map);
typedef char byte;
typedef char* Address;

class HeapObject {
public:
char value() {return reinterpret_cast<Address>(this)[0];}
Map::InstanceType instance_type() {
return reinterpret_cast<Map*>(
reinterpret_cast<Address>(this) - kHeaderSize)->instance_type();
}
void Free() {
auto top = reinterpret_cast<Address>(this) - kHeaderSize - kHeapObjectTag;
free(top);
}
protected:
static Address NewType(Map::InstanceType instance_type, size_t size) {
auto allocated = reinterpret_cast<Address>(
malloc(sizeof(byte) * (size + kHeaderSize + kHeapObjectTag)));
auto map = reinterpret_cast<Map*>(allocated);
map->set_instance_type(instance_type);
return allocated + kHeaderSize + kHeapObjectTag;
}
};

class JSObject: public HeapObject {
public:
static JSObject* New() {
auto a = NewType(Map::JS_OBJECT, 1);
a[0] = 'o';
return reinterpret_cast<JSObject*>(a);
}
};

class JSArray: public JSObject {
public:
static JSArray* New() {
auto a = NewType(Map::JS_ARRAY, 1);
a[0] = 'a';
return reinterpret_cast<JSArray*>(a);
}
};

class JSString: public JSObject {
public:
static JSString* New() {
auto a = NewType(Map::JS_STRING, 1);
a[0] = 's';
return reinterpret_cast<JSString*>(a);
}
};

int main() {
JSObject* objects[] = {
JSObject::New(),
JSArray::New(),
JSString::New()
};

for (int i = 0; i < 3; i++) {
auto o = objects[i];
switch (o->instance_type()) {
case Map::JS_OBJECT:
printf("JSObject => %c\n", o->value());
break;
case Map::JS_ARRAY:
printf("JSArray => %c\n", o->value());
break;
case Map::JS_STRING:
printf("JSString => %c\n", o->value());
break;
}
}

for (int i = 0; i < 3; i++) {
objects[i]->Free();
}
}

执行时,输出JSObject => o,JSArray => a,JSString => s。
我认为这个例子有点长,但我认为你可以看到:你可以正确地分配分配给堆的对象类型。
让我们解释一下Smi正在做些什么。

Smi

Smi是Small Integer的缩写,可以直接在指针区域中保存最多31位的整数。
似乎Ruby中也采用了相同的方法。
对于普通指针,32位CPU使用4个字节,64位CPU使用8个字节。
换句话说,如果它是一个高达31位的整数,则可以存储它而不是使用指针。
以这种方式,通过将其固定在指针区域中而不使用堆,实现了存储器节省和加速。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Smi {
public:
static Smi* FromInt(int value) {
return reinterpret_cast<Smi*>(value);
}

int value() {
return reinterpret_cast<intptr_t>(this);
}
};

Smi* NewSmi(int value) {
return Smi::FromInt(value);
}

int main() {
printf("%d %d\n", NewSmi(120)->value(), NewSmi(110)->value());
// out 120 110
}

另外,在v8::i::HeapObject的情况下,设置低位为1,但在Smi的情况下,结尾用0作标记,
通过cast可以直接进行数值计算。 因此,没有开销。
在64位CPU的情况下,由于指针是64位,因此可以存储更大的整数,但是为了与32位兼容,仅使用31位区域。

JSReceiver

  • JSArray
  • JSArrayBuffer
  • JSArrayBufferView
    • JSTypedArray
    • JSDataView
  • JSBoundFunction
  • JSCollection
    • JSSet
    • JSMap
  • JSStringIterator
  • JSSetIterator
  • JSMapIterator
  • JSWeakCollection
    • JSWeakMap
    • JSWeakSet
  • JSRegExp
  • JSFunction
  • JSGeneratorObject
  • JSGlobalObject
  • JSGlobalProxy
  • JSValue
    • JSDate
  • JSMessageObject
  • JSModuleNamespace
  • WasmInstanceObject
  • WasmMemoryObject
  • WasmModuleObject
  • WasmTableObject

这些v8::i::JS~类是类的真实形式,例如v8::String和v8::Array通过API使用它们。
诸如v8::String之类的类只是wrapper类。
所有实际的实现都是v8::i::JS~类。

FixedArrayBase

v8::i::FixedArray的基本实现,它是v8中的常用类。
v8在里面到处都在使用这个固定长度的数组,v8::i::FixedArray有以下层次结构。

  • DescriptorArray
  • FrameArray
  • HashTable
    • Dictionary
    • StringTable
    • StringSet
    • CompilationCacheTable
    • MapCache
  • OrderedHashTable
    • OrderedHashSet
    • OrderedHashMap
  • Context
  • FeedbackMetadata
  • TemplateList
  • TransitionArray
  • ScopeInfo
  • ModuleInfo
  • ScriptContextTable
  • WeakFixedArray
  • WasmSharedModuleData
  • WasmCompiledModule

特别的,v8::i::DescriptorArray是一个存储属性描述符的数组。

CodeStubAssembler (CSA)

在v8中使用的DSL语言。
实际上,在v8中,编写汇编语言并不是什么新鲜事。
相反,通过描述可以输出汇编的CSA,可以输出具有更高可维护性高速代码。
CSA的一个例子如下所示。
计算Fibonacci数,并将其存储在数组中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fibonacci(num){
var a = 1, b = 0, temp;
const result = [];

while (num >= 0){
result.push(a);
temp = a;
a = a + b;
b = temp;
num--;
}

return result;
}

将javascript函数转换为CSA时,它将成为以下代码。

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
31
TNode<JSArray> Fibonacci(TNode<Context> context) {
TVARIABLE(var_a, MachineType::PointerRepresentation(), IntPtrConstant(0));
TVARIABLE(var_b, MachineType::PointerRepresentation(), IntPtrConstant(1));
TVARIABLE(var_temp, MachineType::PointerRepresentation());
TVARIABLE(var_index, MachineType::PointerRepresentation());

Node* fixed_array = AllocateFixedArray(PACKED_ELEMENTS, IntPtrConstant(11),
INTPTR_PARAMETERS, kAllowLargeObjectAllocation)

Label loop(this), after_loop(this);

Branch(IntPtrGreaterThan(IntPtrConstant(100), var_index), &loop, &after_loop);
BIND(&loop);
{
StoreFixedArrayElement(fixed_array, SmiTag(var_index), var_a,
SKIP_WRITE_BARRIER);
var_temp.Bind(var_a);
var_a.Bind(IntPtrAdd(var_a, var_b));
var_b.Bind(var_temp);
Increment(&var_index, 1);
Branch(IntPtrGreaterThan(IntPtrConstant(100), var_index),
&loop, &after_loop);
}
BIND(&after_loop);
Node* native_context = LoadNativeContext(context);
Node* array_map = LoadJSArrayElementsMap(PACKED_ELEMENTS, native_context);
Node* array = AllocateUninitializedJSArrayWithoutElements(
array_map, SmiConstant(12), nullptr);
StoreObjectField(array, JSArray::kElementsOffset, fixed_array);
return array;
}

尽管它在某种程度上是抽象的,并且有很多冗余代码,但是它不是比汇编更容易阅读吗?

阅读代码

阅读v8代码非常麻烦,但有几种方法。

首先,使用每个IDE的代码跳转。
但是,由于v8使用了大量的宏,甚至即使是类的函数定义也可能由宏执行,因此最好在找不到时使用find | grep。

即使您阅读了代码,您可能也不知道执行时的状态,或者您可能不知道调用的层次结构,因此您应该在调试时按以下方式检查它。

  • c++
    由于src/base/debug/stack_trace.h中有一个StackTrace类,所以最好在要的点调用StackTrace st; st.Print()。
    此外,由于继承v8::Object类的对象始终具有Print方法,因此可以通过调用 ->Print()来查看内容。
  • CSA
    由于Print()函数是在CodeStubAssembler中定义的,我们在那里传递Node *并输出执行a-> Print()的代码。
    但是,要小心,因为传递IntPtrT会失败。 在这种情况下,你可以做SmiTag。