#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Annotated, windowed disassembly of a Dart AOT snapshot, plus a caller index. WHY THIS EXISTS --------------- Two things make Dart AOT code readable at all: 1. **Pool annotations.** A raw `ldr x2, [x27, #0x9d0]` is meaningless; annotated with the pool entry it becomes `"vipflag"` and the surrounding code explains itself. This tool resolves those loads and prints what they point at, including how many other sites reference the same entry. 2. **Windowed disassembly.** Decoding an entire multi-MB `.text` in one pass is how a host gets pinned at 1-2 GB and the run looks like a hang. Disassemble only the ranges you care about -- that is what this tool does, and it is not a limitation but the method. Booleans are annotated too: Dart materialises `true`/`false` as small offsets from the null register, so `add x0, x22, #0x20` is marked TRUE and `#0x30` is marked FALSE. Those are the cheapest patch sites in the whole snapshot. The caller index answers the other half of every question -- "what calls this function". It is built from B/BL target arithmetic on the raw words, so it needs no disassembler at all and finishes in seconds. USAGE ----- # a window around one address (default: 0x180 before, 0x220 after) python dart_disasm.py libapp.so --pp pp.txt --refs pp_refs.json 0x26e390 # an explicit range python dart_disasm.py libapp.so --pp pp.txt --refs pp_refs.json --range 0x64f310 0x64f3e0 # widen / narrow the window python dart_disasm.py libapp.so --refs pp_refs.json 0x64f1bc --before 0x80 --after 0x40 # caller index python dart_disasm.py libapp.so --build-index callers.json python dart_disasm.py libapp.so --index callers.json 0x26e230 0x64f0f0 INPUTS ------ * `--pp` is the decompiler's pool listing (blutter `pp.txt`): lines shaped `[pp+0x9d0] String: "vipflag"`. Without it, loads are annotated with the raw offset only. * `--refs` is the pool-offset -> code-site index built by `dart_pprefs.py`. Requires `capstone` for disassembly; the caller index works without it. """ import argparse import json import os import re import struct import sys POOL_REG = 27 BOOL_TRUE, BOOL_FALSE = 0x20, 0x30 PP_LINE = re.compile(r'^\[(pp\+0x[0-9a-f]+)\](.*)$') def text_section(path): blob = open(path, 'rb').read() if blob[:4] != b'\x7fELF': raise SystemExit('not an ELF file: %s' % path) if blob[4] != 2: raise SystemExit('not ELF64: %s' % path) e_shoff = struct.unpack_from(' call sites, decoded arithmetically (no disassembler needed).""" text_addr, blob = text_section(sample) index = {} words = struct.unpack_from('<%dI' % (len(blob) // 4), blob, 0) for i, w in enumerate(words): if w & 0xFC000000 in (0x14000000, 0x94000000): imm = w & 0x03FFFFFF if imm & 0x02000000: imm -= 0x04000000 site = text_addr + i * 4 index.setdefault(site + (imm << 2), []).append(site) with open(out, 'w', encoding='utf-8') as fh: json.dump({hex(k): v for k, v in index.items()}, fh) print('distinct targets : %d' % len(index)) print('call sites : %d' % sum(len(v) for v in index.values())) print('written %s' % out) def lookup_index(sample, path, addrs): index = {int(k, 16): v for k, v in json.load(open(path, encoding='utf-8')).items()} for spec in addrs: target = int(spec, 16) sites = sorted(index.get(target, [])) print('0x%x: %d caller(s)' % (target, len(sites))) for s in sites[:80]: print(' 0x%x' % s) if len(sites) > 80: print(' ... %d more' % (len(sites) - 80)) def main(): ap = argparse.ArgumentParser( description=__doc__.split('\n')[0], formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__) ap.add_argument('sample', help='libapp.so') ap.add_argument('addr', nargs='*', help='hex address(es) to disassemble around') ap.add_argument('--pp', metavar='PP_TXT', help="decompiler pool listing (blutter pp.txt)") ap.add_argument('--refs', metavar='REFS_JSON', help='index from dart_pprefs.py') ap.add_argument('--range', nargs=2, metavar=('START', 'END')) ap.add_argument('--before', default='0x180') ap.add_argument('--after', default='0x220') ap.add_argument('--build-index', metavar='OUT_JSON') ap.add_argument('--index', metavar='CALLERS_JSON') a = ap.parse_args() if a.build_index: return build_index(a.sample, a.build_index) if a.index: if not a.addr: raise SystemExit('give at least one hex address to look up') return lookup_index(a.sample, a.index, a.addr) pp = load_pp(a.pp) refs = load_refs(a.refs) if a.range: start, stop = int(a.range[0], 16), int(a.range[1], 16) disasm_window(a.sample, start, 0, stop - start, pp, refs) return 0 if not a.addr: raise SystemExit('give an address, --range, --build-index, or --index') for spec in a.addr: disasm_window(a.sample, int(spec, 16), int(a.before, 0), int(a.after, 0), pp, refs) return 0 if __name__ == '__main__': sys.exit(main())