Pointer Compression in V8
1. 관련글
2. V8 힙
자바 스크립트로 생성한 객체, 배열, 함수는 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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
d8> let test = new Uint8Array(0x10) d8> test.fill(0x41) d8> %DebugPrint(test) DebugPrint: 0x1f080810b769: [JSTypedArray] - map: 0x1f08082c24d9 <Map(UINT8ELEMENTS)> [FastProperties] - prototype: 0x1f080828507d <Object map = 0x1f08082c2501> - elements: 0x1f080810b751 <ByteArray[16]> [UINT8ELEMENTS] - embedder fields: 2 - buffer: 0x1f080810b711 <ArrayBuffer map = 0x1f08082c3271> - byte_offset: 0 - byte_length: 16 - length: 16 - data_ptr: 0x1f080810b758 - base_pointer: 0x810b751 - external_pointer: 0x1f0800000007 - properties: 0x1f080800222d <FixedArray[0]> - All own properties (excluding elements): {} - elements: 0x1f080810b751 <ByteArray[16]> { 0-15: 65 } - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) } 0x1f08082c24d9: [Map] - type: JS_TYPED_ARRAY_TYPE - instance size: 72 - inobject properties: 0 - elements kind: UINT8ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1f08080023b5 <undefined> - prototype_validity cell: 0x1f0808202405 <Cell value= 1> - instance descriptors (own) #0: 0x1f08080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)> - prototype: 0x1f080828507d <Object map = 0x1f08082c2501> - constructor: 0x1f0808285005 <JSFunction Uint8Array (sfi = 0x1f0808209dcd)> - dependent code: 0x1f08080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65 gef➤ vmmap Start End Offset Perm Path 0x00001f0800000000 0x00001f0800003000 0x0000000000000000 rw- 0x00001f0800003000 0x00001f0800004000 0x0000000000000000 --- 0x00001f0800004000 0x00001f0800015000 0x0000000000000000 r-x 0x00001f0800015000 0x00001f080003f000 0x0000000000000000 --- 0x00001f080003f000 0x00001f0808000000 0x0000000000000000 --- 0x00001f0808000000 0x00001f08080d5000 0x0000000000000000 r-- 0x00001f08080d5000 0x00001f0808100000 0x0000000000000000 --- 0x00001f0808100000 0x00001f0808212000 0x0000000000000000 rw- 0x00001f0808212000 0x00001f0808240000 0x0000000000000000 --- 0x00001f0808240000 0x00001f0808243000 0x0000000000000000 rw- 0x00001f0808243000 0x00001f0808280000 0x0000000000000000 --- 0x00001f0808280000 0x00001f0808300000 0x0000000000000000 rw- 0x00001f0808300000 0x00001f0900000000 0x0000000000000000 --- 0x0000555555554000 0x0000555555604000 0x0000000000000000 r-- /home/hacker/Documents/fullchain/v8/out.gn/x64.debug/d8 0x0000555555604000 0x0000555555691000 0x00000000000af000 r-x /home/hacker/Documents/fullchain/v8/out.gn/x64.debug/d8 0x0000555555691000 0x0000555555694000 0x000000000013b000 r-- /home/hacker/Documents/fullchain/v8/out.gn/x64.debug/d8 0x0000555555694000 0x0000555555696000 0x000000000013d000 rw- /home/hacker/Documents/fullchain/v8/out.gn/x64.debug/d8 0x0000555555696000 0x0000555555774000 0x0000000000000000 rw- [heap] |
V8 힙은 GDB 등의 디버거에서 [heap]으로 표시되는 메모리 영역과는 다른 영역이다.
위에서 V8 힙은 상위 32비트가 0x00001f08로 표시되는 모든 메모리 영역으로, 보통 프로그램의 최하위 메모리 주소에 매핑된다.
V8에서 메모리는 모두 격리(isolate)된다고 한다. 위에서 보다시피 최하위 메모리 주소에 매핑되며, 바이너리의 텍스트 세그먼트가 바로 다음에 매핑된다.
V8 힙의 주소는 실행될 때마다 변경되지만, 상위 32비트(0x00001f08)는 실행 중에 변경되지 않는다.
3. 포인터 압축
따라서, V8 힙 포인터를 저장할 때 하위 32비트만 저장하기로 하였다.
1 2 3 4 5 6 |
gef➤ x/16xw 0x1f080810b769-1 0x1f080810b768: 0x082c24d9 0x0800222d 0x0810b751 0x0810b711 // Map Pointer Element Pointer 0x1f080810b778: 0x00000000 0x00000000 0x00000010 0x00000000 0x1f080810b788: 0x00000010 0x00000000 0x00000007 0x00001f08 0x1f080810b798: 0x0810b751 0x080023b4 0x00000000 0x00000000 |
이를 포인터 압축이라고 하며, 포인터 압축을 통해 힙 메모리 사용량을 40%까지 절약하였다고 한다.
V8 힙 주소의 상위 32비트를 격리 루트(isolate root)라고 부른다.
격리 루트는 특정 레지스터(R13)에 저장하며, 이 레지스터를 루트 레지스터라고 부른다.
포인터 압축의 단점은 V8 힙의 사이즈가 최대 4GB(32비트만 주소로 사용)로 제한된다는 것이다.
이런 단점 때문에, node.js는 포인터 압축을 사용하지 않는다.
포인터 압축과 관련된 코드는 아래의 위치에 구현되어 있다.
- v8/src/common/prt-compr.h
- v8/src/common/ptr-compr-inl.h
4. 포인터 압축과 V8 익스플로잇
자바 스크립트를 통해 격리 루트를 알아내는 건 어렵다. 하지만, 격리 루트를 굳이 알아야할 필요는 없다.
addrof 또는 fakeobj가 가능하다면, 가짜 JSArray를 만든 후 엘리먼트 포인터를 수정하여 임의의 주소에 데이터를 쓰고, 읽을 수 있다.
JSArray의 엘리먼트 포인터에는 32비트로 압축된 포인터가 저장된다.
따라서, V8 힙 내부의 주소에만 데이터를 쓰고, 읽을 수 있다.
이를 극복하기 위한 기본적인 방법은 V8 힙에 ArrayBuffer를 할당하는 것이다.
ArrayBuffer의 엘리먼트 포인터는 PartitionAlloc를 사용해서 할당되기 때문이다.
PartitionAlloc은 V8 힙이 아닌 메모리 영역에 할당한다.
즉, ArrayBuffer의 엘리먼트 포인터에 압축되지 않은 64비트 포인터를 저장하여, V8 힙 이외의 메모리 영역에 접근할 수 있다.