ArkTS Runtime & HookKit
by xiangxiang
ArkTS Runtime & HookKit
0x00 Overview
- Architectural Foundations of ArkTS Runtime
┌────────────────────────────────────────────────────────────────┐
│ ArkTS Source (.ets) │
└─────────────────────────────────┬──────────────────────────────┘
│ es2abc (es2panda Frontend)
▼
┌────────────────────────────────────────────────────────────────┐
│ Ark Bytecode (.abc) │
│ (Panda Bytecode: Compact Reg-Acc ISA) │
└─────────────────┬───────────────────────────────┬──────────────┘
│ (+ .ap PGO Profile) │
ark_aot_compiler JIT / Interpreter
▼ ▼
┌───────────────────────────┐ ┌────────────────────────────────┐
│ Ark Native AOT (.an/.ai) │ │ Ark Runtime (ETS VM) │
│ Optimized Machine Code │ │ - ASM Interpreter (stub.an) │
└─────────────────┬─────────┘ │ - Inline Caches (IC) & PGO │
│ │ - Regional / Shared / CMC-GC │
▼ └─────────────────┬──────────────┘
┌─────────────────────────────────────────────────▼──────────────┐
│ HarmonyOS Microkernel │
└────────────────────────────────────────────────────────────────┘
0x01 Source Code to Bytecode
- es2abc
- .abc Binary Format, ark_disasm (disassembler/), ark_asm (assembler/)
ark_asmnot compiled by default, and NOT working
# Compile ArkTS (.ets) to Ark Bytecode (.abc) as an ES Module
es2abc --module --merge-abc input.ets --output input.abc
# Disassemble .abc into Panda Assembly (.pa) with verbose metadata
ark_disasm --verbose input.abc input.pa
0x02 ArkCompiler ETS Runtime
| Sub System | Source Directory | Core Responsibilities |
|---|---|---|
| Common Components | common_components | Unified runtime foundation (objects/, heap/, mutator/): CMC-GC (Concurrent Mark-Compact), Regional Heap, Write Barriers, Lock-Free String Table, and Mutator/Safepoint management |
| ECMAScript / ArkTS Engine | ecmascript | Object model (JSObject/JSHClass); ASM (stub.an) & C++ Bytecode Interpreter; Builtins; @Sendable Shared Heap; Inline Caches (IC) & PGO Profiler |
| Compiler Infrastructure | ecmascript/compiler | Circuit IR (Sea-of-Nodes), AOT (ark_aot_compiler), JIT, and Stub compiler (ark_stub_compiler) with LLVM and LiteCG backends |
| AOT (Ahead-of-Time) compilation | compiler_service | System Ability (aot_compiler_service) IPC wrapper for thermal- and idle-aware on-device AOT compilation scheduling |
2.1 JSTaggedValue
- ArkTS uses 64-bit NaN-boxing / Pointer Tagging to represent primitives and heap pointers (
JSTaggedValue) - https://github.com/openharmony/arkcompiler_ets_runtime/blob/OpenHarmony-v6.1-LTS/ecmascript/js_tagged_value_internals.h
// NaN-boxing pointer is used and the upper 16 bits (two bytes) act as the primary tag:
// Object: [0x0000] [48-bit 8B-aligned direct pointer]
// WeakRef: [0x0000] [47-bit direct pointer | 0x1]
// / [0x0001] [48-bit payload]
// TaggedDouble: ...... (IEEE-754 double + 0x0001000000000000ULL offset)
// \ [0xFFFE] [48-bit payload]
// TaggedInt: [0xFFFF] [0x0000] [32-bit signed integer]
//
// Immediate Special Markers (Top 48 bits == 0):
// Undefined: [56 bits 0] | 0x02 // 0000_0010
// Null: [56 bits 0] | 0x03 // 0000_0011
// Hole: [56 bits 0] | 0x05 // 0000_0101 (Engine-internal empty array/property slot)
// False: [56 bits 0] | 0x06 // 0000_0110
// True: [56 bits 0] | 0x07 // 0000_0111
// Exception: [56 bits 0] | 0x0A // 0000_1010
// OptimizedOut:[56 bits 0] | 0x12 // 0001_0010
JSTaggedValue (64-bit uint64_t) [ecmascript/js_tagged_value_internals.h]
|
+---> [Immediate Value (Encoded in 64-bit register, No Heap Allocation)]
| |-- TaggedInt : [0xFFFF][0x0000][32-bit signed int] (TAG_INT = 0xFFFF000000000000ULL)
| |-- TaggedDouble : IEEE-754 bits + 0x0001000000000000ULL (Top 16 bits in 0x0001..0xFFFE)
| |-- Boolean : 0x06 (VALUE_FALSE), 0x07 (VALUE_TRUE)
| |-- Undefined : 0x02 (VALUE_UNDEFINED)
| |-- Null : 0x03 (VALUE_NULL)
| +-- Engine Markers: 0x05 (VALUE_HOLE), 0x0A (VALUE_EXCEPTION), 0x12 (VALUE_OPTIMIZED_OUT)
|
+---> [Heap Pointer ((value & TAG_HEAPOBJECT_MASK) == 0, != 0x05 VALUE_HOLE, != 0x00)]
| // TAG_HEAPOBJECT_MASK = 0xFFFF000000000006ULL (Checks Top 16 bits == 0 and Bits[2:1] == 00).
| // Because 0x05 (0b0101) also has Bits[2:1] == 00, IsHeapObject() explicitly excludes 0x05 and 0x00.
|-- Strong Ref : [0x0000][48-bit 8B-aligned address] (Bit 0 == 0)
|-- Weak Ref : [0x0000][47-bit address | 0x1] (Bit 0 == 1, TAG_WEAK)
|
v
common::BaseObject [common_components/objects/base_object.h & common/type_def.h]
|
+---> TaggedObject [ecmascript/mem/tagged_object.h]
|-- [Offset 0: 64-bit ClassWord / StateWord] --> JSHClass* [ecmascript/js_hclass.h#L380]
| +-- JSType [ecmascript/js_hclass.h#L360]
|
+-- Subtypes of TaggedObject:
|-- ECMAObject -> JSObject [ecmascript/js_object.h#L366]
| (Standard JS/ArkTS objects, JSArray, JSFunction, JSSharedObject)
|-- EcmaString [ecmascript/ecma_string.h#L64]
| (In-place overlay of BaseString [common_components/objects/base_string.h]:
| LineString, SlicedString, TreeString, CachedExternalString)
|-- BigInt [ecmascript/js_bigint.h#L32]
|-- JSSymbol [ecmascript/js_symbol.h#L28]
+-- JSHClass [ecmascript/js_hclass.h#L380]
2.2 JS Object and JS HClass Memory Layout
- JSObject: actual runtime instance data and property values
- JSHClass: structural layout (hidden class) and type metadata
-
Multiple JSObject instances created with the same structure share a single JSHClass
- JSObject
+-------------------------------------------------------------------+ <-- Offset 0 (TaggedObject) | StateWord / ClassWord (8 bytes) | | Points to JSHClass* (plus GC marking / forwarding state bits) | +-------------------------------------------------------------------+ <-- Offset 8 (ECMAObject) | Hash / ECMAObject Word (8 bytes, JSTaggedValue) | | Immediate TaggedInt hashcode, or TaggedArray* [hash, NativePtr] | +-------------------------------------------------------------------+ <-- Offset 16 | Properties Pointer (8 bytes, JSTaggedValue) | | Points to TaggedArray (Out-of-line properties) or NameDictionary| +-------------------------------------------------------------------+ <-- Offset 24 | Elements Pointer (8 bytes, JSTaggedValue) | | Points to TaggedArray / MutantTaggedArray (Indexed elements) | +===================================================================+ <-- Offset 32 (JSObject::SIZE) | In-Object Property 0 (8 bytes, JSTaggedValue) | +-------------------------------------------------------------------+ <-- Offset 40 | In-Object Property 1 (8 bytes, JSTaggedValue) | +-------------------------------------------------------------------+ | ... (Count = ObjectSizeInWords - InlinedPropsStart in JSHClass) | +-------------------------------------------------------------------+ - JSHClass
+-------------------------------------------------------------------+ <-- Offset 0 | ClassWord (8 bytes) -> Points to the root Meta-JSHClass | +-------------------------------------------------------------------+ <-- Offset 8 | Proto (8 bytes, JSTaggedValue) | | Points to the prototype object (e.g., Foo.prototype or null) | +-------------------------------------------------------------------+ <-- Offset 16 | Layout (8 bytes, JSTaggedValue) | | Points to LayoutInfo (TaggedArray mapping Key -> Attr + Index) | +-------------------------------------------------------------------+ <-- Offset 24 | Transitions (8 bytes, JSTaggedValue) | | WeakJSTaggedValue or TransitionsDictionary for HClass tree | +-------------------------------------------------------------------+ <-- Offset 32 | Parent (8 bytes, JSTaggedValue) | | Weak reference to the previous JSHClass in the transition chain | +-------------------------------------------------------------------+ <-- Offset 40 | ProtoChangeMarker (8 bytes, JSTaggedValue) | | Cell guarding whether any prototype in the chain has mutated | +-------------------------------------------------------------------+ <-- Offset 48 | ProtoChangeDetails (8 bytes, JSTaggedValue) | | Tracks dependent child HClasses for Prototype IC invalidation | +-------------------------------------------------------------------+ <-- Offset 56 | EnumCache (8 bytes, JSTaggedValue) | | Cached property keys for `for..in` / `Object.keys` | +-------------------------------------------------------------------+ <-- Offset 64 | BitField (4 bytes, uint32_t) | | [0..7]: JSType (e.g., JS_OBJECT, LINE_STRING, JS_ARRAY, etc.) | | [8]: IsCallable | | [9]: IsConstructor | | [10]: IsExtensible | | [11]: IsPrototype | | [12..16]: ElementsKind (5 bits: Int, Number, Tagged, Hole...) | | [17]: IsDictionaryMode (Fast vs. Slow dictionary mode) | | [21]: IsTS / IsAOT (Static type / AOT pre-generated HClass) | | [22]: IsJSShared (True for @Sendable shared objects) | +-------------------------------------------------------------------+ <-- Offset 68 | BitField1 (4 bytes, uint32_t) | | [0..13]: ObjectSizeInWords (Total instance size in 8B words) | | [14..21]: InlinedPropsStart (Word offset where inline props beg)| | [22..31]: NumberOfProps (Total existing props, max 1023) | | * Note: InlinedProperties = ObjectSizeInWords - InlinedPropsStart| +-------------------------------------------------------------------+ <-- Offset 72 (JSHClass::SIZE) - JSObject and JSHClass
JSObject and JSHClass Layout (v6.1-LTS)
Properties JS Object JS HClass (72B)
+------------+ +------------+ +--------------------+
|arrayHClass | <---------+ | ClassWord | ------------> | Meta HClass | (0)
+------------+ | +------------+ +--------------------+
| property 0 | | | Hash | | Proto | (8)
+------------+ | +------------+ +--------------------+
| property 1 | +-------- | Properties | | Layout | (16)
+------------+ +------------+ +--------------------+
| ... | +-------- | Elements | | Transitions | (24)
+------------+ | +------------+ +--------------------+
| | inl-prop-0 | | Parent | (32)
Elements | +------------+ +--------------------+
+------------+ | | inl-prop-1 | | ProtoChangeMarker | (40)
|arrayHClass | <---------+ +------------+ +--------------------+
+------------+ | ... | | ProtoChangeDetails | (48)
| value 0 | +------------+ +--------------------+
+------------+ | EnumCache | (56)
| value 1 | +--------------------+
+------------+ | BitField/BitField1 | (64..72)
| ... | +--------------------+
+------------+
Proto: [[Prototype]] in ECMA spec
Layout: LayoutInfo recording property keys and attributes
ProtoChangeMarker / ProtoChangeDetails: monitor [[Prototype]] chain mutations for IC invalidation
EnumCache: cached enumerable keys for for-in / Object.keys
0x03 Hook Framework
3.1 How it works
- Inside arkcompiler_ets_runtime,
JSObject::DefineOwnPropertyoverwrites the slot and notifies the prototype change listener (ProtoChangeMarker / ProtoChangeDetails), which invalidates any existing Inline Caches (ICs) pointing to the old verify function.
3.2 What is not working
@Sendable**:Reflect.definePropertynot allowed for classes- Non-Configurable / Frozen NAPI Module Exports napi_default: writable: false, configurable: false
- Per-Module Import Bindings (ldexternalmodulevar) Callers that do import { openSync } from ‘@ohos.file.fs’ resolve the binding directly from the module export slot or a local binding copy.
- NAPI Factory Instances Without Exported Prototype
- Compiler Intrinsics & Built-in Fast Paths String, Array, Math, JSON
3.3 Implementation (Simplified)
type OriginalFn = (...args: Object[]) => Object;
type InstanceProxy = (original: OriginalFn, self: Object, args: Object[]) => Object;
function asRecord(v: Object): Record<string, Object> {
return v as unknown as Record<string, Object>;
}
function resolveMethodOnTarget(target: Object, method: string): OriginalFn | undefined {
try {
const direct = asRecord(target)[method];
if (typeof direct === 'function') {
return direct as OriginalFn;
}
} catch (e) {
}
return undefined;
}
function hookInstanceMethod(cls: Object, method: string, proxy: InstanceProxy): boolean {
const proto = asRecord(cls)['prototype'];
const original = asRecord(target)[method]
const wrapper = function (this: Object, ...args: Object[]): Object {
const self = this;
const boundOriginal: OriginalFn = function (this: Object, ...callArgs: Object[]): Object {
const targetThis = (this !== undefined && this !== null) ? this : self;
return Reflect.apply(original, targetThis, callArgs);
} as OriginalFn;
return proxy(boundOriginal, self, args);
};
// Override the original function to our wrapper
// Static method - Override proto instead of asRecord(proto)
Reflect.defineProperty(asRecord(proto), method, {
value: wrapper,
writable: true,
enumerable: true,
configurable: true
});
return true;
}