"""Run the original Kaleidostrobe PPC code under Unicorn, as iTunes would. Imports are trapped at their __picsymbol_stub entries and answered in Python; GL calls are recorded (name, args) so a frame's draw list can be compared. """ import math, struct, sys sys.path.insert(0, __file__.rsplit('/', 1)[0]) import macho from unicorn import Uc, UC_ARCH_PPC, UC_MODE_PPC32, UC_MODE_BIG_ENDIAN, UC_HOOK_CODE from unicorn import UC_HOOK_MEM_UNMAPPED, UcError import unicorn.ppc_const as P R = [getattr(P, f'UC_PPC_REG_{i}') for i in range(32)] F = [getattr(P, f'UC_PPC_REG_FPR{i}') for i in range(32)] STUB0, STUBEND, STUBSZ = 0x11f5c, 0x12d00, 36 MAGIC = 0x7fff0 # return address: emu_stop when reached STACK_TOP = 0x0ff0000 HEAP = 0x1000000 NOOPS = {'_GetPortForeColor', '_GetPortBackColor', '_RGBForeColor', '_RGBBackColor', '_PaintRect', '_EraseRect', '_InsetRect', '_TextFont', '_TextSize', '_TextMode', '_TETextBox', '_FMGetFontFamilyFromName', '_ShowWindow', '_HideWindow', '_fprintf', '_fclose', '_strcat', '_MPBlockCopy'} STUBS = {a: n for a, (t, n) in macho.stubname.items() if t == 'stub'} class ParkMiller: """FreeBSD 4 / Darwin 6 libc rand(): RAND_MAX 0x7fffffff.""" def __init__(self, seed=1): self.x = seed def srand(self, s): self.x = s & 0xffffffff def rand(self): x = self.x if x == 0: x = 123459876 hi, lo = divmod(x, 127773) x = 16807 * lo - 2836 * hi if x < 0: x += 0x7fffffff self.x = x return x % 0x80000000 class KS: def __init__(self, seed=1): u = self.u = Uc(UC_ARCH_PPC, UC_MODE_PPC32 | UC_MODE_BIG_ENDIAN) u.mem_map(0, 0x80000) u.mem_write(0, macho.D[:0x24000]) u.mem_map(STACK_TOP - 0x100000, 0x110000) u.mem_map(HEAP, 0x400000) self.heap = HEAP u.reg_write(P.UC_PPC_REG_MSR, u.reg_read(P.UC_PPC_REG_MSR) | 0x2000) # the weak import aglChoosePixelFormat must look present u.mem_write(0x23fb8, struct.pack('>I', 1)) # Carbon's MathLib `extern const double pi` (fp.h) self.heap += 16 u.mem_write(HEAP, struct.pack('>d', math.pi)) u.mem_write(0x23fa4, struct.pack('>I', HEAP)) u.hook_add(UC_HOOK_CODE, self._stub, begin=STUB0, end=STUBEND - 1) u.hook_add(UC_HOOK_CODE, self._magic, begin=MAGIC, end=MAGIC + 3) itcall = macho.func_size['_ITCallApplication'][0] u.hook_add(UC_HOOK_CODE, lambda u, a, s, d: self._ret(r3=0), begin=itcall, end=itcall + 3) self.rng = ParkMiller(seed) self.ticks = 1000 self.micro = 10_000_000 self.gl = [] self.textures = {} self.trace_gl = True # ---- memory helpers ---- def r32(self, a): return struct.unpack('>I', self.u.mem_read(a, 4))[0] def w32(self, a, v): self.u.mem_write(a, struct.pack('>I', v & 0xffffffff)) def rf(self, a): return struct.unpack('>f', self.u.mem_read(a, 4))[0] def wf(self, a, v): self.u.mem_write(a, struct.pack('>f', v)) def rd(self, a): return struct.unpack('>d', self.u.mem_read(a, 8))[0] def wd(self, a, v): self.u.mem_write(a, struct.pack('>d', v)) def mem(self, a, n): return bytes(self.u.mem_read(a, n)) def alloc(self, n): a = (self.heap + 15) & ~15 self.heap = a + n self.u.mem_write(a, b'\0' * n) return a def reg(self, i): return self.u.reg_read(R[i]) def setreg(self, i, v): self.u.reg_write(R[i], v & 0xffffffff) def freg(self, i): v = self.u.reg_read(F[i]) return struct.unpack('>d', struct.pack('>Q', v & 0xffffffffffffffff))[0] def setfreg(self, i, x): self.u.reg_write(F[i], struct.unpack('>Q', struct.pack('>d', x))[0]) # ---- calling ---- def call(self, addr, *args, fargs=()): if isinstance(addr, str): addr = macho.func_size[addr][0] u = self.u sp = STACK_TOP - 0x400 u.reg_write(R[1], sp) self.w32(sp, 0) for i, a in enumerate(args[:8]): self.setreg(3 + i, a) for i, a in enumerate(args[8:]): self.w32(sp + 24 + 4 * (8 + i), a) for i, x in enumerate(fargs): self.setfreg(1 + i, x) u.reg_write(P.UC_PPC_REG_LR, MAGIC) try: u.emu_start(addr, MAGIC + 4) except UcError as e: pc = u.reg_read(P.UC_PPC_REG_PC) raise RuntimeError(f'{e} at pc={pc:#x} ({macho.symfor(pc)})') from None return self.reg(3) def _magic(self, u, addr, size, ud): u.emu_stop() def _ret(self, r3=None, f1=None): if r3 is not None: self.setreg(3, r3) if f1 is not None: self.setfreg(1, f1) self.u.reg_write(P.UC_PPC_REG_PC, self.u.reg_read(P.UC_PPC_REG_LR)) def stackarg(self, i): sp = self.reg(1) return self.r32(sp + 24 + 4 * i) # ---- imports ---- def _stub(self, u, addr, size, ud): if (addr - STUB0) % STUBSZ: return name = STUBS.get(addr, '?') fn = getattr(self, 'imp' + name, None) if fn is not None: fn() return if name.startswith('_gl') or name.startswith('_agl'): self._glcall(name) return if name in NOOPS: self._ret(r3=0) return raise RuntimeError(f'unhandled import {name}') def f(self, n): return [self.freg(1 + i) for i in range(n)] def r(self, n): return [self.reg(3 + i) for i in range(n)] def imp_sin(self): self._ret(f1=math.sin(self.freg(1))) def imp_cos(self): self._ret(f1=math.cos(self.freg(1))) def imp_tan(self): self._ret(f1=math.tan(self.freg(1))) def imp_sqrt(self): self._ret(f1=math.sqrt(self.freg(1)) if self.freg(1) >= 0 else float('nan')) def imp_floor(self): self._ret(f1=math.floor(self.freg(1))) def imp_atan2(self): y, x = self.f(2); self._ret(f1=math.atan2(y, x)) def imp_pow(self): x, y = self.f(2) try: v = math.pow(x, y) except (ValueError, OverflowError): v = float('nan') self._ret(f1=v) def imp_modf(self): x = self.freg(1) ip = float(math.trunc(x)) self.wd(self.reg(5), ip) self._ret(f1=x - ip) def imp_rand(self): self._ret(r3=self.rng.rand()) def imp_srand(self): self.rng.srand(self.reg(3)); self._ret() def imp_TickCount(self): self._ret(r3=self.ticks) def imp_Microseconds(self): p = self.reg(3) self.w32(p, self.micro >> 32); self.w32(p + 4, self.micro) self._ret() def imp_memcpy(self): d, s, n = self.r(3) self.u.mem_write(d, self.mem(s, n)); self._ret(r3=d) def imp_BlockMoveData(self): s, d, n = self.r(3) self.u.mem_write(d, self.mem(s, n)); self._ret() def imp_memset(self): d, c, n = self.r(3) self.u.mem_write(d, bytes([c & 0xff]) * n); self._ret(r3=d) def imp_NewPtrClear(self): self._ret(r3=self.alloc(self.reg(3))) def imp_NewRgn(self): self._ret(r3=self.alloc(16)) def imp_DisposePtr(self): self._ret() def imp_DisposeRgn(self): self._ret() def imp_GetPort(self): self.w32(self.reg(3), 0x4444); self._ret() def imp_SetPort(self): self._ret() def imp_EqualRect(self): self._ret(r3=0) def imp_strlen(self): a = self.reg(3); n = 0 while self.mem(a + n, 1) != b'\0': n += 1 self._ret(r3=n) port_rect = (0, 0, 480, 640) # top, left, bottom, right def imp_GetPortBounds(self): rect = self.reg(4) self.u.mem_write(rect, struct.pack('>4h', *self.port_rect)) self._ret(r3=rect) def imp_PlayerGetPluginFileSpec(self): self._ret(r3=0) # ---- GL / AGL ---- GL_FLOATS = { '_glClearColor': 4, '_glColor4f': 4, '_glRotatef': 4, '_glTranslatef': 3, '_glVertex2f': 2, '_glVertex3f': 3, '_glTexCoord2f': 2, '_gluPerspective': 4, '_gluLookAt': 9, } def _glcall(self, name): ret = 0 if name in self.GL_FLOATS: args = tuple(self.f(self.GL_FLOATS[name])) elif name == '_glTexEnvf': args = (self.reg(3), self.reg(4), self.freg(1)) elif name == '_glTexImage2D': args = tuple(self.r(8)) + (self.stackarg(8),) w, h = args[3], args[4] self.textures[len(self.gl)] = self.mem(args[8], w * h * 4) elif name == '_glGenTextures': n, p = self.r(2) for i in range(n): self.w32(p + 4 * i, 1 + i) args = (n,) elif name.startswith('_agl'): args = tuple(self.r(4)) ret = 1 if name not in ('_aglGetError',) else 0 if name == '_aglCreateContext': ret = 0xc0c0 else: args = tuple(self.r(4)) if self.trace_gl: self.gl.append((name, args)) self._ret(r3=ret) if __name__ == '__main__': k = KS() print(k.call('_newValue', 3, fargs=(10.0,)))