Reverse-engineered from its PowerPC binary and checked against the original running under emulation: textures byte for byte and every triangle, over 3,600 frames. 25 fps like the plugin; its colour maps and wave images are extracted from its own code; tests/kaleidostrobe_golden.npz was recorded from the original.
189 lines
7.6 KiB
Python
189 lines
7.6 KiB
Python
"""Kaleidostrobe Mach-O PPC helper: sections, symbols, stubs, annotated disassembly."""
|
|
import struct, sys, re
|
|
import capstone
|
|
import os
|
|
BIN = os.environ.get('KALEIDOSTROBE_BIN', os.path.expanduser(
|
|
'~/documents/misc/lil hacky bits/Kaleidostrobe.bundle/Contents/MacOS/Kaleidostrobe'))
|
|
D = open(BIN, 'rb').read()
|
|
|
|
def u32(a): return struct.unpack('>I', D[a:a+4])[0]
|
|
def s16(v): return v - 0x10000 if v & 0x8000 else v
|
|
|
|
sections = {} # name -> (addr,size,off,reserved1,segname)
|
|
syms = [] # (addr, sect, name)
|
|
allsyms = []
|
|
off = 28; ncmds = struct.unpack('>I', D[16:20])[0]
|
|
dysym = None; symtab = None
|
|
seclist = []
|
|
for _ in range(ncmds):
|
|
cmd, sz = struct.unpack('>II', D[off:off+8])
|
|
if cmd == 1:
|
|
ns = struct.unpack('>I', D[off+48:off+52])[0]
|
|
so = off + 56
|
|
for _ in range(ns):
|
|
sn = D[so:so+16].rstrip(b'\0').decode(); sg = D[so+16:so+32].rstrip(b'\0').decode()
|
|
a, s, o, al, ro, nr, fl, r1, r2 = struct.unpack('>9I', D[so+32:so+68])
|
|
seclist.append((sg, sn, a, s, o, fl, r1, r2))
|
|
sections[(sg, sn)] = (a, s, o, r1, r2)
|
|
so += 68
|
|
elif cmd == 2:
|
|
symtab = struct.unpack('>4I', D[off+8:off+24])
|
|
elif cmd == 11:
|
|
dysym = struct.unpack('>18I', D[off+8:off+80])
|
|
off += sz
|
|
so, ns, stro, strs = symtab
|
|
for i in range(ns):
|
|
sx, ty, sect, desc, val = struct.unpack('>IBBhI', D[so+i*12:so+i*12+12])
|
|
name = D[stro+sx:D.index(b'\0', stro+sx)].decode()
|
|
allsyms.append((name, ty, sect, val))
|
|
if not (ty & 0xe0) and (ty & 0x0e) == 0x0e:
|
|
syms.append((val, sect, name))
|
|
syms.sort()
|
|
addr2name = {}
|
|
for v, s, n in syms:
|
|
addr2name.setdefault(v, n)
|
|
funcs = [(v, n) for v, s, n in syms if s == 1]
|
|
func_size = {}
|
|
for i, (v, n) in enumerate(funcs):
|
|
func_size[n] = (v, (funcs[i+1][0] if i+1 < len(funcs) else 0x112e4) - v)
|
|
|
|
# indirect symbols
|
|
indirectsymoff, nindirect = dysym[12], dysym[13]
|
|
ind = [u32(indirectsymoff + 4*i) for i in range(nindirect)]
|
|
stubname = {}
|
|
for (sg, sn, a, s, o, fl, r1, r2) in seclist:
|
|
typ = fl & 0xff
|
|
if typ in (6, 7): # non-lazy / lazy pointers, 4 bytes each
|
|
for k in range(s // 4):
|
|
idx = ind[r1 + k]
|
|
nm = allsyms[idx][0] if idx < len(allsyms) and idx not in (0x80000000, 0x40000000) else '?local'
|
|
stubname[a + 4*k] = ('ptr', nm)
|
|
elif typ == 8: # symbol stubs
|
|
for k in range(s // r2):
|
|
idx = ind[r1 + k]
|
|
nm = allsyms[idx][0] if idx < len(allsyms) else '?'
|
|
stubname[a + r2*k] = ('stub', nm)
|
|
|
|
def symfor(a):
|
|
if a in addr2name: return addr2name[a]
|
|
if 0x112e4 <= a < 0x11f5c and not (0x113c0 <= a < 0x1140c): return f'K_{a:x}'
|
|
best = None
|
|
for v, s, n in syms:
|
|
if v <= a: best = (v, n)
|
|
else: break
|
|
if best: return f'{best[1]}+{a-best[0]:#x}'
|
|
return hex(a)
|
|
|
|
def section_of(a):
|
|
for (sg, sn, sa, s, o, fl, r1, r2) in seclist:
|
|
if sa <= a < sa + s: return sn
|
|
return None
|
|
|
|
def fval(a, dbl):
|
|
if a + (8 if dbl else 4) > len(D): return None
|
|
return struct.unpack('>d' if dbl else '>f', D[a:a+(8 if dbl else 4)])[0]
|
|
|
|
md = capstone.Cs(capstone.CS_ARCH_PPC, capstone.CS_MODE_32 | capstone.CS_MODE_BIG_ENDIAN)
|
|
|
|
def disasm(name, seed=None):
|
|
start, size = func_size[name]
|
|
out = []
|
|
base = dict(seed or {})
|
|
stack = {}
|
|
last_call_ret = None
|
|
for ins in md.disasm(D[start:start+size], start):
|
|
m, ops = ins.mnemonic, ins.op_str
|
|
note = ''
|
|
regs = [r.strip() for r in ops.split(',')]
|
|
word = u32(ins.address)
|
|
opc = word >> 26
|
|
dest = regs[0] if regs and re.fullmatch(r'r\d+', regs[0]) else None
|
|
handled = False
|
|
if opc in (16, 18):
|
|
mm = re.search(r'0x[0-9a-f]+$', ops)
|
|
if mm:
|
|
tgt = int(mm.group(0), 16)
|
|
if word & 1:
|
|
last_call_ret = ins.address + 4
|
|
note = stubname[tgt][1] if tgt in stubname else symfor(tgt)
|
|
if tgt != ins.address + 4:
|
|
for r in ('r0','r3','r4','r5','r6','r7','r8','r9','r10','r11','r12','ctr'):
|
|
base.pop(r, None)
|
|
base['lr'] = last_call_ret
|
|
elif not (start <= tgt < start + size):
|
|
note = symfor(tgt)
|
|
handled = True
|
|
elif m == 'mflr':
|
|
v = base.get('lr', last_call_ret)
|
|
if v is not None: base[dest] = v; note = f'= {v:#x}'
|
|
handled = True
|
|
elif m == 'mtlr':
|
|
if regs[0] in base: base['lr'] = base[regs[0]]
|
|
else: base.pop('lr', None)
|
|
handled = True
|
|
elif m == 'mtctr':
|
|
if regs[0] in base: base['ctr'] = base[regs[0]]
|
|
else: base.pop('ctr', None)
|
|
handled = True
|
|
elif m == 'mfctr':
|
|
if 'ctr' in base: base[dest] = base['ctr']
|
|
else: base.pop(dest, None)
|
|
handled = True
|
|
elif m == 'mr':
|
|
if regs[1] in base: base[dest] = base[regs[1]]
|
|
else: base.pop(dest, None)
|
|
handled = True
|
|
elif m == 'addis':
|
|
if regs[1] in base:
|
|
base[dest] = (base[regs[1]] + (s16(word & 0xffff) << 16)) & 0xffffffff
|
|
else: base.pop(dest, None)
|
|
handled = True
|
|
elif m == 'addi':
|
|
if regs[1] in base:
|
|
a = (base[regs[1]] + s16(word & 0xffff)) & 0xffffffff
|
|
base[dest] = a; note = f'&{symfor(a)}'
|
|
else: base.pop(dest, None)
|
|
handled = True
|
|
else:
|
|
mm = re.match(r'(\S+), (-?(?:0x)?[0-9a-f]*)\((r\d+)\)$', ops)
|
|
if mm and (m.startswith('l') or m.startswith('st')):
|
|
breg = mm.group(3); rt = mm.group(1)
|
|
d16 = s16(word & 0xffff)
|
|
if breg == 'r1':
|
|
if m == 'stw':
|
|
if rt in base: stack[d16] = base[rt]
|
|
else: stack.pop(d16, None)
|
|
elif m == 'lwz':
|
|
if d16 in stack: base[rt] = stack[d16]; note = f'(spill {stack[d16]:#x})'
|
|
else: base.pop(rt, None)
|
|
elif m.startswith('l') and rt.startswith('r'):
|
|
base.pop(rt, None)
|
|
handled = True
|
|
elif breg in base:
|
|
a = (base[breg] + d16) & 0xffffffff
|
|
note = symfor(a)
|
|
loaded = None
|
|
if a in stubname and stubname[a][0] == 'ptr':
|
|
note = f'[ptr->{stubname[a][1]}]'
|
|
if section_of(a) == '__nl_symbol_ptr':
|
|
loaded = u32(a); note += f' = {symfor(loaded)}'
|
|
if m in ('lfs','lfd','stfs','stfd') and section_of(a) in ('__const','__data'):
|
|
v = fval(a, m in ('lfd','stfd'))
|
|
note += f' = {v!r}'
|
|
if m.startswith('l') and rt.startswith('r'):
|
|
if loaded is not None: base[rt] = loaded
|
|
else: base.pop(rt, None)
|
|
handled = True
|
|
else:
|
|
if m.startswith('l') and rt.startswith('r'): base.pop(rt, None)
|
|
handled = True
|
|
if not handled and dest and m not in ('cmpwi','cmplwi','cmpw','cmplw','stmw','fcmpu','stwx','stbx','sthx','stfsx','stfdx','stw','stb','sth','stfs','stfd'):
|
|
base.pop(dest, None)
|
|
out.append(f'{ins.address:5x} {m:8s} {ops:30s} {("; " + note) if note else ""}')
|
|
return '\n'.join(out)
|
|
|
|
if __name__ == '__main__':
|
|
for n in sys.argv[1:]:
|
|
print(f'===== {n} ({func_size[n][0]:#x}, {func_size[n][1]} bytes)')
|
|
print(disasm(n))
|