Home V8 Internals Part 1
Post
Cancel

V8 Internals Part 1

Pointer Compression

Pointer Compression in V8 leverages a fascinating characteristic of heap-based objects, namely their proximity to one another. This proximity often results in a significant portion of the pointer having identical most significant bits. Exploiting this, V8 conserves memory by storing only the least significant bits of the pointer, while reserving the upper 32 bits (referred to as the isolate root) in a designated root register (R13). When a pointer needs to be accessed, it is simply combined with the value in the register, yielding the complete address. This compression strategy is implemented in the /src/common/ptr-compr-inl.h source file within V8.

Fundamentally, the objective pursued by the V8 team was to find a way to accommodate both types of tagged values within 32 bits on 64-bit architectures. This endeavor was undertaken to minimize overhead in V8, with the specific aim of reclaiming as many of the 4-byte wastages inherent to the x64 architecture as possible.

Pointer Tagging

The technique of pointer tagging relies on the recognition that on x32 and x64 systems, allocated data is required to be positioned at word-aligned (4-byte) boundaries. This alignment characteristic ensures that the least significant bits (LSB) will consistently hold a value of zero. Consequently, pointer tagging utilizes these two least significant bits to discern between a pointer referring to a heap object and an integer or Small Integer (SMI). This optimization allows for efficient distinction and handling of different data types within the system.

Array

Here’s an general explanation of how Arrays are stored in memory in V8:

When an Array is created in V8, memory is allocated for it on the heap. The object’s value is a pointer to the JSArray structure, which encompasses the following components:

  • Map: This is a pointer pointing to the HiddenClass object. The HiddenClass object essentially defines the “shape” or structure of the object, akin to a blueprint for the array.
  • Properties: This pointer directs to an object that holds the named properties of the array.
  • Elements: This pointer directs to an object that contains the numbered properties of the array.

Attaching GDB to the d8 instance with the --allow-natives-syntax flag grants us enhanced debugging capabilities. This allows the utilization of %DebugPrint(arr) to obtain detailed information regarding its internal structure and memory allocation. This debugging feature is instrumental in gaining deeper insights into how V8 handles arrays in memory.

1
2
var arr1 = [1.1,2.2]
%DebugPrint(arr)
1
2
3
4
5
6
DebugPrint: 0x52117690b99: [JSArray]
 - map: 0x000f010c2ed9 <Map(PACKED_DOUBLE_ELEMENTS)>
 - elements: 0x052117690b81 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS] { 0: 1.1 , 1:2.2 } 
 - properties: 0x0ac43ba80c71 <FixedArray[0]>
 - length: 2
 
1
var arr = [1.1,2.2]

img_1

# Pointer Compression OFF 


DebugPrint: 0x52117692fb1: [JSArray]
 - map: 0x000f010c2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> 
 - properties: 0x0ac43ba80c71 <FixedArray[0]> 
 - elements: 0x052117692f91 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS] { 0: 1.1   1: 2.2 }
 - length: 2

pwndbg> x/8gx 0x52117692fb1-1 [JSArray]
0x52117692fb0:	0x0000000f010c2ed9 (Map)           0x00000ac43ba80c71 (Properties)
0x52117692fc0:	0x0000052117692f91 (Elements)	     0x0000000200000000 (Length)
0x52117692fd0:	0x00000ac43ba80941 (Ignore this)   0x00000010dab15b0e (Ignore this)
0x52117692fe0:	0x7250677562654425 (Ignore this)   0x2972726128746e69 (Ignore this)


pwndbg> x/8gx 0x0000052117692f91-1 [Element]
0x52117692f90:	0x00000ac43ba814f9 (Map)	0x0000000200000000 (Length)
0x52117692fa0:	0x3ff199999999999a (1.1)	0x400199999999999a (2.2)






# Pointer Compression ON


DebugPrint: 0x2f990024cf85: [JSArray]
 - map: 0x2f990014ed75 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - properties: 0x2f9900000219 <FixedArray[0]>
 - elements: 0x2f990024cf6d <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS] { 0: 1.1   1: 2.2 }
 - length: 2
 
pwndbg> x/5wx 0x2f990024cf85-1 [JSArray]
0x2f990024cf84:	0x0014ed75 (Map) 	0x00000219 (Properties) 	0x0024cf6d (Elements)   	0x00000004 (length << 1)
0x2f990024cf94:	0x0000058d (Ignore This)

