12.8. OAT containers: what’s actually AOT-compiled to native code, and where

Preinstalled apps (Boox, GMS, AOSP) catalogues the 124 preinstalled APKs but explicitly does not open their compiled Java. This page does: it looks inside the 308 .odex files under oat/<arch>/ (each with a matching .vdex sitting right there) plus 60 more boot-*.vdex files that live directly in system/framework/ rather than under any oat/ subdirectory — 368 .vdex files total — across system_a, vendor_a, product_a and system_ext_a, establishes what container format they actually are on this device, and disassembles the handful of places that hold genuine AArch64 machine code rather than interpreted DEX bytecode.

Key finding: almost none of it is native code. Of 308 .odex files, only 28 (9.1%) contain any AOT-compiled machine code at all — the rest are ELF containers whose “executable” region is literally absent, because ART’s build-time compiler filter for this image is verify for the overwhelming majority of apps (including every first-party Boox/Onyx app checked). The exceptions are System Server and a cluster of Mainline APEX system services, which are speed/speed-profile compiled and do hold megabytes of real compiled code — plus one vendor outlier, TrustZoneAccessService, whose only compiled code is six JNI trampolines bridging into Qualcomm’s Mink/QSEE IPC layer.

12.8.1. Container format, confirmed against raw bytes

An .odex under oat/<arch>/ is a real ET_DYN ELF shared object. Confirmed directly on this dump (not assumed from AOSP documentation):

$ xxd artifacts/system_a/system/app/knote2-release/oat/arm64/knote2-release.odex | head -1
00000000: 7f45 4c46 0201 0103 0000 0000 0000 0000  .ELF............
00000010: 0300 b700 0100 0000 0000 0000 0000 0000  ................

