commit 71563fcf33dcde9bf1730b58485adb8491a9df37 Author: Lephenixnoir Date: Fri Oct 22 17:00:10 2021 +0200 initial patch; 3 functions, reg. alloc-independence, lax order diff --git a/g35pe2_ml_patch.py b/g35pe2_ml_patch.py new file mode 100755 index 0000000..d681a06 --- /dev/null +++ b/g35pe2_ml_patch.py @@ -0,0 +1,323 @@ +#! /usr/bin/env python3 + +import sys + +def align(start, end, alignment): + """Align (, ) on multiples of .""" + start = (start + alignment - 1) & ~(alignment - 1) + end &= ~(alignment - 1) + return start, end + +def hexa(data): + """Readable notation for bytes/bytearray objects""" + return " ".join(f"{x:02x}" for x in data) + +def vardict(vars): + """Readable notation for variable dictionaries""" + return " ".join(f"{k}={v:x}" for k, v in vars.items()) + +#--- + +class Pattern: + def __init__(self, string, nibbles): + self.string = string + self.nibbles = nibbles + + def __repr__(self): + return self.string + +def make_pattern(string): + """Compile a pattern of pure hex intermixed with "" references into a + list of either ints or strings wrapped as a Pattern object.""" + + nibbles = [] + i = 0 + while i < len(string): + if string[i] == "<": + end = string.index(">", i+1) + nibbles.append(string[i+1:end]) + i = end+1 + else: + nibbles.append(int(string[i], 16)) + i += 1 + + return Pattern(string, nibbles) + +def match_pattern(p, data, vars): + """Match against , unifying with variables ; returns + either False or extended set of variables.""" + + if 2 * len(data) != len(p.nibbles): + return False + + pos = 0 + vars = vars.copy() + + for pn in p.nibbles: + dn = (data[pos // 2] >> (0 if pos % 2 else 4)) & 0xf + + if isinstance(pn, str): + if pn in vars and vars[pn] != dn: + return False + vars[pn] = dn + elif pn != dn: + return False + + pos += 1 + + return vars + +def search_pattern(p, data, start, end, *, alignment=1): + """Returns a list of (, ) for all occurrences of pattern

+ in -aligned positions of between and .""" + + start, end = align(start, end, alignment) + n = len(p.nibbles) // 2 + + matches = [] + + for offset in range(start, end, alignment): + v = match_pattern(p, data[offset:offset+n], dict()) + if v != False: + matches.append((offset, v)) + + return matches + +def eval_pattern(p, vars): + """Evaluate

