lpch/patch_rip.py

308 lines
8.9 KiB
Python
Raw Normal View History

2019-03-23 09:23:00 +00:00
#!/usr/bin/env python3
import argparse
import math
import struct
def parse_res_file(f):
import macresources
res = macresources.parse_rez_code(open(f, 'rb').read())
res = (r for r in res if r.type == b'lpch')
return sorted((r.id,r.data) for r in res)
def parse_raw_files(ff):
biglist = []
for f in ff:
num = ''
for char in reversed(f):
if char not in '0123456789': break
num = char + num
num = int(num)
dat = open(f, 'rb').read()
biglist.append((num, dat))
return sorted(biglist)
def exact_log(n):
if not n: return None
sh = 0
while n & 1 == 0:
sh += 2
n >>= 1
if n != 1: return None
return sh
class Mod:
def __init__(self):
self.entry_points = []
self.references = []
self.start = -1
self.stop = -1
self.jt_entry = -1
def __str__(self):
return '%x:%x/jt%d entries=(%s) refs=(%s)' % (self.start, self.stop, self.jt_entry, ', '.join(str(x) for x in self.entry_points), ', '.join('%x'%x for x in self.references))
class Ent:
def __init__(self):
self.offset = -1
self.jt_entry = -1
def __str__(self):
return '%x/jt%d' % (self.offset, self.jt_entry)
parser = argparse.ArgumentParser(description='''
Very hacky.
''')
parser.add_argument('src', nargs='+', action='store', help='Source file (.rdump) or files (numbered)')
parser.add_argument('-roms', nargs='+', default=['Plus', 'SE', 'II', 'Portable', 'IIci', 'SuperMario'])
2019-03-23 09:26:43 +00:00
parser.add_argument('-pt', action='store_true', help='Print raw module tokens')
2019-03-23 09:23:00 +00:00
parser.add_argument('-pm', action='store_true', help='Print information about modules and code references')
parser.add_argument('-pr', action='store_true', help='Print information about ROM references')
2019-03-23 09:24:39 +00:00
parser.add_argument('-oo', action='store', help='Base destination path to dump resources as raw files')
2019-03-23 09:23:00 +00:00
parser.add_argument('-oc', action='store', help='Base destination path to dump code files')
parser.add_argument('-oe', action='store', help='Base destination path to dump code files with refs changed to NOPs')
args = parser.parse_args()
if len(args.src) == 1:
lpch_list = parse_res_file(args.src[0])
else:
lpch_list = parse_raw_files(args.src)
# Add an "annotations" field...
lpch_list = [(a,b,[]) for (a,b) in lpch_list]
# Check that we have the right number of declared ROMs...
roms_now = len(args.roms)
roms_at_build = int(math.log(lpch_list[-1][0] + 1, 2.0))
if roms_now != roms_at_build:
print('Warning: %d ROMs specified but there were %d at build time' % (roms_now, roms_at_build))
########################################################################
large_rom_table = []
large_jump_table = []
for num, data, annotations in lpch_list:
2019-03-23 09:24:39 +00:00
if args.oo: open(args.oo + str(num), 'wb').write(data)
2019-03-23 09:23:00 +00:00
if exact_log(num) is not None:
is_single = True
else:
is_single = False
if num == lpch_list[-1][0]:
is_all = True
else:
is_all = False
matches_roms = []
for i, r in enumerate(args.roms):
if (num >> i) & 1: matches_roms.append(r)
idx = 0
if is_single:
num_lpch_for_this_rom, = struct.unpack_from('>H', data, offset=idx); idx += 2
counted = len([xnum for (xnum,xdata,*_) in lpch_list if xnum & num])
assert num_lpch_for_this_rom == counted
if is_all:
bound_rom_addr_table_cnt, jump_table_cnt = struct.unpack_from('>HH', data, offset=idx); idx += 4
code_size, = struct.unpack_from('>I', data, offset=idx); idx += 4
code = data[idx:idx+code_size]; idx += code_size
annotations[:] = [None] * code_size
print('lpch %d\t\t%db(%db)\t\t%s' % (num, len(data), code_size, ','.join(matches_roms)))
if args.oc:
open(args.oc + str(num), 'wb').write(code)
# do the rom table
rom_table_start, = struct.unpack_from('>H', data, offset=idx); idx += 2
if rom_table_start == 0xFFFF: rom_table_start = None
rom_table = []
if rom_table_start is not None:
while 1:
this_dict = {}
for r in matches_roms:
the_int = int.from_bytes(data[idx:idx+3], byteorder='big'); idx += 3
this_dict[r] = the_int & 0x7FFFFF
rom_table.append(this_dict)
if the_int & 0x800000:
break
while len(large_rom_table) < rom_table_start + len(rom_table):
large_rom_table.append(None)
large_rom_table[rom_table_start:rom_table_start+len(rom_table)] = rom_table
if args.pr: print('ROM table entries are %d:%d' % (rom_table_start, rom_table_start+len(rom_table)))
# Figure out where all the ROM references are
rom_exception_table = []
for i in range(10):
the_int = int.from_bytes(data[idx:idx+3], byteorder='big'); idx += 3
if the_int == 0:
break
else:
rom_exception_table.append(the_int)
for code_offset in rom_exception_table:
while 1:
link, which_rom_part = struct.unpack_from('>HH', code, offset=code_offset)
annotations[code_offset] = (4, 'rom_reference', which_rom_part) # offset within large_rom_table
if link == 0: break
code_offset += 4 + 2 * link
tokens = []
# do the exception table
while 1:
opcode = data[idx]; idx += 1
this_is_an_entry = False
if opcode <= 251:
tok = ('distance', opcode * 2)
elif opcode == 252: # skip entries in the jump table
opcode2 = data[idx]; idx += 1
if opcode2 == 0: # end of packed jump table
tok = ('end', None)
elif 1 <= opcode2 <= 254: # number of jump table entries to skip
tok = ('skipjt', opcode2)
elif opcode2 == 255: # word follows with number of jump table entries to skip
opcode3, = struct.unpack_from('>H', data, offset=idx); idx += 2
tok = ('skipjt', opcode3)
elif opcode == 253: # previous was reference list head for this module
tok = ('prev=ref_list_head', None)
elif opcode == 254: # previous was an entry, not a new module
tok = ('prev=entry_not_module', None)
elif opcode == 255: # word distance from current position in the code to next
# entry or module specified in the packed jump table follows
opcode2, = struct.unpack_from('>H', data, offset=idx); idx += 2
tok = ('distance', opcode2)
tokens.append(tok)
if tok[0] == 'end': break
2019-09-06 09:05:09 +00:00
# Mutate the tokens list to merge the 'prev=' tokens
for i in reversed(range(len(tokens) - 1)):
if tokens[i+1][0].startswith('prev='):
assert tokens[i][0] == 'distance'
tokens[i] = (tokens[i][0] + tokens[i+1][0][4:],) + tokens[i][1:]
del tokens[i+1]
# From here on, a 'distance' token can be treated as 'distance=module_end'
2019-03-23 09:26:43 +00:00
if args.pt:
daccum = 0
for i, (a, b) in enumerate(tokens):
2019-09-06 09:05:09 +00:00
if a.startswith('distance'):
2019-03-23 09:26:43 +00:00
daccum += b
print('%02d'%i, a, hex(b), '='+hex(daccum))
elif b is None:
print('%02d'%i, a)
else:
print('%02d'%i, a, hex(b))
2019-03-23 09:23:00 +00:00
jt_offset = 0
cur_offset = 0
modules = []
modules.append(Mod())
modules[-1].start = 0
2019-09-06 09:05:09 +00:00
for tok, arg in tokens:
if tok == 'skipjt':
jt_offset += arg
if tok.startswith('distance'):
cur_offset += arg
if tok == 'distance': # to end of module
modules[-1].stop = cur_offset
modules[-1].jt_entry = jt_offset
modules.append(Mod())
modules[-1].start = cur_offset
jt_offset += 1
if tok == 'distance=entry_not_module':
modules[-1].entry_points.append(Ent())
modules[-1].entry_points[-1].offset = cur_offset
modules[-1].entry_points[-1].jt_entry = jt_offset
jt_offset += 1
if tok == 'distance=ref_list_head':
modules[-1].references.append(cur_offset)
2019-03-23 09:23:00 +00:00
modules.pop()
if modules: assert modules[-1].stop == code_size
for m in modules:
if m.references:
ofs, = m.references
while 1:
dist_to_next, = struct.unpack_from('>H', code, offset=ofs)
dist_to_next &= 0x7FFF
dist_to_next *= 2
if dist_to_next == 0: break
ofs += dist_to_next
m.references.append(ofs)
if args.pm:
for m in modules:
print(m)
edited_code = bytearray(code)
# Now edit the code to look more sexier...
for m in modules:
for r in m.references:
edited_code[r:r+4] = b'NqNq'
if args.oe:
open(args.oe + str(num), 'wb').write(edited_code)
for el in large_rom_table:
assert el is not None
# print(large_jump_table)
# print(large_rom_table)