e_ident = 7f 45 4c 46 02 01 01 03 (ELF64, little-endian, EI_OSABI = 3/Linux); e_type = 3 (ET_DYN); e_machine = 0x00b7 = 183 = EM_AARCH64. Every oat/arm64/*.odex on this device (216 of the 308) is this format. The other 92 — the oat/arm/ 32-bit compatibility copies — are the ELF32/EM_ARM (40) equivalent of the same container; confirmed by extending the same parser to 32-bit Elf32_Phdr/Elf32_Sym layout (see Provenance).

Section layout (services.odex, the system_server OAT file, as a representative example — llvm-objdump -h):

Idx Name               Size     VMA              Type
  1 .note.gnu.build-id 00000024 00000000000003c0 DATA
  2 .rodata            00163000 0000000000001000 DATA
  3 .text              003c9968 0000000000164000 TEXT
  4 .data.img.rel.ro   000031cc 000000000052e000 DATA
  5 .bss               0000cd30 0000000000532000 BSS
  6 .dex               00048990 000000000053f000 BSS
  7 .dynstr / .dynsym / .hash / .dynamic   (standard dynamic-linking sections)

and its dynamic symbol table (no .symtab — these are stripped, but the dynsym table is intact and readable with llvm-objdump -T without needing to hand-walk PT_DYNAMIC):

0000000000001000 g  DO .rodata  0000000000163000  oatdata
0000000000164000 g  DO .text    0000000000000000  oatexec
000000000052d964 g  DO .text    0000000000000004  oatlastword
000000000052e000 g  DO .data.img.rel.ro  00000000000031cc  oatdataimgrelro
0000000000532000 g  DO .bss     0000000000007ce8  oatbss
000000000053f000 g  DO .dex     0000000000000000  oatdex
000000000058798c g  DO .dex     0000000000000004  oatdexlastword

oatdata marks the start of .rodata: the OAT header itself, the key/value metadata store, and per-dex-file tables. oatexec/oatlastword bound the actual compiled-native-code region — note oatexec’s own symbol size is always reported as 0 by the linker (it’s a boundary marker, not a sized object); the real size of the executable region has to be computed as oatlastword.value + oatlastword.size - oatexec.value. For services.odex that is 0x52d964 + 4 - 0x164000 = 0x3c9968 bytes (3,971,432 bytes) — matching the .text section size exactly. When an OAT file has zero compiled methods, the ``oatexec``/``oatlastword`` symbols are absent from the dynsym table entirely (not present-with-zero-size) — confirmed on every “verify”-filter file checked (see next section). oatdex marks the embedded/quickened DEX blob (.dex section, mapped SHT_NOBITS in some files — see the system_server table above where it’s typed BSS, meaning the on-disk .odex doesn’t even carry those bytes; they come from the paired .vdex at load time via mmap).

OAT header magic, confirmed in every file checked:

00001000: 6f61 740a 3234 3400 ...   oat\n244\0 ...

oat\n (4 bytes) + a NUL-terminated ASCII version string. Every file in this dump reports version 244 except four vendor-partition apps (IWlanService, TimeService, TrustZoneAccessService, pasrservice — all under artifacts/vendor_a/app/), which report version 183. This tracks the same Android-version split documented in The Onyx/Boox platform layer and Security and DRM userspace: the vendor partition is frozen on an older Android/ART release than system, and these four vendor apps were compiled by that older toolchain’s dex2oat. We did not locate or verify an authoritative OAT-version-to-AOSP-tag mapping for 183 vs. 244 — both numbers are read directly from the header bytes, not looked up.

Following the fixed header fields is a flat key/value store of NUL-terminated key\0value\0 pairs (apex-versions, bootclasspath, classpath-dir, compiler-filter, debuggable, …). We did not reverse-engineer the exact fixed-width OatHeader struct that precedes this store (checksum/instruction-set/trampoline-offset fields) — that layout is known to have changed repeatedly across AOSP art/runtime/oat.h revisions and pinning it for version 244 specifically was out of scope here. What we did instead, robustly and without needing the struct layout, is a direct byte search for the compiler-filter\0 key and read its value — see oat_scan.py in Provenance.

12.8.2. The compiler-filter reality

This is the load-bearing finding. compiler-filter is an ART build-time setting that controls how much of an APK’s DEX bytecode dex2oat actually turns into native machine code at build time, versus leaving to be interpreted (or quickened — bytecode rewritten in place with faster virtual-dispatch opcodes, still not native) at runtime. Scanning all 308 .odex files (script: oat_scan.py, see Provenance):

compiler-filter distribution (308 files):
  verify          277   (89.9%)  -- zero compiled methods, no oatexec symbol at all
  speed            24   ( 7.8%)  -- full AOT compilation, large oatexec
  quicken           4   ( 1.3%)  -- only JNI-native-method trampolines compiled
  speed-profile      3   ( 1.0%)  -- profile-guided partial AOT compilation

oatexec symbol present:  28 / 308  (9.1%)

verify means dex2oat did class/method verification only — no compilation whatsoever, not even the small JNI entry stubs. Every single one of the 277 verify-filter files we checked has no oatexec symbol in its dynamic symbol table (confirmed both by llvm-objdump -T spot checks and by the bulk scanner): the .text section, if a section-header table is even present, is size 0. This includes every first-party Onyx/Boox app in the dump that was checked: knote2-release (Notes), kreader2-release (Reader), kcb-release, ksync-release, kmail-release, floatingbutton-release, dict-release, ai-assistant-release, OnyxLatinIME-release, and SystemUI itself. None of these apps have a single AOT-compiled method on this device image — everything in them runs interpreted or quickened-interpreted at runtime, compiled on-device later (if at all) by the JIT/profile-guided background dex2oat pass that Android normally runs after enough real-world usage, which isn’t captured in a factory image dump.

speed / speed-profile (27 files total) is where the real compiled code lives, and it is concentrated almost entirely in system framework and Mainline APEX service jars — i.e., the platform’s own core services, not any app:

oatexec size   filter          file
9,352,200      speed           apex@com.android.wifi@...@service-wifi.jar@classes.odex
3,971,432      speed-profile   system/framework/oat/arm64/services.odex   (system_server)
3,676,216      speed           system/app/KeyChain/oat/arm64/KeyChain.odex
2,873,976      speed           apex@com.android.tethering@...@service-connectivity.jar@classes.odex
1,747,192      speed           apex@com.android.appsearch@...@service-appsearch.jar@classes.odex
1,692,168      speed           apex@com.android.uwb@...@service-uwb.jar@classes.odex
1,232,920      speed           apex@com.android.permission@...@service-permission.jar@classes.odex
  684,792      speed           priv-app/SettingsProvider/oat/arm64/SettingsProvider.odex
... (19 more apex service-*.jar entries, 9.2 KB - 918 KB each)
     36      speed           priv-app/InputDevices/oat/arm64/InputDevices.odex

(Full 28-row table with exact byte counts: /tmp/oat_scan_all.csv from the --find run described in Provenance — not checked into the repo since it’s a full re-derivable dump, but the command to reproduce it is given there.) Note the long tail down to InputDevices.odex at a mere 36 bytes — a single trivial compiled method (see below) sitting alongside gigabytes of interpreted-only DEX. KeyChain.odex is the one exception among apps (not framework/APEX services) with full speed compilation — plausible given it’s a boot-critical, security-sensitive system app, which (per general, unverified-for-this-device knowledge of Android’s compilation policy) tends to be prioritized for AOT compilation.

12.8.3. Disassembly: three real oatexec regions

All addresses below are file-relative (this ELF’s PT_LOAD segments are a 1:1 file-offset-equals-vaddr mapping, verified with elfdis.py --segs on each file — no relocation/rebasing needed). Disassembled with elfdis.py (capstone 5.0.7, AArch64) from artifacts/re_static/.

One artifact worth flagging up front: a raw linear disassembly starting exactly at the oatexec symbol’s address routinely decodes a few garbage/udf instructions before it locks onto real code. This isn’t a bug in the tooling — it’s a small non-instruction header (ART’s per-method OatQuickMethodHeader: a packed, variable-length-encoded record of vmap-table offset, CFI offset and frame info that precedes every compiled method’s entry point) sitting in the byte stream. capstone’s linear disassembler has no way to know to skip it, so it either decodes it as nonsense (udf #0 happens to be the AArch64 encoding for four zero bytes) or, worse, desyncs and stops entirely at the first byte sequence with no valid encoding. We did not reverse the exact OatQuickMethodHeader byte layout for OAT version 244; the practical workaround used throughout this section was to nudge the start address forward by 0x10–0x20 bytes past a run of zeros/garbage until capstone locks onto a recognizable AArch64 function prologue.

12.8.3.1. 1. services.odex (system_server, speed-profile) — first compiled method

Raw bytes at oatexec (0x164000):

00164000: 0000 0000 0000 0000 0000 0000 4ff4 0e00  ................

12 zero bytes (the tail of the method header) then non-instruction bytes, then at 0x164010 a completely ordinary ART “quick”-ABI method prologue:

0x164010  sub   sp, sp, #0xb0
0x164014  stp   x19, x20, [sp, #0x50]
0x164018  stp   x21, x22, [sp, #0x60]
0x16401c  stp   x23, x24, [sp, #0x70]
0x164020  stp   x25, x26, [sp, #0x80]
0x164024  stp   x27, x28, [sp, #0x90]
0x164028  stp   x29, x30, [sp, #0xa0]
0x16402c  stp   d8, d9,   [sp, #0x10]
0x164030  stp   d10, d11, [sp, #0x20]
0x164034  stp   d12, d13, [sp, #0x30]
0x164038  stp   d14, d15, [sp, #0x40]
0x16403c  str   x0, [sp]
...
0x16404c  mov   x16, sp
0x164050  str   x16, [x19, #0xa8]
0x164054  ldxr  w16, [x19]
0x164058  mov   w17, #0x5c000000
0x16405c  cbnz  w16, #0x164110
0x164060  stlxr w16, w17, [x19]
0x164064  cbnz  w16, #0x164054
...
0x164098  mov   x0, x22
0x16409c  ldr   x30, [x25, #0x10]
0x1640a0  blr   x30
0x1640a4  ldaxr w16, [x19]
0x1640a8  mov   w17, #0x5c000000
0x1640ac  cmp   w16, w17
0x1640b0  b.ne  #0x16411c
0x1640b4  stxr  w16, wzr, [x19]
0x1640b8  cbnz  w16, #0x1640a4
...
0x164104  ldr   w20, [x19, #0x20]
0x164108  add   sp, sp, #0xb0
0x16410c  ret

Register x19 is ART’s reserved “Thread self” pointer in the AAPCS64 “quick” calling convention (never used as a general-purpose scratch register by dex2oat-generated code). The body is an ldxr/stlxr compare-and-swap loop against [x19] (the Thread::tls32_.state_and_flags word) toggling between a runnable value and 0x5c000000 before and after an indirect call through x30 loaded from [x25, #0x10] — i.e. this is a thread-state transition to a “suspended/native” state, call out, then transition back. That is the textbook shape of a compiled JNI entry stub (the same pattern ART emits for GenericJNI/@FastNative native-method trampolines — see the TrustZoneAccessService example below for an unambiguous, name-attributed instance of the identical pattern). We cannot name which System Server method this specific instance belongs to without parsing the OAT method-offset table (not done — see Honest limits), but the instruction-level identity of the pattern itself is not in doubt.

12.8.3.2. 2. KeyChain.odex (speed) — GC write-barrier (“card mark”) idiom

At oatexec + 0x20000 (file offset 0x109010), a run of 5 back-to-back occurrences of the same 12-instruction idiom, each following a different object’s field store:

0x109040  adrp  x1, #0x472000
0x109044  ldr   w1, [x1, #0xc8c]
0x109048  cbz   w1, #0x109174
0x10904c  mov   x23, x0
0x109050  mov   x0, x1
0x109054  ldr   x30, [x19, #0x1d8]
0x109058  blr   x30
0x10905c  str   w24, [x0, #8]
0x109060  ldr   x16, [x19, #0x90]
0x109064  lsr   w17, w0, #0xa
0x109068  strb  w16, [x16, x17]
0x10906c  dmb   ishst

Two recognizable ART codegen idioms stacked here:

  • adrp x1,#0x472000 / ldr w1,[x1,#0xc8c] / cbz w1,<slow-path> is the standard “resolve-if-not-already-resolved” guard ART emits before touching a Class/String/static-field target: the pointer lives in the .data.img.rel.ro boot-image-relocation section (this device’s .data.img.rel.ro for KeyChain.odex spans 0x46b0000x46d384, comfortably containing 0x472000… actually this particular target falls just past that window, meaning it resolves into the boot image’s own relocatable data rather than this OAT file’s private table — consistent with referencing a boot-classpath class/string, not an app-private one); on a cache miss it calls a runtime helper through [x19, #0x1d8] (a QuickEntryPoints slot in the Thread struct) to resolve it the slow way.

  • str w24,[x0,#8] (a plain object-reference field store) immediately followed by ldr x16,[x19,#0x90] (load the GC card-table base pointer, cached in Thread), lsr w17,w0,#0xa (divide the stored-to address by 1024 to get a card index) and strb w16,[x16,x17] (mark that card dirty), then a dmb ishst store-store barrier. This is ART’s mandatory write barrier: every compiled store of a heap-reference-typed field has to dirty the corresponding card in the concurrent-GC’s card table, and the compiler always emits it inline rather than calling out. This exact five-times-in-a-row shape (5 consecutive field-store + card-mark blocks) is consistent with a compiled constructor or a readFields-style deserialization method initializing several reference fields in sequence, though which one specifically was not determined (same OAT-method-table caveat as above).

12.8.3.3. 3. service-wifi apex jar (speed) — a single trivial compiled method

At the very start of oatexec (0x213010, past the same header-garbage gap seen in example 1):

0x213010  str   w2, [x1, #0x10]
0x213014  cbz   w2, #0x213024
0x213018  ldr   x16, [x19, #0x90]
0x21301c  lsr   w17, w1, #0xa
0x213020  strb  w16, [x16, x17]
0x213024  dmb   ishst
0x213028  ret

The same card-mark idiom as example 2, but with the compiler’s null-check optimization visible: cbz w2, #0x213024 skips the card-mark entirely when the value just stored is null (a null reference can never point at a young/uncollected object, so marking the card would be wasted work) — a 7-instruction compiled setter, and (per the byte-range check against oatlastword) one of only a literal handful of compiled methods in this 12 MB APEX jar’s OAT file relative to its multi-megabyte DEX payload.

12.8.4. 4. TrustZoneAccessService.odex (vendor, quicken) — six JNI stubs, name-attributed

This is the one file in the whole scan where we could tie compiled code to named Java methods with confidence, because the paired .apk is small enough to extract its classes.dex directly and read the method table.

artifacts/vendor_a/app/TrustZoneAccessService/TrustZoneAccessService.odex reports compiler-filter=quicken (not speed) yet still has a nonzero oatexec — 1,752 bytes, from 0x2000 to 0x26d4. Under quicken, dex2oat compiles no regular Java-bytecode methods but does still compile a small, fixed-shape entry trampoline for every native-declared Java method, because there is no other way to call into JNI without one. Pulling classes.dex straight out of the APK (7z e ... classes.dex) and grepping its string pool confirms the class and its six native methods:

Lcom/qualcomm/qti/qms/service/trustzoneaccess/TZAccessService;
  nativeGetEnvHandle
  nativeGetOpenerHandle
  nativeInit
  nativeInvoke
  nativeIsNull
  nativeRelease

— six methods, and the disassembly of oatexec is exactly six back-to-back trampolines of near-identical shape (at 0x2000, 0x21a0, 0x22a0, 0x2390, 0x2480, 0x25e0 — each preceded by the same kind of header-garbage gap as example 1, each ending in a ret/slow-path pair). The count matching exactly is strong circumstantial evidence for “these six stubs are these six methods, in some order”; we did not establish the exact stub-to-name mapping (that needs the OAT method-offset table, not done here), but the structural finding — this file’s entire compiled-code footprint is JNI glue for a Mink/TrustZone IPC class, nothing else — stands on its own. One representative stub (nativeInit, most likely, given it takes the most arguments of the six — 6 registers pushed into the JNI local-ref frame vs. 1–5 for the others):

0x2010  sub   sp, sp, #0xd0
0x2014  stp   x19, x20, [sp, #0x70]
...                                    ; full quick-ABI register save
0x203c  str   x0, [sp]                 ; this/jclass
0x2040  str   w1, [sp, #0xd8]           ; spill JNIEnv-visible args
0x2044  stur  x2, [sp, #0xdc]
0x2048  str   w3, [sp, #0xe4]
0x204c  str   w4, [sp, #0xe8]
0x2050  str   w5, [sp, #0xec]
0x2054  str   w6, [sp, #0xf0]
0x2058  str   w7, [sp, #0xf4]
0x205c  mov   x16, #6                  ; JNI local-ref-table "6 refs" marker
0x2060  str   w16, [sp, #0x10]
0x2064  ldr   x16, [x19, #0x128]        ; Thread::tlsPtr_.jni_env / top handle scope
0x2068  str   x16, [sp, #8]
0x206c  add   x16, sp, #8
0x2070  str   x16, [x19, #0x128]        ; push new HandleScope/local-ref frame
...                                     ; per-arg: null stays null, else &stack-slot
0x20b0  mov   x0, x19
0x20b4  ldr   x16, [x0, #0x338]         ; QuickEntryPoints: JNI method-start helper
0x20b8  blr   x16
...
0x2120  ldr   x0, [x19, #0xd8]           ; ArtMethod* / down-call target
0x2124  ldr   x16, [sp, #0x10]
0x2128  ldr   x16, [x16, #0x18]          ; dlsym'd native fn pointer
0x212c  blr   x16                        ; call into libnative-api.so
0x2130  ldr   w0, [sp, #0x3c]
0x2134  mov   x1, x19
0x2138  ldr   x16, [x1, #0x350]          ; QuickEntryPoints: JNI method-end helper
0x213c  blr   x16
0x2144  ldr   x16, [x19, #0xa8]          ; check pending suspend/exception
0x2148  cbnz  x16, #0x2180               ; slow path -> brk #0 (deopt/exception trap)
0x214c  ldp   x19, x20, [sp, #0x70]
...                                      ; full register restore
0x217c  ret

This is the ART “GenericJNI” argument-marshalling/handle-scope pattern: push a JNI local-reference-table frame recorded in Thread ([x19, #0x128]), replace each non-null reference argument with a pointer to its own stack slot (a lightweight indirect reference so the native side can’t outlive the frame), call the JNI method-entry QuickEntryPoint ([x19,#0x338]), blr the actual native function pointer, call the JNI method-exit QuickEntryPoint ([x19,#0x350]/[x19,#0x368] — two different exit helpers appear across the six stubs, unconfirmed whether this is return-type dependent (reference-returning vs. primitive-returning)), then check for a pending suspend request/exception before restoring registers.

The dex string pool also contains the literal string native-api positioned immediately before the six nativeXxx method-name strings — consistent with being the argument to System.loadLibrary("native-api"). libnative-api.so does exist on this device, at artifacts/vendor_a/lib64/libnative-api.so (and a 32-bit copy in lib/) — confirming the target of these trampolines’ final blr, though that library’s own internals were not examined here (out of scope — this page is about the OAT container, not the Mink native implementation). The dex string pool additionally references com.qualcomm.qti.qms.api.mink. {CMinkObject,IMinkObject,JMinkObject} and com.qualcomm.qti.qms.api.minksocket.IMinkSocketFd — Qualcomm’s “Mink” IDL IPC framework, which is how privileged Android userspace processes invoke QSEE trustlets on this SoC family. This is the userspace-JNI edge of the same QSEECOM bridge Security and DRM userspace documents from the HAL/daemon side (qseecomd / libQSEEComAPI.so / vendor.qti.hardware.qseecom@1.0); TZAccessService is a second, separate (Mink-based rather than QSEECOM-ioctl-based) path into the same secure world, not previously catalogued on this page’s sibling doc.

12.8.5. The .vdex container (lower priority, not fully resolved)

.vdex files hold the original/quickened DEX bytecode plus a small quickening-info table — never native code. Header, confirmed on services.vdex (297,360 bytes):

00000000: 7664 6578 3032 3700 0400 0000 0000 0000  vdex027.........
00000010: 3c00 0000 0c00 0000 0100 0000 0000 0000  <...............
00000020: 0000 0000 0200 0000 4800 0000 3b49 0200  ........H...;I..

Magic is vdex027\0 (8 bytes) followed by several 32-bit fields we did not map to named VdexFile::VdexFileHeader struct members with confidence (same reasoning as the OAT header: the layout has shifted across AOSP versions and we had no source tree to check it against for “027” specifically). Structural confirmation that this is a DEX-bytecode container rather than something else: a plain byte search for Landroid/Ljava class-descriptor prefixes finds real hits starting around file offset 0x101** (e.g. ``Landroid/view/accessibility/ AccessibilityRecord; at 0x101x4), and immediately before that region is a table of steadily-increasing 4-byte little-endian offsets — the shape of a DEX string_ids table pointing forward into a string-data section.

Unexplained: a plain search for the literal bytes dex\n (the standalone-.dex file magic + version marker, e.g. dex\n035\0) returns zero matches anywhere in services.vdex, despite the file demonstrably containing real DEX structural data (string tables, type descriptors). Either this vdex format variant elides the per-embedded-dex magic/checksum/signature header (storing dex payloads as a shared, header-less blob addressed purely by the vdex header’s own offset/size fields) or the embedded dex bodies are processed in some way not identified here. The vdex section table was not fully parsed to resolve this.

12.8.6. Honest limits

  • No OAT method-offset table (the per-OatDexFile/per-class/per-method structures inside oatdata that map a DEX method_id to a code offset) was parsed. Every disassembly excerpt above is address-only — real, verified machine code at a real, verified file offset — but method name attribution was only possible for TrustZoneAccessService (by the 6-methods/6-stubs count match against its APK’s DEX, not by parsing OAT metadata), and even there the specific stub-to-method order is not established. A full OatQuickMethodHeader / OatMethodOffsets parser would be needed for exact per-method boundaries.

  • The fixed-width portion of OatHeader preceding the key/value store (checksum, instruction-set-features bitmap, trampoline offsets, etc.) was not mapped field-by-field; only the leading magic/version and the key/value store (found by direct string search, not by walking the struct) were used.

  • The .vdex per-dex-file section layout was not resolved (see above).

  • libnative-api.so (the JNI target of the TrustZoneAccessService stubs) was located but not analyzed — out of scope for an OAT-container page.

  • 92 of the 308 .odex files are the oat/arm/ 32-bit copies; the bulk scan covers them (ELF32 support was added to oat_scan.py), but no 32-bit file’s oatexec was individually hand-disassembled — spot checks of a few (org.apache.http.legacy.odex, com.android.location. provider.odex) via the compiler-filter/size columns alone show the same filter/size pattern as their arm64 siblings, so no separate 32-bit disassembly examples are included here.

12.8.7. Provenance

Source:

All 308 .odex files (each with a matching .vdex) under artifacts/{system_a,vendor_a,product_a,system_ext_a}/**/oat/**/, plus the 60 boot-*.vdex files directly under system_a/system/framework/ (368 .vdex total). Specific files quoted above: artifacts/system_a/system/framework/oat/arm64/services.odex, artifacts/system_a/system/app/KeyChain/oat/arm64/KeyChain.odex, artifacts/system_a/system/framework/oat/arm64/apex@com.android.wifi@ javalib@service-wifi.jar@classes.odex, artifacts/vendor_a/app/TrustZoneAccessService/oat/arm64/ TrustZoneAccessService.odex (+ its sibling .apk and .vdex in the same directory), artifacts/system_a/system/app/knote2-release/oat/ arm64/knote2-release.odex and the other Onyx-app odex files named in The compiler-filter reality, and artifacts/system_a/system/framework/oat/arm64/services.vdex.

