166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
# discover.py - find additional code entry points that plain recursive descent cannot see:
|
|
# * interrupt / indirect-jump vectors written with LDA #lo / STA vec, LDA #hi / STA vec+1
|
|
# * address tables copied into the operand of a JSR/JMP (self-modifying dispatch)
|
|
# * address tables inside data runs (3+ consecutive words that point at plausible code)
|
|
# Every candidate is only accepted by the caller after a clean transactional trace.
|
|
from m6502 import decode, IMM, ABS, ABX, ABY, IND
|
|
|
|
|
|
def instructionStarts(mem, codeSet):
|
|
starts = {}
|
|
nextStart = 0
|
|
for a in sorted(codeSet):
|
|
if a < nextStart:
|
|
continue
|
|
insn = decode(mem, a)
|
|
if insn.isLegal():
|
|
starts[a] = insn
|
|
nextStart = a + insn.length
|
|
return starts
|
|
|
|
|
|
def vectorEntries(mem, starts, vectorAddrs):
|
|
"""values stored to vector lo/hi bytes via immediate loads -> set of candidate addresses"""
|
|
lo = {v: set() for v in vectorAddrs}
|
|
hi = {v: set() for v in vectorAddrs}
|
|
order = sorted(starts)
|
|
for i, a in enumerate(order):
|
|
insn = starts[a]
|
|
if insn.mnemonic not in ("sta", "stx", "sty") or insn.mode != ABS:
|
|
continue
|
|
t = insn.target
|
|
reg = {"sta": "lda", "stx": "ldx", "sty": "ldy"}[insn.mnemonic]
|
|
# look back up to 4 instructions for the immediate load of the same register
|
|
val = None
|
|
for j in range(i - 1, max(-1, i - 5), -1):
|
|
p = starts[order[j]]
|
|
if p.mnemonic == reg and p.mode == IMM:
|
|
val = p.operand
|
|
break
|
|
if p.mnemonic in ("jsr", "jmp", "rts", "rti") or (p.mnemonic == reg):
|
|
break
|
|
if val is None:
|
|
continue
|
|
if t in lo:
|
|
lo[t].add(val)
|
|
if (t - 1) in hi:
|
|
hi[t - 1].add(val)
|
|
out = set()
|
|
for v in vectorAddrs:
|
|
for l in lo[v]:
|
|
for h in hi[v]:
|
|
out.add(l | (h << 8))
|
|
return out
|
|
|
|
|
|
def patchedCallTables(mem, starts):
|
|
"""Find tables whose entries are copied into the operand of a JSR/JMP.
|
|
|
|
Two shapes exist:
|
|
word table lda tbl,x / sta jsr+1 ; lda tbl+1,x / sta jsr+2 (x = index*2)
|
|
split table lda lo,x / sta jsr+1 ; lda hi,x / sta jsr+2 (x = index)
|
|
Returns a list of (kind, baseLo, baseHi) with kind in {"word", "split"}.
|
|
"""
|
|
order = sorted(starts)
|
|
pos = {a: i for i, a in enumerate(order)}
|
|
found = []
|
|
for a, insn in starts.items():
|
|
if insn.mnemonic != "sta" or insn.mode != ABS:
|
|
continue
|
|
target = insn.target # operand byte being patched
|
|
for back in (1, 2):
|
|
base = target - back
|
|
if base not in starts:
|
|
continue
|
|
callInsn = starts[base]
|
|
if callInsn.mnemonic not in ("jsr", "jmp") or callInsn.mode != ABS:
|
|
continue
|
|
if back != 1:
|
|
continue # anchor the search on the low-byte store
|
|
loSrc = _sourceTable(order, pos, starts, a)
|
|
hiStore = _findStoreTo(order, pos, starts, a, target + 1)
|
|
if loSrc is None or hiStore is None:
|
|
continue
|
|
hiSrc = _sourceTable(order, pos, starts, hiStore)
|
|
if hiSrc is None:
|
|
continue
|
|
found.append(("word" if hiSrc == loSrc + 1 else "split", loSrc, hiSrc))
|
|
return found
|
|
|
|
|
|
def _sourceTable(order, pos, starts, storeAddr, lookBack=4):
|
|
"""base address of the lda tbl,x / tbl,y that feeds the store at storeAddr"""
|
|
i = pos[storeAddr]
|
|
for j in range(i - 1, max(-1, i - 1 - lookBack), -1):
|
|
p = starts[order[j]]
|
|
if p.mnemonic == "lda" and p.mode in (ABX, ABY):
|
|
return p.target
|
|
if p.mnemonic in ("jsr", "jmp", "rts", "rti", "lda"):
|
|
return None
|
|
return None
|
|
|
|
|
|
def _findStoreTo(order, pos, starts, fromAddr, wantTarget, lookAhead=6):
|
|
"""address of the next sta wantTarget after fromAddr"""
|
|
i = pos[fromAddr]
|
|
for j in range(i + 1, min(len(order), i + 1 + lookAhead)):
|
|
p = starts[order[j]]
|
|
if p.mnemonic == "sta" and p.mode == ABS and p.target == wantTarget:
|
|
return p.addr
|
|
return None
|
|
|
|
|
|
def splitTableEntries(mem, baseLo, baseHi, isPlausible, maxEntries=64):
|
|
"""entries of a split low/high byte table, stopping at the first implausible pair"""
|
|
out = []
|
|
for i in range(maxEntries):
|
|
w = mem[baseLo + i] | (mem[baseHi + i] << 8)
|
|
if not isPlausible(w):
|
|
break
|
|
out.append(w)
|
|
return out
|
|
|
|
|
|
def wordsFrom(mem, base, isPlausible, maxEntries=64):
|
|
"""read consecutive words from base while they look like code addresses"""
|
|
out = []
|
|
a = base
|
|
gaps = 0
|
|
for _ in range(maxEntries):
|
|
w = mem[a] | (mem[a + 1] << 8)
|
|
if isPlausible(w):
|
|
out.append(w)
|
|
gaps = 0
|
|
elif w in (0xFFFF, 0x0000) and gaps == 0 and out:
|
|
gaps += 1 # tolerate a single placeholder entry
|
|
else:
|
|
break
|
|
a += 2
|
|
return out
|
|
|
|
|
|
def addressTablesInData(mem, codeSet, ranges, isPlausible, minRun=3):
|
|
"""scan data bytes inside the given ranges for runs of plausible code addresses"""
|
|
tables = []
|
|
for lo, hi in ranges:
|
|
a = lo
|
|
while a + 1 < hi:
|
|
if a in codeSet:
|
|
a += 1
|
|
continue
|
|
run = []
|
|
b = a
|
|
while b + 1 < hi and b not in codeSet and (b + 1) not in codeSet:
|
|
w = mem[b] | (mem[b + 1] << 8)
|
|
if isPlausible(w):
|
|
run.append(w)
|
|
b += 2
|
|
else:
|
|
break
|
|
if len(run) >= minRun:
|
|
tables.append((a, run))
|
|
a = b
|
|
else:
|
|
a += 1
|
|
return tables
|