‹ projects

reverb-m1

Native wired VR on Apple silicon: Reverb G1 display bring-up and stereo demo
git clone https://umhau.org/code/reverb-m1.git
Log | Files | Refs

xref.py (4199B)


      1 # Cross-reference scanner for the decompressed arm64e kernelcache.
      2 # Finds code that loads a given C string (ADRP + ADD) and says whether it's passed to PE_parse_boot_argn,
      3 # i.e. whether the string is a boot-arg.
      4 #
      5 # Usage: venv/bin/python xref.py kernelcache.macho <string> [<string> ...]
      6 # Rebuild kernelcache.macho: see SETUP.md ("How the boot-arg was found").
      7 import struct
      8 import sys
      9 
     10 import numpy as np
     11 from capstone import CS_ARCH_ARM64, CS_MODE_ARM, Cs
     12 
     13 data = open(sys.argv[1] if len(sys.argv) > 1 else 'kernelcache.macho', 'rb').read()
     14 md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
     15 
     16 # Top-level LC_SEGMENT_64s of the fileset map every byte of the file.
     17 segs = []
     18 ncmds, off = struct.unpack_from('<I', data, 16)[0], 32
     19 for _ in range(ncmds):
     20     cmd, sz = struct.unpack_from('<II', data, off)
     21     if cmd == 0x19:
     22         vm, vms, fo, fs = struct.unpack_from('<QQQQ', data, off + 24)
     23         segs.append((vm, fo, fs))
     24     off += sz
     25 
     26 
     27 def f2v(fo):
     28     for vm, sfo, fs in segs:
     29         if sfo <= fo < sfo + fs:
     30             return vm + fo - sfo
     31 
     32 
     33 def v2f(va):
     34     for vm, sfo, fs in segs:
     35         if vm <= va < vm + fs:
     36             return sfo + va - vm
     37 
     38 
     39 def cstr(va):
     40     fo = v2f(va)
     41     if fo is None:
     42         return None
     43     e = data.find(b'\0', fo, fo + 100)
     44     s = data[fo:e]
     45     return s.decode() if s and all(32 <= c < 127 for c in s) else None
     46 
     47 
     48 # Every ADRP's target page, computed once (vectorized).
     49 words = np.frombuffer(data[:len(data) // 4 * 4], dtype='<u4')
     50 adrp_idx = np.nonzero((words & 0x9F000000) == 0x90000000)[0]
     51 aw = words[adrp_idx].astype(np.int64)
     52 imm = (((aw >> 5) & 0x7FFFF) << 2 | ((aw >> 29) & 3)) << 12
     53 imm = np.where(imm & (1 << 32), imm - (1 << 33), imm)
     54 fo_arr = adrp_idx.astype(np.uint64) * np.uint64(4)
     55 pc_arr = np.zeros(len(adrp_idx), dtype=np.uint64)
     56 valid = np.zeros(len(adrp_idx), dtype=bool)
     57 for vm, sfo, fs in segs:
     58     m = (fo_arr >= np.uint64(sfo)) & (fo_arr < np.uint64(sfo + fs))
     59     pc_arr[m] = np.uint64(vm) + fo_arr[m] - np.uint64(sfo)
     60     valid |= m
     61 target = (pc_arr & ~np.uint64(0xFFF)) + imm.astype(np.uint64)
     62 
     63 
     64 def find_xrefs(s):
     65     """Addresses of instructions that load the address of C string `s`."""
     66     out, start = [], 0
     67     while (i := data.find(b'\0' + s.encode() + b'\0', start)) >= 0:
     68         start = i + 1
     69         sva = f2v(i + 1)
     70         if sva is None:
     71             continue
     72         for wi in adrp_idx[valid & (target == np.uint64(sva & ~0xFFF))]:
     73             wi = int(wi)
     74             rd = int(words[wi]) & 31
     75             for k in range(1, 6):
     76                 w2 = int(words[wi + k])
     77                 if (w2 & 0xFF800000) == 0x91000000 and ((w2 >> 5) & 31) == rd and ((w2 >> 10) & 0xFFF) == (sva & 0xFFF):
     78                     out.append(f2v(wi * 4))
     79                     break
     80     return out
     81 
     82 
     83 def calls_after(pc, n=8):
     84     fo = v2f(pc)
     85     return [int(i.op_str[1:], 16) for i in md.disasm(data[fo:fo + n * 4], pc) if i.mnemonic == 'bl']
     86 
     87 
     88 def find_boot_arg_parser():
     89     """PE_parse_boot_argn = the function called right after loading a known boot-arg string."""
     90     counts = {}
     91     for s in ('amfi_get_out_of_my_way', 'debug', 'serial'):
     92         for pc in find_xrefs(s):
     93             for t in calls_after(pc, 6):
     94                 counts[t] = counts.get(t, 0) + 1
     95     return max(counts, key=counts.get)
     96 
     97 
     98 def disasm(va, n):
     99     """Disassembly with ADRP+ADD string operands annotated."""
    100     fo, regs, lines = v2f(va), {}, []
    101     for ins in md.disasm(data[fo:fo + n * 4], va):
    102         note = ''
    103         if ins.mnemonic == 'adrp':
    104             regs[ins.op_str.split(',')[0]] = int(ins.op_str.split('#')[1], 16)
    105         elif ins.mnemonic == 'add' and '#' in ins.op_str:
    106             r = [x.strip() for x in ins.op_str.split(',')]
    107             if r[1] in regs:
    108                 s = cstr(regs[r[1]] + int(r[2][1:], 16))
    109                 note = f'   ; {s!r}' if s else ''
    110         lines.append(f'{ins.address:#x}: {ins.mnemonic:7} {ins.op_str}{note}')
    111     return lines
    112 
    113 
    114 if __name__ == '__main__':
    115     pb = find_boot_arg_parser()
    116     print(f'PE_parse_boot_argn = {pb:#x}')
    117     for s in sys.argv[2:]:
    118         for pc in find_xrefs(s):
    119             kind = 'BOOT-ARG' if pb in calls_after(pc, 6) else 'other'
    120             print(f'{s:32} ref {pc:#x}: {kind}')