Method:
  • Container identification: raw xxd/Python struct reads of e_ident/e_machine/e_type and the oat\n magic; cross- checked against llvm-objdump -h/-T (pixi LLVM 23.1.1) which independently confirms the section table and dynamic symbol table.

  • Bulk survey of all 308 files: artifacts/re_static/oat_scan.py (written for this task, saved for reuse) — walks PT_DYNAMIC -> DT_SYMTAB/DT_STRTAB/DT_HASH by hand (no reliance on a section-header table, works on stripped ELFs) to recover oatdata/oatexec/oatlastword and computes real oatexec size from symbol values; separately byte-searches the first 32 KiB for the compiler-filter\0 key. Handles both ELF64/EM_AARCH64 ( oat/arm64/) and ELF32/EM_ARM (oat/arm/). Reproduce with: python3 artifacts/re_static/oat_scan.py --csv --find artifacts > /tmp/oat_scan_all.csv (takes a few seconds; no external dependencies beyond the standard library).

  • Disassembly: artifacts/re_static/elfdis.py (pre-existing tool, reused as-is) with system /usr/bin/python3 + capstone 5.0.7 (the pixi python lacks capstone, so the system interpreter was used instead).

  • TrustZoneAccessService method-name attribution: 7z e TrustZoneAccessService.apk classes.dex, then strings classes.dex grepped for the class’s method-name string-pool entries (no DEX decompiler used — this was a raw string-pool read, not a jadx-style decompile).

  • No code was executed; no OAT/DEX/VDEX file was mutated. All figures (counts, sizes, offsets) are re-derivable from the commands given above against the artifacts as they sit in this checkout.

Cross-refs:

Preinstalled apps (Boox, GMS, AOSP) (the app catalogue this page opens up), The Onyx/Boox platform layer (the Android-version split between system and vendor that explains the OAT-version-183-vs-244 divide), Security and DRM userspace (the QSEECOM/Mink bridge to TrustZone that TrustZoneAccessService is a second instance of), Userspace boot — init, SELinux, A/B updates (OnyxOtaService, decompiled separately by DEX source rather than by this page’s OAT-container approach — its own .odex/.vdex AOT copy was not separately examined there, and is covered by this page’s bulk scan: compiler-filter=verify, no compiled code, like the other Onyx apps above).