See every byte of an Android VDEX file. Parse, explain, extract, diff, and patch — from the CLI, as a Go library, or in the browser via WebAssembly.
한국어 문서: README.ko.md
$ vdexcli explain app.vdex
offset size type path value
---------- ----- ----------- ----------------------------------- ---------------------------
0x00000000 4 magic vdex.header.magic "vdex"
0x00000004 4 bytes vdex.header.version "027\0"
0x00000008 4 uint32_le vdex.header.num_sections 4
0x0000000c 12 bytes vdex.section[0].header kind=0 off=0x3c size=4
0x00000018 12 bytes vdex.section[1].header kind=1 off=0x40 size=112
...
0x000000cc 0 padding <gap> —
coverage: 204/204 bytes (100.0%) — all bytes explained
- What is VDEX?
- Features
- Quick Start
- CLI Usage
- Go Library API
- WebAssembly Engine
- Browser Demo
- Install
- Project Structure
- CI / Workflows
- Testing
- VDEX v027 Format Reference
- Contributing
VDEX (Verified DEX) is a binary container produced by the Android ART runtime during dexpreopt. It wraps one or more DEX files together with:
- Verifier dependency data — which classes ART verified and against what type hierarchy
- Checksum table — CRC32 fingerprint for each embedded DEX
- Type lookup table — hash table for fast class descriptor lookups
VDEX files live at /data/dalvik-cache/ or inside .dm (.vdex-carrying DM format) on-device files. Understanding their structure is essential for:
- Auditing
dexpreoptoutput in AOSP builds - Researching ART verification behavior
- Building custom class-loader tooling
| Category | Capability |
|---|---|
| CLI | parse, explain, extract-dex, modify, diff, dump |
| Output formats | text, json, jsonl, summary, sections, coverage, table |
| Byte-level explain | Every byte mapped to a named, typed, annotated field |
| DEX decomposition | string_ids, type_ids, proto_ids, field_ids, method_ids, class_defs individually resolved |
| Go library | pkg/vdex — importable public API (ExplainBytes, ParseBytes, ...) |
| WebAssembly | wasm/ — runs in any browser, zero server-side dependencies |
| Browser demo | Drag-and-drop VDEX analyzer at demo/index.html |
| AOSP-compatible | Section-absolute offsets matching ART runtime encoding |
| Diagnostics | 34 error/warning codes with actionable hints |
| Strict mode | Pattern-filtered warnings treated as fatal for CI gating |
| Verifier patching | Replace or merge verifier-deps via JSON |
| DEX extraction | Pull embedded DEX files with customizable filename templates |
| Diff | Structural comparison; exit 0 = identical, 1 = different |
# Install
go install github.com/0xc0de1ab/vdexcli@latest
# Parse — full structural dump
vdexcli parse app.vdex
# Explain — byte-level annotated field map
vdexcli explain app.vdex
# Query a specific byte offset
vdexcli explain --offset 0x3c app.vdex
# Extract embedded DEX files
vdexcli extract-dex app.vdex ./dex-out/
# Compare two builds
vdexcli diff before.vdex after.vdex
# Patch verifier dependencies
vdexcli modify --verifier-json patch.json in.vdex out.vdexParse the complete VDEX structure and display it in the chosen format.
vdexcli parse app.vdex # human-readable text (default)
vdexcli parse --json app.vdex # pretty-printed JSON
vdexcli parse --format jsonl app.vdex # compact single-line JSON
vdexcli parse --format summary app.vdex # one-line key=value for CI
vdexcli parse --format sections app.vdex # TSV section table
vdexcli parse --format coverage app.vdex # byte coverage only
vdexcli parse --format table --color never app.vdex # aligned table
vdexcli parse --strict --strict-warn "re:(checksum|version)" app.vdexSample output (text):
file: app.vdex
size: 204 bytes
vdex magic="vdex" version="027" sections=4
sections:
kind=kChecksumSection (0) off=0x3c size=0x4
kind=kDexFileSection (1) off=0x40 size=0x70
kind=kVerifierDepsSection (2) off=0xb0 size=0x1c
kind=kTypeLookupTableSection (3) off=0xcc size=0x0
checksums: 1
[0]=0xcafebabe
dex files: 1
[0] off=0x40 size=0x70 magic="dex\n" ver="035" endian=little-endian file_size=112
sha1=0000000000000000000000000000000000000000
strings=0 types=0 protos=0 fields=0 methods=0 class_defs=3
verifier_deps: off=0xb0 size=0x1c
[dex 0] verified=2 unverified=1 pairs=1 extra_strings=0
byte_coverage: 204/204 bytes (100.0%)
Batch pipeline:
for f in *.vdex; do vdexcli parse --format summary "$f"; done
# status=ok file=base.vdex size=524288 version=027 coverage=100.0% gaps=0Map every byte of the VDEX file to a named, typed, annotated primitive field. All bytes are accounted for — including padding and gaps.
vdexcli explain app.vdex # hex-dump table (text)
vdexcli explain --format json app.vdex # full PrimitiveMap as JSON
vdexcli explain --offset 0x3c app.vdex # field at specific byte offset
vdexcli explain --offset 60 app.vdex # decimal offset also supported
vdexcli explain --offset 0x3c --json app.vdex # single field as JSONDEX table decomposition — each DEX embedded in the VDEX is decomposed into its constituent tables:
vdex.dex[0].string_ids[0].offset → uint32_le 0x00000070
vdex.dex[0].type_ids[0] → uint32_le 0x00000005
vdex.dex[0].proto_ids[0].shorty_idx → uint32_le 0x00000001
vdex.dex[0].field_ids[0].class_idx → uint16_le 0x0000
...
Extract all embedded DEX files from a VDEX container.
vdexcli extract-dex app.vdex ./dex-out/
vdexcli extract-dex --json app.vdex ./out/
vdexcli extract-dex --extract-name-template "{base}_{index}_{checksum_hex}.dex" app.vdex ./out/
vdexcli extract-dex --extract-continue-on-error app.vdex ./out/Template tokens: {base}, {index}, {checksum}, {checksum_hex}, {offset}, {size}
Patch the verifier-deps section via a JSON descriptor.
# Replace mode (default) — rebuild entire verifier section
vdexcli modify --verifier-json patch.json in.vdex out.vdex
# Merge mode — overlay on top of existing data
vdexcli modify --mode merge --verifier-json patch.json in.vdex out.vdex
# Dry run — validate patch without writing output
vdexcli modify --dry-run --json --verifier-json patch.json in.vdex out.vdex
# Read patch from stdin
cat patch.json | vdexcli modify --verifier-json - in.vdex out.vdexPatch JSON schema:
{
"mode": "replace",
"dexes": [{
"dex_index": 0,
"extra_strings": ["Ljava/lang/Object;"],
"classes": [
{"class_index": 0, "verified": true, "pairs": [{"dest": 5, "src": 10}]},
{"class_index": 1, "verified": false}
]
}]
}See samples/ for example patch files.
Structurally compare two VDEX files. Exit code: 0 = identical, 1 = different.
vdexcli diff before.vdex after.vdex # text with color
vdexcli diff --json before.vdex after.vdex # JSON diff
vdexcli diff --format summary a.vdex b.vdex # CI-friendly one-linerExample:
VDEX diff
A: before.vdex (204 bytes)
B: after.vdex (204 bytes)
verifier_deps: (2 classes changed)
[dex 0] verified 2→0 (-2) pairs 1→0 (-1) extras 0→0
summary: sections=0 checksums=0 dexes=0 verifier=2 typelookup=0
Print the field-meaning dictionary embedded in the binary.
vdexcli dump # YAML meanings
vdexcli dump --format jsonl # JSON for piping| Flag | Description |
|---|---|
-i, --in <path> |
Input VDEX path (alternative to positional argument) |
--format <mode> |
text | json | jsonl | summary | sections | coverage | table |
--json |
Shorthand for --format json |
--color <mode> |
auto (default) | always | never |
--strict |
Treat matched warnings as fatal (non-zero exit) |
--strict-warn <patterns> |
Comma-separated filters; prefix re: for regex |
--show-meaning |
Include field descriptions (default: true) |
--extract-dex <dir> |
Extract DEX files during parse |
--extract-name-template |
Filename template (default: {base}_{index}_{checksum}.dex) |
--extract-continue-on-error |
Skip failures and continue extracting |
-v, --version |
Print version and exit |
vdexcli exposes a stable public API in pkg/vdex that other Go projects can import directly.
import "github.com/0xc0de1ab/vdexcli/pkg/vdex"// Byte-level annotated field map — every byte is accounted for.
// WASM-compatible (no filesystem access).
fm, err := vdex.ExplainBytes(data []byte) (*vdex.FieldMap, error)
// High-level structural report.
// WASM-compatible.
report, err := vdex.ParseBytes(data []byte, opts ...vdex.Option) (*vdex.Report, error)
// Convenience wrappers for non-WASM (desktop/server) builds.
fm, err := vdex.ExplainFile(path string) (*vdex.FieldMap, error)
report, err := vdex.ParseFile(path string, opts ...vdex.Option) (*vdex.Report, error)vdex.WithMeanings() // include human-readable field descriptions
vdex.WithDexPreview() // include DEX class preview in report// Field — a single annotated byte range.
type Field struct {
Offset uint32
Size uint32
Type FieldType // "uint32_le", "magic", "bytes", "padding", ...
RawBytes []byte
ParsedValue interface{}
LogicalPath string // e.g. "vdex.dex[0].string_ids[3].offset"
Summary string
Description string
}
// FieldMap — complete annotated view of the VDEX file.
type FieldMap struct {
Fields []*Field
TotalBytes uint32
UnmappedGaps []ByteRange
}
// Report — high-level parsed result.
type Report struct {
File string
Header *VdexHeader
Sections []SectionHeader
Checksums []uint32
DexFiles []DexInfo
VerifierDeps *VerifierDepsInfo
TypeLookup *TypeLookupInfo
Coverage *CoverageReport
Diagnostics []Diagnostic
}package main
import (
"fmt"
"os"
"github.com/0xc0de1ab/vdexcli/pkg/vdex"
)
func main() {
data, _ := os.ReadFile("app.vdex")
// Byte-level explain
fm, err := vdex.ExplainBytes(data)
if err != nil {
panic(err)
}
fmt.Printf("Total bytes: %d, Fields: %d\n", fm.TotalBytes, len(fm.Fields))
for _, f := range fm.Fields[:5] {
fmt.Printf("0x%08x %-12s %s\n", f.Offset, f.Type, f.LogicalPath)
}
// High-level parse
report, _ := vdex.ParseBytes(data, vdex.WithMeanings())
fmt.Printf("VDEX version: %d, DEX count: %d\n",
report.Header.Version, report.Header.DexCount)
}The wasm/ package compiles the full engine to WebAssembly using the standard Go toolchain (GOOS=js GOARCH=wasm). No TinyGo or code-gen hacks required.
GOOS=js GOARCH=wasm go build \
-trimpath -ldflags="-s -w" \
-o vdex.wasm ./wasm/
# Or via Makefile:
make demoOutput: vdex.wasm (~3.3 MB stripped)
// Load runtime + WASM
const go = new Go(); // from wasm_exec.js
const result = await WebAssembly.instantiateStreaming(fetch("vdex.wasm"), go.importObject);
go.run(result.instance);
// Analyze a VDEX file (FileReader → Uint8Array → WASM)
const bytes = new Uint8Array(await file.arrayBuffer());
// Byte-level field map → { fields: [...], dex_previews: [...], total_bytes: N, unmapped_gaps: [...] }
const fieldMap = window.vdex.explain(bytes);
// High-level structural report → { header: {...}, dex_files: [...], ... }
const report = window.vdex.parse(bytes);
// Engine version string
console.log(window.vdex.version); // "v0.1.0"| Function | Input | Output |
|---|---|---|
window.vdex.explain(Uint8Array) |
Raw VDEX bytes | { fields, dex_previews, total_bytes, unmapped_gaps } or { error } |
window.vdex.parse(Uint8Array) |
Raw VDEX bytes | VdexReport object or { error } |
window.vdex.version |
— | Version string |
Both functions are synchronous — no async/await needed on the JS side.
A fully client-side VDEX analyzer is included in demo/. No server required — the engine runs entirely inside the browser via WebAssembly.
# 1. Build the WASM binary and copy wasm_exec.js
make demo
# or: bash demo/build_demo.sh
# 2. Serve (any static file server works)
make demo-serve
# → http://localhost:8080
# Manual alternative:
cd demo && python3 -m http.server 8080- Drag-and-drop or file picker to load any
.vdexfile - Explain tab — complete byte-level field table with:
- Offset, size, type badge, logical path, decoded value, hex preview
- Filterable by path prefix and field type
- Padding toggle, JSON copy button
- Click any row for a full detail modal with complete hex dump
- Parse tab — high-level structural summary cards (header, sections, DEX files, verifier deps, coverage)
- Coverage bar — animated byte coverage indicator
- 100% client-side — no file uploads, no analytics, no dependencies
Note: Browsers require a proper HTTP server for WASM (
fetch+Content-Type: application/wasm). Openingindex.htmldirectly viafile://will not work.
Every warning and error carries a structured diagnostic with severity, code, message, and hint.
Text output:
section warnings (1):
! section kDexFileSection has zero size
~ this section is empty; normal for DM-format VDEX (no embedded DEX)
verifier warnings (1):
! dex 0: inferred class_def_count=246 from verifier section (DM format)
~ no embedded DEX; class count inferred from offset table heuristic
JSON output:
{
"severity": "warning",
"category": "section",
"code": "WARN_SECTION_ZERO_SIZE",
"message": "section kDexFileSection has zero size",
"hint": "this section is empty; normal for DM-format VDEX (no embedded DEX)"
}34 diagnostic codes covering: truncated files, invalid magic, corrupted sections, broken LEB128, legacy version guards, type-lookup failures, and more.
CI integration:
# Fail only on checksum/verifier issues; ignore structural warnings
vdexcli parse --strict --strict-warn "re:(checksum|verifier)" app.vdex
# Extract all errors from JSON output
vdexcli parse --json app.vdex | jq '[.diagnostics[] | select(.severity == 0)]'
# Batch scan
for f in *.vdex; do
errs=$(vdexcli parse --json "$f" | jq '[.diagnostics[] | select(.severity==0)] | length')
[ "$errs" -gt 0 ] && echo "FAIL $f ($errs errors)"
donego install github.com/0xc0de1ab/vdexcli@latestgit clone https://github.com/0xc0de1ab/vdexcli.git
cd vdexcli
make build # → build/<os>-<arch>/release/vdexcliAvailable Make targets:
| Target | Description |
|---|---|
make all |
fmt + vet + lint + test + build |
make build |
Release binary for current OS/arch |
make test |
Run full test suite |
make lint |
golangci-lint |
make fmt |
gofmt in-place |
make vet |
go vet + mod tidy check |
make demo |
Build WASM demo artifacts |
make demo-serve |
Build demo and serve at localhost:8080 |
make clean |
Remove build artifacts |
Download from GitHub Releases:
# Verify checksum
sha256sum -c vdexcli-checksums.txtAvailable platforms: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64
make build VARIANT=debugvdexcli/
├── main.go # Entry point (7 lines)
├── Makefile # Build, test, demo targets
├── cmd/ # Cobra CLI commands
│ ├── root.go # Root command + global flags
│ ├── parse.go # parse subcommand
│ ├── explain.go # explain subcommand
│ ├── extract.go # extract-dex subcommand
│ ├── modify.go # modify subcommand
│ ├── diff.go # diff subcommand
│ ├── dump.go # dump subcommand
│ └── version.go # version subcommand
├── pkg/
│ └── vdex/ # ★ Public Go library API
│ ├── api.go # ExplainBytes, ParseBytes
│ ├── api_fs.go # ExplainFile, ParseFile (non-WASM)
│ ├── types.go # Field, FieldMap, Report, ...
│ ├── options.go # WithMeanings, WithDexPreview
│ └── doc.go # Package documentation
├── wasm/
│ └── main.go # ★ WebAssembly entry point (syscall/js)
├── demo/ # ★ Browser-based VDEX analyzer
│ ├── index.html # Single-page app
│ ├── style.css # Dark glassmorphism UI
│ ├── script.js # FileReader → WASM bridge + renderer
│ └── build_demo.sh # Build WASM + copy wasm_exec.js
├── internal/
│ ├── binutil/ # Low-level binary I/O (ReadU32, LEB128, ...)
│ ├── model/ # Shared types, constants, diagnostics
│ ├── dex/ # DEX format parsing (VDEX-independent)
│ ├── parser/ # VDEX container parsing
│ │ ├── explain.go # ExplainVdexBytes — byte-level field map
│ │ ├── explain_dex.go # DEX table decomposition
│ │ ├── parser.go # ParseVdexBytes — structural report
│ │ ├── verifier.go # VerifierDeps parsing
│ │ ├── typelookup.go # TypeLookup table parsing
│ │ └── legacy.go # Legacy VDEX v021-v026 support
│ ├── modifier/ # Verifier section build/patch/compare
│ ├── extractor/ # DEX file extraction
│ └── presenter/ # Output formatting + ANSI color
│ ├── color_terminal.go # Terminal color support (!js build)
│ └── color_wasm.go # No-op overrides for WASM build
├── .github/
│ └── workflows/
│ ├── ci.yml # [01] CI Build and required gate
│ ├── bump-up.yml # [02] Version tag creation
│ ├── release.yml # [03] Release pipeline
│ ├── pages-deploy.yml # [04] Web/WASM Pages deployment
│ └── test-integration.yml # [05] 166-file integration test
├── docs/
│ ├── architecture.md # Package diagram + design decisions
│ └── vdex-format.md # VDEX v027 binary format reference
├── samples/ # Example verifier patch JSON files
├── testdata/ # Real VDEX files for integration tests
└── ROADMAP.md # Phased expansion plan
Triggers: push to main, pull requests targeting main, and manual dispatch (workflow_dispatch).
| Job | What it checks |
|---|---|
fmt |
gofmt formatting |
vet |
go mod tidy drift · go mod verify · go vet ./... · GOOS=js GOARCH=wasm go vet ./wasm/ |
lint |
golangci-lint (staticcheck, errcheck, ...) |
vulncheck |
govulncheck — CVE scan |
test |
go test -v -count=1 ./... · coverage ≥ 85% across all packages including pkg/vdex/ |
build |
5-platform matrix (linux/darwin/windows × amd64/arm64) + linux/amd64 smoke test |
build-wasm |
GOOS=js GOARCH=wasm build · uploads vdex.wasm artifact |
[01] Required |
Requires every CI job above to succeed; use this as the branch-protection check |
Manual trigger: GitHub → Actions → [01] CI Build and test → Run workflow
Manual workflow that validates a vMAJOR.MINOR.PATCH version, creates the tag from the current protected main, and triggers the release pipeline.
Triggers: version tag v*.*.* or workflow_dispatch.
Produces: per-platform archives + SHA256 checksums + GitHub Release.
Builds the Go WASM engine and Node 24 web app, runs the WASM bridge smoke test, and deploys the version-matched engine/runtime pair to GitHub Pages.
Triggers: weekly (Monday 00:00 UTC), push to main, workflow_dispatch.
Runs: 166 real Android 16 VDEX files through the full parser pipeline.
go test -v ./...
# or
make test| Package | Tests | Notes |
|---|---|---|
cmd |
35 | E2E subprocess tests for all commands and formats |
internal/binutil |
18 | LEB128, alignment, encoding round-trips (100% coverage) |
internal/parser |
51+ | Header, sections, verifier, typelookup, byte-level explain |
internal/modifier |
30 | Patch parse/validate/build, atomic write |
internal/extractor |
9 | Mock filesystem, interface verification |
pkg/vdex |
14 | Public API stability, option validation, type aliases |
| Integration | 166 | Real Android 16 VDEX files (android-16.0.0_r4) |
Coverage gate: ≥ 85% across testable packages (enforced in CI).
Based on AOSP ART runtime/vdex_file.h:
Offset Size Field
------ ---- -----
0x00 4 magic "vdex"
0x04 4 version "027\0"
0x08 4 num_sections (= 4)
0x0c N×12 VdexSectionHeader[N]
├ kind uint32 (0=Checksum, 1=Dex, 2=VerifierDeps, 3=TypeLookup)
├ offset uint32 section start (absolute)
└ size uint32 section length in bytes
Section 0 kChecksumSection uint32[D] — CRC32 per embedded DEX
Section 1 kDexFileSection Concatenated DEX payloads (empty in DM format)
Section 2 kVerifierDepsSection Per-DEX verification dependency data
Section 3 kTypeLookupTableSection Per-DEX class descriptor hash tables
Full field-level reference: docs/vdex-format.md
Measured on a single core (arm64), single VDEX file:
| File size | Parse time | Throughput |
|---|---|---|
| 204 B | ~4 ms | instant |
| 5.6 KB | ~4 ms | instant |
| 178 KB (28 DEX) | ~13 ms | ~14 MB/s |
Batch: 166 files in 1.4 seconds (including DM format class inference).
This project is licensed under the MIT License.