A V8 bytecode disassembler & decompiler (In Progress)
blog (chinese only): a-quick-guide-to-disassemble-v8-bytecode
- disassembler
- version brute force
- checksum rewrite: allow modify bytecode
- rewrite header
- decompiler
- make it works for electron :(
- some bug about
- checkversion: standalone bytecode version bruteforce
- v8patch: v8 patches
- disassembler: standalone Python cached-bytecode disassembler; it does not load or link V8 at runtime
- decompiler: decompiler (in progress)
- ghidra/v8-bytecode: experimental Ghidra SLEIGH processor module for V8 Ignition bytecode
checkversion reads the uint32 version hash from a .jsc header and searches
the V8 version space with worker processes. It defaults to at most four workers
because the search is CPU-bound; increase it explicitly when appropriate:
python3 -m checkversion input.jsc
python3 -m checkversion --hash 0x2b2c7714 -j 10
python3 -m checkversion --calculate 13.6.233.10Use --major-range, --minor-range, --build-range, and --patch-range to
narrow the search. Ranges use START:END with an exclusive end. See
checkversion/readme.md for examples.
disassembler parses the V8 code-cache header and serializer stream directly.
Runtime use requires only Python 3 and the checked-in per-version JSON profiles:
python3 -m disassembler input.jsc > /tmp/input.disasm.txt
python3 decompiler/v8decompiler.py /tmp/input.disasm.txt --level 4 --runtimeThe cache header normally selects the exact V8 profile. For a custom build with an unknown version hash, select a known matching bytecode layout explicitly:
python3 -m disassembler input.jsc --version 13.4.114.21Profiles cover V8 10.2, 10.8, 11.3, 11.4, 11.9, 12.4, 12.9, 13.2, 13.4,
and 13.6 tags used by the test matrix. Opcode mappings, serializer tags,
BytecodeArray/SFI/ScopeInfo layouts, runtime IDs, intrinsics, roots, and jump
semantics are stored under disassembler/profiles/. Regenerate them from an
official V8 checkout without changing its current branch:
python3 -m disassembler.generate_profiles \
--v8-repo /home/aynakeya/workspace/tmp/v8test/v8 \
--output-dir disassembler/profilesSnapshot-only read-only objects cannot always be named from a .jsc file
alone. Pass the matching startup snapshot to resolve sequential one-byte and
two-byte strings directly from its read-only heap image:
python3 -m disassembler atom.compiled.dist.jsc \
--version 13.4.114.21 \
--snapshot-blob v8context/v8_context_snapshot.bin \
> /tmp/atom.disasm.txtThe reader validates the V8 version, snapshot magic, and read-only checksum.
It supports the page-image formats used by V8 11.9 through 13.6, including
static-roots and relocatable snapshots. Without --snapshot-blob, unresolved
objects remain explicit <read_only_<space>,<offset>> values instead of being
omitted or guessed. See
note/disassembler-standalone-2026-07-15.md for architecture and
validation results.
For an Electron application that transforms .jsc files before V8 sees them,
preload disassembler/capture_cached_data.cjs to save the exact input and a
runtime-normalized cache. The disassembler also accepts a known raw serializer
payload through --payload-offset. See disassembler/README.md for the
capture workflow, Windows commands, output files, and limitations.
python3 decompiler/v8decompiler.py samples/main.d8.jsc.txt --level 1
python3 decompiler/v8decompiler.py samples/main.d8.jsc.txt --level 2
python3 decompiler/v8decompiler.py samples/main.d8.jsc.txt --level 3
python3 decompiler/v8decompiler.py samples/main.d8.jsc.txt --level 4 --runtime- level 1: linear, bytecode-aligned listing (best for reverse mapping to offsets).
- level 2: CFG structured output (
if/while), keeps most low-level operations. - level 3: level 2 + conservative simplification (register propagation, safer readability).
- level 4: level 3 + high-level pattern recovery (e.g. iterator state
machine ->
for...of,+=folding, string-concat return folding, bound method calls, dead temporary register cleanup, local context-slot closure name recovery).
./tests/decomp_rounds/run_round.shThe round tests compile each case with local v8asm and bytenode, disassemble
both outputs, run the level-4 Python decompiler, and write
tests/decomp_rounds/summary.md. The summary tracks low-level residue
(ACCU, register refs, raw gotos), missing translator coverage (unknown), and
best-effort object-print placeholders (undefined_fallbacks). It also counts
unique unresolved object-print failures from the disassembly
(unresolved_objects) and lists their low address suffixes plus
object_chunk_offsets and current_ro_objects when the selected v8asm
prints them. These offsets are more useful than full heap addresses because the
address base moves between runs. current_ro_objects shows whether a failed
address lands at a current read-only heap object start or inside another object,
which is useful for separating missing print guards from real snapshot layout
mismatches. The summary also compares bytenode placeholder constants with the
same case's self-cache constants and prints Bytenode Placeholder Name Hints;
use those hints to identify likely RO-heap object names while debugging
snapshot recovery, not as a Python-side substitution source. The companion
Bytenode Placeholder Offset Summary groups those hints by
object_chunk_offset, which is the stable key to use when comparing failed
objects across cases and runs. Cached-data header mismatches, including
ro_snapshot, are recorded so missing bytenode object/property names can be
tied back to V8/Node snapshot recovery instead of being mistaken for Python
translator loss. The report records the exact v8asm, Node, Node V8, and
bytenode versions used for that run.
By default, bytenode mode uses Node 24.7.0 through nvm. Override the binary or
Node version explicitly when validating another target:
V8ASM_BIN=/path/to/v8/out/v8asm.12.9.x64.release/v8asm \
ROUND_NODE_VERSION=22.17.0 \
./tests/decomp_rounds/run_round.shIf the Node V8 version does not match v8asm, bytenode rows are
forced-incompatible research coverage, not proof that the V8 branch is a native
match. Per-case *.checkversion.txt files are written under
tests/decomp_rounds/out/.
For a lighter compatibility check across available v8asm binaries and local
nvm Node versions, run:
./tests/decomp_rounds/run_version_matrix.shThis writes tests/decomp_rounds/version_matrix/summary.md. The matrix uses
one case by default, verifies self-generated v8asm cache strictly, and checks
bytenode cache against each v8asm. It requires --force-incompatible when
the Node V8 numeric version and pointer-compression layout both match, and skips
direct forced loads for numeric or pointer-layout mismatches by default. Set
VERSION_MATRIX_FORCE_MISMATCH=1 only for an explicit research probe. The
default Node set is 18.20.8, 20.20.2, 22.17.0, and 24.7.0 when those versions
are installed through nvm.
The round runner follows the same snapshot convention as the matrix: if the
selected V8ASM_BIN has a sibling snapshot_blob.bin, commands are launched as
v8asm --snapshot_blob <sibling> .... Set ROUND_SNAPSHOT_BLOB=/path/to/blob
to force a specific snapshot for bytenode checkversion and forced disasm
probes. In that mode, checkversion is invoked with --force-incompatible so
the expected read-only snapshot checksum is computed after loading the supplied
startup blob.
Important snapshot cache rule: a cached v8asm binary's sibling
snapshot_blob.bin must be the snapshot_blob.bin generated by that exact V8
build output directory. Do not place Electron, Node, Chromium, or app-provided
snapshots next to the cached v8asm binary. Keep those snapshots in a separate
location and pass them explicitly with ROUND_SNAPSHOT_BLOB,
VERSION_MATRIX_SNAPSHOT_BLOB, or v8asm --snapshot_blob ... for the forced
probe being tested. Electron packages can contain both snapshot_blob.bin and
v8_context_snapshot.bin; test both when validating Electron-generated
bytenode caches because the cached-data read-only checksum can match the
context snapshot. A mismatched sibling snapshot can make even metadata commands
such as v8asm version initialize the wrong startup data in older builds, and
it invalidates the matrix result.
Audit cached binaries before treating them as a gate:
python3 tests/decomp_rounds/audit_patch_coverage.py --strict
python3 tests/decomp_rounds/check_bin_cache.py
python3 tests/decomp_rounds/check_patch_text.py
python3 tests/decomp_rounds/check_electron_snapshot_round.py
python3 tests/decomp_rounds/check_electron_version_matrix.py
python3 tests/decomp_rounds/audit_electron_release_coverage.pyThe default matrix automatically enumerates executable
tests/decomp_rounds/bin_cache/*/v8asm binaries, plus ./v8asm when it exists.
Cached support files under tests/decomp_rounds/bin_cache/ are used for V8
builds that are expensive to recreate. The default list deliberately avoids
stale local out/ paths so VERSION_MATRIX_REQUIRE_BINS=1 can be used as a
real gate; add experimental builds explicitly with VERSION_MATRIX_V8ASM_BINS
when probing another branch without adding it to the shared cache.
For Electron-generated bytenode cache, use the Electron matrix gate. It
enumerates local Electron releases under
/home/aynakeya/workspace/tmp/v8test/electron-cache, reads
process.versions.v8, selects only matching Electron-flavored cached v8asm
binaries, compiles a .jsc with that Electron runtime, then tests both
snapshot_blob.bin and v8_context_snapshot.bin explicitly:
python3 tests/decomp_rounds/check_electron_version_matrix.pyIf a matching Electron release is not present locally, cache it first from the official GitHub release zip. Existing zips and unpacked release directories are reused, not deleted:
python3 tests/decomp_rounds/fetch_electron_releases.py 34.3.0
python3 tests/decomp_rounds/check_electron_version_matrix.pyTo see which Electron-style build rows have an official stable Linux x64 release and which ones are source/self-cache only, run:
python3 tests/decomp_rounds/audit_electron_release_coverage.pyDo not use this as cross-version evidence. If the matching Electron release is not present locally, fetch/cache that release first or treat the row as unverified rather than substituting a nearby V8 branch.
The script now behaves like a small CI gate by default:
- existing
v8asmbinaries must pass self-generated asm, strict disasm, and level-4 decompile; - bytenode force-disasm is required when numeric V8 and pointer compression both match; numeric or pointer-layout mismatches are skipped by default;
- numeric mismatches can still be probed with
VERSION_MATRIX_FORCE_MISMATCH=1for research, but any signal-style exit code such asfail:139is a gate failure rather than a warning; - successful level-4 outputs must keep raw
goto offset_...statements and missing-opcode// 0x... @ ...comments at zero by default; tune withVERSION_MATRIX_MAX_RAW_GOTOandVERSION_MATRIX_MAX_UNKNOWNonly when deliberately recording a known regression; - crash signatures such as SIGSEGV, CHECK/DCHECK failures, and sanitizer errors are failures;
<undefined: segmentfault...>placeholders are recorded asundefined_fallbacks; setVERSION_MATRIX_MAX_UNDEFINED_FALLBACKSto make them fatal for a focused run;- missing optional Node versions or optional
v8asmbinaries are warnings unlessVERSION_MATRIX_REQUIRE_NODES=1orVERSION_MATRIX_REQUIRE_BINS=1.
v8asm disasm validates the cached-data header before deserializing. Matching the
numeric V8 version hash is not enough: magic, flags_hash, and the read-only
snapshot checksum must also match the current v8asm build.
./v8asm checkversion sample.jsc
./v8asm disasm sample.jsc
./v8asm disasm sample.jsc --force-incompatible # best-effort research fallbackUse --force-incompatible only when intentionally inspecting cache from a
different embedder/build, such as Node/bytenode cache with a vanilla V8 build.
The V8 patches also bypass V8's internal SerializedCodeData::SanityCheck*
rejection path in forced mode, so --force-incompatible reaches the real
deserializer even when the cached-data magic or flags hash does not match.
Before entering V8's deserializer, v8asm still verifies that the current V8
header layout can parse a plausible payload length. A length outside the file,
or a zero payload length while payload bytes are present, is treated as a
different major-header layout. v8asm also recognizes common Node/Electron V8
version hashes and warns when forced mode is crossing a V8 major version.
Plausible mismatches enter V8 in forced mode, but the warning is intentional:
cross-major bytecode layouts can still abort inside V8 and should normally be
handled with a matching major-version patch/build.
For Electron samples, pass the matching startup snapshot explicitly when it is
available:
./v8asm --snapshot_blob v8context/v8_context_snapshot.bin \
disasm atom.compiled.dist.jsc --force-incompatible
./v8asm --snapshot_blob v8context/v8_context_snapshot.bin \
checkversion atom.compiled.dist.jsc --force-incompatibleThe snapshot must match the V8 baseline used to build v8asm. For example,
a 13.4.114.21-electron.0 snapshot is valid for the matching 13.4 Electron
build, not for a 13.2.152.41-electron.0 v8asm. Cross-baseline startup
snapshots may still abort inside V8 even when forced version checks are
bypassed.
Check failed: <bool> == fixed_offset in read-only-deserializer.cc means the
v8asm binary's V8_STATIC_ROOTS_BOOL does not match whether the loaded
snapshot was serialized with fixed read-only roots. Build a matching
v8_enable_static_roots=true or false v8asm variant and treat it as a
best-effort snapshot-specific probe rather than patching over the cached-data
header checks.
For official Electron 34.3.0 snapshots (13.2.152.41-electron.0), the normal
13.2 Electron build with v8_enable_static_roots=true is the matching path.
The non-static-roots build is kept only as a best-effort probe for snapshots
that fail in the opposite direction; it is expected to abort on Electron 34.3.0
with Check failed: false == fixed_offset.
cd /home/aynakeya/workspace/tmp/v8test
source start_env.md
cd v8
gn gen out/v8asm.13.2.152.41.electron.nostaticroots.x64.release --args='is_debug=false v8_enable_object_print=true v8_enable_disassembler=true v8_enable_pointer_compression=true v8_enable_sandbox=true v8_embedder_string="-electron.0" v8_enable_static_roots=false'
autoninja -j10 -C out/v8asm.13.2.152.41.electron.nostaticroots.x64.release v8asmThe cached special probe, when present, lives at
tests/decomp_rounds/bin_cache/v8asm.13.2.152.41.electron.nostaticroots.x64.release/.
The focused Electron validation command is:
python3 tests/decomp_rounds/check_electron_snapshot_round.py \
--v8asm tests/decomp_rounds/bin_cache/v8asm.13.2.152.41.electron.x64.release/v8asmFor the Atom 13.4 context snapshot in v8context/v8_context_snapshot.bin, the
matching best-effort build is the explicit static-roots Electron variant:
cd /home/aynakeya/workspace/tmp/v8test
source start_env.md
cd v8
gn gen out/v8asm.13.4.114.21.electron.staticroots.x64.release --args='is_debug=false v8_enable_object_print=true v8_enable_disassembler=true v8_enable_pointer_compression=true v8_enable_sandbox=true v8_embedder_string="-electron.0" v8_enable_static_roots=true'
autoninja -j10 -C out/v8asm.13.4.114.21.electron.staticroots.x64.release v8asmThe cached special probe, when present, lives at
tests/decomp_rounds/bin_cache/v8asm.13.4.114.21.electron.staticroots.x64.release/.
When --snapshot_blob and --force-incompatible are used together, v8asm
opens the forced snapshot recovery switches before V8 startup data is loaded.
This lets an Electron context snapshot with a version string such as
13.4.114.21-electron.0 initialize a plain 13.4.114.21 research build. The
version mismatch is printed on stderr and strict mode remains unchanged.
Startup snapshots from a different V8 baseline are attempted only in forced
mode. The 13.6 patch has additional recovery probes for read-only heap
post-processing, shared string-table insertion, external-reference-table
sentinels, and root synchronization, but a 13.6 binary still cannot reliably
initialize the 13.4 Electron startup snapshot because the startup root stream
layout diverges after those checks. Use the matching V8 baseline when the goal
is usable output. In forced Node/bytenode rounds, newer 13.6 builds also print
the current read-only heap object range for object-print failures; addresses
that consistently land inside current RO objects point at snapshot/layout
mismatch, not merely a missing printer guard.
The matrix runner uses --snapshot_blob automatically when a cached v8asm
binary has a sibling snapshot_blob.bin. Metadata commands (version and
build-args) never receive a snapshot override, so they cannot be polluted by
startup snapshot warnings from older cached builds. Strict commands use a
snapshot only for sibling blobs; an explicit override is used only for forced
commands and only when the v8asm binary contains the forced snapshot recovery
hook.
Bytenode checkversion rows also pass --force-incompatible, so an override
snapshot affects the header comparison instead of only the following disasm.
Override that for embedder snapshots, for example:
VERSION_MATRIX_SNAPSHOT_BLOB=v8context/v8_context_snapshot.bin \
tests/decomp_rounds/run_version_matrix.shUse the patch coverage audit before switching V8 branches or claiming that the cross-version matrix is complete:
python3 tests/decomp_rounds/audit_patch_coverage.pyThe default mode reports current gaps but exits 0. Use --strict only when the
expected end state is that every listed patch family has both node-style and
Electron-style cached binaries.
When the question is whether the patches still compile from source, use the
build matrix script instead. It uses the official checkout environment, runs
gclient sync --with_branch_heads --with_tags after each tag checkout, applies
the matching patch with --3way --recount, and builds with autoninja -j10:
tests/decomp_rounds/build_v8asm_matrix.sh
tests/decomp_rounds/build_v8asm_matrix.sh 13.2-electron 13.4-electron-staticrootsThe build matrix deliberately uses stable, version-specific out/ directories
and copies successful artifacts into tests/decomp_rounds/bin_cache/. Reuse
those directories for rebuilds so existing compiler outputs and generated
snapshots stay warm. Do not run gclient sync -D, remove out/, remove
Electron release caches, or move app/Electron snapshots into bin_cache while
trying to prove a patch. If a new V8/Electron variant is needed, add a named
matrix row with its own persistent output directory, then record the new rule in
README.md, agents.md, and a note under note/.
Static-roots variants in that script are snapshot-specific best-effort builds.
If a snapshot fails with a fixed_offset check, compile the matching
v8_enable_static_roots=true or false row and verify it with the target
snapshot instead of patching over the read-only snapshot layout check.
v8patch/v8asm.patch: current 13.6-oriented patch, verified on13.6.233.10. It includes global--snapshot_blob, internal cached-data sanity bypass, direct forced-load payload plausibility guards, forced cross-major cached-data warnings, and the forced snapshot version mismatch bypass. It is not a usable recovery path for the 13.4 Electron Atom snapshot.v8patch/v8asm-13.2.patch: V8 13.2 adaptation, verified on13.2.152.41with Electron-style, Electron no-static-roots, and Node-style build args. The cachedv8asm.13.2.152.41.electron.x64.releasebuild passes the Electron 34.3.0 snapshot round with both Electron package snapshots. The cachedv8asm.13.2.152.41.node.x64.releasebuild usesv8_enable_pointer_compression=falseandv8_enable_sandbox=false, passes explicit self--snapshot_blobasm/checkversion/disasm and level-4 decompile. The no-static-roots Electron cache is a best-effort probe for snapshots that fail withCheck failed: true == fixed_offset.v8patch/v8asm-13.4.patch: V8 13.4 adaptation used for the Electronatom.compiled.dist.jscrecovery notes. It adds--snapshot_blob, a direct V8SerializedCodeData::SanityCheck*bypass for forced incompatible loads, a forced snapshot version mismatch bypass, a research-only Electron startup snapshot external-reference-table bypass, and keeps the other V8 deserializer checks intact. The explicitv8asm.13.4.114.21.node.x64.releasebuild usesv8_enable_pointer_compression=falseandv8_enable_static_roots=false, and passes explicit self--snapshot_blobasm/checkversion/disasm plus level-4 decompile. The explicitv8asm.13.4.114.21.electron.staticroots.x64.releasebuild usesv8_enable_static_roots=true; it loadsv8context/v8_context_snapshot.binforatom.compiled.dist.jscwithout thefixed_offsetfailure.v8patch/v8asm-12.4.patch: V8 12.4 adaptation. Node 22/bytenode needs a separate no-pointer-compression build of this branch. Verified on12.4.254.21with both the normal and Node22-like build args. The cachedtests/decomp_rounds/bin_cache/v8asm.12.4.node22.x64.release/v8asmbuild passes self-cache and Node22/bytenode forced disasm/decompile. This branch has the--snapshot_blob, cached-data sanity bypass, direct forced-load payload plausibility guards, forced cross-major warnings, forced snapshot version mismatch bypass; it does not have the 13.x startup external-reference-table validation hook.v8patch/v8asm-12.9.patch: V8 12.9 adaptation, verified on12.9.202.28with Node-style and Electron-style cached binaries. It preserves the 12.x TrustedObject sandbox guard inobjects-printer.ccand includes forced snapshot version mismatch handling.v8patch/v8asm-11.9.patch: V8 11.9 adaptation, verified on11.9.169.7with Node-style and Electron-style cached binaries. It uses the 11.xCodeSerializer::Deserialize(..., ScriptOriginOptions())API and older object-cast helpers. This branch has the--snapshot_blob, cached-data sanity bypass, and forced snapshot version mismatch bypass; it does not have the 13.x startup external-reference-table validation hook.v8patch/v8asm-11.4.patch: V8 11.4 adaptation, verified on11.4.183.14with Node-style and Electron-style cached binaries. The snapshot version reader must accept the older startup snapshot string offset used by this branch. The cached Node-style build usesv8_enable_pointer_compression=falseandv8_enable_static_roots=false; both Node-style and Electron-style rows are covered by the source build matrix.v8patch/v8asm-10.2.patch: V8 10.2 adaptation for Node 18/bytenode (v18.20.8, V810.2.154.26-node.39). It uses the older cached-data header shape without a read-only snapshot checksum, keeps global--snapshot_blob, bypasses internal cached-data sanity checks for forced recovery, adds direct forced-load payload plausibility guards, forced cross-major warnings, adds forced snapshot version mismatch bypass. Verified with the cachedtests/decomp_rounds/bin_cache/v8asm.10.2.node18.x64.release/v8asm; self-cache and Node18/bytenode forced disasm/decompile both pass.v8patch/v8asm-10.8.patch: V8 10.8 Electron adaptation for10.8.168.25-electron.0. Do not use the 10.2 patch directly on this tag: 10.8 already has the newerBytecodeArray::Disassemblehandle-based constant-pool and handler-table code, so a 10.2 3way apply conflicts insrc/objects/code.cc. The dedicated patch keeps that 10.8 native code and carries the samev8asm,--snapshot_blob, cached-data sanity bypass, and forced snapshot version mismatch recovery surface. The cachedv8asm.10.8.node.x64.releasebuild usesv8_enable_pointer_compression=falseandv8_enable_static_roots=false, and passes explicit self--snapshot_blobasm/checkversion/disasm plus level-4 decompile.v8patch/v8asm-11.3.patch: V8 11.3 adaptation for Node 20/bytenode (v20.20.2, V811.3.244.8-node.38). It uses the olderObjectmember predicate API, moves the short-print segfault guard tosrc/objects/objects.cc, and treats the read-only snapshot checksum as unavailable because the V8 11.3 cached-data header does not contain that field. It includes the direct forced-load payload plausibility guards, forced cross-major warnings, and forced snapshot version mismatch bypass. Verified withtests/decomp_rounds/bin_cache/v8asm.11.3.node20.x64.release/v8asm; self-cache and Node20/bytenode forced disasm/decompile both pass.
apply patches
git apply --check v8asm.patch
git apply --3way v8asm.patchgenerate patch
git diff --staged > v8asm.patch- View8
- check out my blog post