Summary
The Dalvik VM is bound to a single DEX, so a method whose bytecode lives in a different DEX of a multi-dex APK is never executed. When a method in classes.dex invokes one defined in classes2.dex, the direct/static dispatch lookup misses the single-DEX method map and the call is silently downgraded to an external API miss (stub or no-op). dextrace run <apk> therefore only produces correct results when every method actually executed happens to live in the one DEX that was loaded.
Root cause
# src/dextrace/vm/engine.py (_do_invoke, direct/static path)
callee_code_off = self._sig_to_codeoff.get(callee_sig)
if callee_code_off is None:
self._handle_external_miss(callee_sig, insn, state)
return None
self._sig_to_codeoff, self._parser, self._disasm, self._resolver, and self._hierarchy are all built from a single dex_bytes. A code_off is a byte offset valid only within its own DEX, so the engine has no way to reach a callee defined in another DEX — the lookup misses and real bytecode is skipped.
Fix
Make the VM module-aware: build one DexModule (parser, resolver, disasm, local method map) per DEX, resolve direct/static calls through a global map, and carry the current module through the execution loop and CallFrame.
# global dispatch map: sig -> (module_id, code_off), stable order (classes.dex first)
location = self._sig_to_location.get(callee_sig)
if location is None:
self._handle_external_miss(callee_sig, insn, state)
return None
callee_module_id, callee_code_off = location
# switch active module on invoke; restore caller's module on return
Cross-DEX virtual dispatch and inheritance additionally require merging ClassHierarchy across all DEXes (vtable values become (module_id, code_off)) — a separable follow-up.
Affected locations
| File |
Line(s) |
Context |
src/dextrace/vm/engine.py |
137-162 |
Per-DEX state built from single dex_bytes |
src/dextrace/vm/engine.py |
708-711 |
Direct/static dispatch misses cross-DEX callee |
src/dextrace/vm/engine.py |
692-694 |
Virtual dispatch via single-DEX ClassHierarchy |
src/dextrace/vm/class_hierarchy.py |
71-90 |
Vtables built from one DEX; values are bare code_off |
src/dextrace/cli/cmd_run.py |
137-139 |
dextrace run loads only the entry DEX |
Summary
The Dalvik VM is bound to a single DEX, so a method whose bytecode lives in a different DEX of a multi-dex APK is never executed. When a method in
classes.dexinvokes one defined inclasses2.dex, the direct/static dispatch lookup misses the single-DEX method map and the call is silently downgraded to an external API miss (stub or no-op).dextrace run <apk>therefore only produces correct results when every method actually executed happens to live in the one DEX that was loaded.Root cause
self._sig_to_codeoff,self._parser,self._disasm,self._resolver, andself._hierarchyare all built from a singledex_bytes. Acode_offis a byte offset valid only within its own DEX, so the engine has no way to reach a callee defined in another DEX — the lookup misses and real bytecode is skipped.Fix
Make the VM module-aware: build one
DexModule(parser, resolver, disasm, local method map) per DEX, resolve direct/static calls through a global map, and carry the current module through the execution loop andCallFrame.Cross-DEX virtual dispatch and inheritance additionally require merging
ClassHierarchyacross all DEXes (vtable values become(module_id, code_off)) — a separable follow-up.Affected locations
src/dextrace/vm/engine.pydex_bytessrc/dextrace/vm/engine.pysrc/dextrace/vm/engine.pyClassHierarchysrc/dextrace/vm/class_hierarchy.pycode_offsrc/dextrace/cli/cmd_run.pydextrace runloads only the entry DEX