by substituting variables from .""" + + data = [] + for n in p.nibbles: + if isinstance(n, str): + data.append(vars[n]) + else: + data.append(n) + + return bytes((data[2*i] << 4) | data[2*i+1] + for i in range(len(data) // 2)) + +#--- + +class Transform: + def __init__(self, name, patterns): + self.name = name + self.insn = [(make_pattern(p), make_pattern(repl)) + for p, repl in patterns] + +def match_transform(tr, base_vars, data, start, end): + """Matches all instructions from in between and , + returning either (False, []) or a pair (, [...]) + indicating where instructions matched and with what variables.""" + + start, end = align(start, end, 2) + vars = base_vars.copy() + offsets = [] + + for p, repl in tr.insn: + n = len(p.nibbles) // 2 + + for offset in range(start, end, 2): + d = data[offset:offset+n] + v = match_pattern(p, d, vars) + if v != False: + offsets.append(offset) + vars = v + break + else: + return (False, []) + + return (vars, offsets) + +def apply_transform(tr, vars, offsets, data): + """Apply replacements from on in-place, using the and + returned by match_transform().""" + + repls = [repl for p, repl in tr.insn] + replacements = [(offset, eval_pattern(p, vars)) + for offset, p in zip(offsets, repls)] + + for offset, contents in replacements: + n = len(contents) + src = data[offset:offset+n] + print(f" Patching at 0x{offset:08x}: {hexa(src)} -> {hexa(contents)}") + data[offset:offset+n] = contents + +#--- + +def pcrel_compute(pc, disp, *, size=2): + """Computes the target of a @(disp,pc) argument.""" + return (pc & ~(size - 1)) + 4 + (disp * size) + +def pcrel_find(target, data, start, end): + """Finds all instructions that load with @(disp,pc) in + between and ") + v = match_pattern(movw_atdisppc_rn, data[pc:pc+2], dict()) + if v != False: + disp = (v["D1"] << 4) | v["D2"] + if pcrel_compute(pc, disp, size=2) == target: + occurrences.append((pc, v["N"])) + + # mov.l @(disp,pc), rn + movl_atdisppc_rn = make_pattern("d") + v = match_pattern(movl_atdisppc_rn, data[pc:pc+2], dict()) + if v != False: + disp = (v["D1"] << 4) | v["D2"] + if pcrel_compute(pc, disp, size=4) == target: + occurrences.append((pc, v["N"])) + + return occurrences + +#--- + +ML_display_vram = Transform("ML_display_vram", [ + ("e07", "e0a"), # Data register is 10 + ("e00", "e04"), # Y-Up mode is 4 + ("e04", "e08"), # Command register is 8 + ("63", "e00"), # Still 0, but rB was changed; use mov #imm, rn + ("ec0", "e80"), # X-address starts at 0x80 +]) +ML_set_contrast = Transform("ML_set_contrast", [ + ("e06", "e06"), # Old contrast register: 6 (detection only) + ("20", "0009"), # Remove: selection of register 6 + ("20", "0009"), # Remove: write contrast value +]) +ML_get_contrast = Transform("ML_get_contrast", [ + ("e06", "e06"), # Old contrast register: 6 (detection only) + ("20", "0009"), # Remove: selection of register 6 + ("600", "e000"), # Remove: read contrast value; return 0 +]) + +ALL_TRANSFORMS = [ + ML_display_vram, + ML_set_contrast, + ML_get_contrast, +] + +def main(g1a_input, g1a_output): + with open(g1a_input, "rb") as fp: + data = bytearray(fp.read()) + + # Find 0xb4000000 and 0xb4010000 + + t6k11_addr1 = make_pattern("b4000000") + refs1 = search_pattern(t6k11_addr1, data, 0, len(data), alignment=4) + + t6k11_addr2 = make_pattern("b4010000") + refs2 = search_pattern(t6k11_addr2, data, 0, len(data), alignment=4) + + if len(refs1) == 0: + print(f"no reference to T6K11 address {t6k11_addr1} found") + return 1 + + for offset, _ in refs1: + print(f"T6K11 reference address {t6k11_addr1} found at 0x{offset:08x}") + fun_occ = pcrel_find(offset, data, 0, len(data)) + + if len(fun_occ) == 0: + print(" Never loaded") + for (pc, n) in fun_occ: + print(f" 0x{pc:08x}: Load into r{n}") + print() + + # Find function boundaries around, assuming rts marks the end of a function + # and the start of the next (which is resonable considering the size and + # simplicity of these functions) + + # Look up to 32 bytes behind, 128 bytes ahead + LOOKBEHIND = 32 + LOOKAHEAD = 128 + RTS = bytes([0x00, 0x0b]) # 000b rts + + functions = [] + for pc, n in fun_occ: + print(f"Looking for function around 0x{pc:08x}...") + start, end = -1, -1 + + for i in range(0, LOOKBEHIND // 2): + if data[pc-2*(i+1):pc-2*i] == RTS: + start = pc-2*(i+1)+4 + print(f" Starting at 0x{start:08x} after rts") + break + else: + print(f" No rts found up to {LOOKBEHIND} bytes behind") + + for i in range(0, LOOKAHEAD // 2): + if data[pc+2*i:pc+2*(i+1)] == RTS: + end = pc+2*i+4 + print(f" Stopping at 0x{end:08x} after rts") + break + else: + print(f" No rts found up to {LOOKAHEAD} bytes ahead") + + if start >= 0 and end >= 0: + # Find register holding 0xb4000000 + vars = { "D0": n } + print(f" 0xb4000000 loaded in r{vars['D0']}") + + # Find register holding 0xb4010000 + for offset, _ in refs2: + occurrences = pcrel_find(offset, data, start, end) + if len(occurrences) > 0: + vars["D1"] = occurrences[0][1] + print(f" 0xb4010000 loaded in r{vars['D1']}") + break + + functions.append((start, end, vars)) + + patched_functions = 0 + ml_display_vram_found = False + + for start, end, base_vars in functions: + print(f"\nFunction analysis for 0x{start:08x} ... 0x{end:08x}:") + + for tr in ALL_TRANSFORMS: + vars, offsets = match_transform(tr, base_vars, data, start, end) + if vars == False: + continue + + print(f" Identified {tr.name} with [{vardict(vars)}]:") + maxlen = max(len(str(p)) for p, repl in tr.insn) + for offset, (p, repl) in zip(offsets, tr.insn): + n = len(p.nibbles) // 2 + d = data[offset:offset+n] + print(f" Matched {str(p):{maxlen}s} at 0x{offset:08x}:", + hexa(d)) + + apply_transform(tr, vars, offsets, data) + patched_functions += 1 + if tr.name == "ML_display_vram": + ml_display_vram_found = True + + with open(g1a_output, "wb") as fp: + fp.write(data) + + print() + print(f"References found to {t6k11_addr1}:", len(refs1)) + print(f"References found to {t6k11_addr2}:", len(refs2)) + print(f"Functions: {len(fun_occ)} hints, {len(functions)} analyzed, " + f"{patched_functions} patched") + print(f"ML_display_vram found:", ml_display_vram_found) + + if not ml_display_vram_found: + print("ML_display_vram not found!") + return 1 + elif patched_functions == 0: + print("Nothing patched!") + return 1 + else: + print("Success!") + return 0 + +if __name__ == "__main__": + if len(sys.argv) != 3 or "-h" in sys.argv[1:] or "--help" in sys.argv[1:]: + print(f"usage: {sys.argv[0]} ",file=sys.stderr) + sys.exit(0) + sys.exit(main(sys.argv[1], sys.argv[2]))