initial patch; 3 functions, reg. alloc-independence, lax order

This commit is contained in:
Lephenixnoir 2021-10-22 17:00:10 +02:00
commit 71563fcf33
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
1 changed files with 323 additions and 0 deletions

323
g35pe2_ml_patch.py Executable file
View File

@ -0,0 +1,323 @@
#! /usr/bin/env python3
import sys
def align(start, end, alignment):
"""Align (<start>, <end>) on multiples of <alignment>."""
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 "<var>" 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 <pattern> against <data>, unifying with variables <vars>; 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 (<offset>, <vars>) for all occurrences of pattern <p>
in <alignment>-aligned positions of <data> between <start> and <end>."""
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 <p> by substituting variables from <vars>."""
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 <tr> in <data> between <start> and <end>,
returning either (False, []) or a pair (<vars>, [<offset>...])
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 <tr> on <data> in-place, using the <offsets> and
<vars> 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 <target> with @(disp,pc) in <data>
between <start> and <end."""
start, end = align(start, end, 2)
occurrences = []
for pc in range(start, end, 2):
# mov.w @(disp,pc), rn
movw_atdisppc_rn = make_pattern("9<N><D1><D2>")
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<N><D1><D2>")
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", [
("e<A>07", "e<A>0a"), # Data register is 10
("e<B>00", "e<B>04"), # Y-Up mode is 4
("e<C>04", "e<C>08"), # Command register is 8
("6<D><B>3", "e<D>00"), # Still 0, but rB was changed; use mov #imm, rn
("e<E>c0", "e<E>80"), # X-address starts at 0x80
])
ML_set_contrast = Transform("ML_set_contrast", [
("e<A>06", "e<A>06"), # Old contrast register: 6 (detection only)
("2<D0><A>0", "0009"), # Remove: selection of register 6
("2<D1><B>0", "0009"), # Remove: write contrast value
])
ML_get_contrast = Transform("ML_get_contrast", [
("e<A>06", "e<A>06"), # Old contrast register: 6 (detection only)
("2<D0><A>0", "0009"), # Remove: selection of register 6
("60<D1>0", "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]} <input g1a> <output g1a>",file=sys.stderr)
sys.exit(0)
sys.exit(main(sys.argv[1], sys.argv[2]))