pwndbg> x/5wx 0x2f990024cf6d-1 [Element]
0x2f990024cf6c:	0x00000925 (Map)	0x00000004 (Length << 1)	0x9999999a (1.1[0])	0x3ff19999 (1.1[1])
0x2f990024cf7c:	0x9999999a (2.1[0])


pwndbg> x/5gf 0x2f990024cf6d-1
0x2f990024cf6c:	8.4879843204687662e-314 (Map|Length <<1)	1.1000000000000001 (1.1)
0x2f990024cf7c:	2.2000000000000002 (2.2)                	1.1395124173638311e-311 (Ignore This)




1
2
var arr = [1,2,3]
%DebugPrint(arr)
1
2
3
4
5
6
// Clean up some of the output 
DebugPrint: 0x5211768dd49: [JSArray]
 - map: 0x000f010c2d99 <Map(PACKED_SMI_ELEMENTS)> 
 - properties: 0x0ac43ba80c71 <FixedArray[0]> 
 - elements: 0x05211768dcd9 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] {  0:1  1:2  2:3 }
 - length: 3
1
2
3
4
5
6
7
8
9
var obj = {1:'A'}
var arr2 = ['A']
var arr3 = [arr1]
var arr4 = [arr2]
var arr5 = [obj]
%DebugPrint(arr2)
%DebugPrint(arr3)
%DebugPrint(arr4)
%DebugPrint(arr5)
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
DebugPrint: 0x52117691a59: [JSArray]
 - map: 0x000f010c2f79 <Map(PACKED_ELEMENTS)> 
 - properties: 0x0ac43ba80c71 <FixedArray[0]> 
 - elements: 0x052117690e01 <FixedArray[1]> [PACKED_ELEMENTS (COW)] { 0: 0x3d7d68562569 <String[#1]: A> }
 - length: 1
["A"]

DebugPrint: 0x52117691ad1: [JSArray]
 - map: 0x000f010c2f79 <Map(PACKED_ELEMENTS)> [FastProperties]
 - properties: 0x0ac43ba80c71 <FixedArray[0]>
 - elements: 0x052117691ab9 <FixedArray[1]> [PACKED_ELEMENTS] { 0: 0x052117690b99 <JSArray[1]> }
 - length: 1
[[1.1]]

DebugPrint: 0x52117691b49: [JSArray]
 - map: 0x000f010c2f79 <Map(PACKED_ELEMENTS)> [FastProperties]
 - properties: 0x0ac43ba80c71 <FixedArray[0]> 
 - elements: 0x052117691b31 <FixedArray[1]> [PACKED_ELEMENTS] { 0: 0x052117691a59 <JSArray[1]> }
 - length: 1

[["A"]]

DebugPrint: 0x52117692901: [JSArray]
 - map: 0x000f010c2f79 <Map(PACKED_ELEMENTS)> [FastProperties]
 - properties: 0x0ac43ba80c71 <FixedArray[0]>
 - elements: 0x0521176928e9 <FixedArray[1]> [PACKED_ELEMENTS] { 0: 0x052117692381 <Object map = 0xf010c0459> }
 - length: 1

 [{1: "A"}]


Indeed, the use of different map types based on the data within the array serves to facilitate optimization. This dynamic mapping allows V8 to adapt to runtime changes in array content. It’s important to note that these transitions are unidirectional. Once an array transitions from, for example, PACKED_SMI_ELEMENTS to PACKED_DOUBLE_ELEMENTS, it will not revert back. This progression toward less optimized representations as we move further to the right underscores the adaptive nature of V8’s optimization strategies.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const array = [1, 2, 3];
// elements kind: PACKED_SMI_ELEMENTS
array.push(4.56);
// elements kind: PACKED_DOUBLE_ELEMENTS
array.push('x');
// elements kind: PACKED_ELEMENTS
const array = [1, 2, 3, 4.56, 'x'];
const array = [1, 2, 3, 4.56, [1]];
// elements kind: PACKED_ELEMENTS
array.length; // 5
array[9] = 1;
// array[5] until array[8] are now holes
// elements kind: HOLEY_ELEMENTS

img_2

Reference

https://v8.dev/blog/elements-kinds

This post is licensed under CC BY 4.0 by the author.