Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

916 wiersze
35KB

  1. #!/usr/bin/env python3
  2. """pcopt - peephole optimizer for Power C object files (.o).
  3. Reads an object produced by the original Power C `cc`, decodes every compiled
  4. function into 6502 instructions, applies rewrites that are checked against a
  5. register/flag liveness analysis, and writes a smaller/faster object with all
  6. offsets, relocations, labels and branches fixed up. Anything it cannot decode
  7. with certainty is left untouched. The original compiler, linker and libraries
  8. are never modified.
  9. usage: pcopt.py in.o out.o [--lib libdir] [--no-rules] [-v]
  10. """
  11. import sys, os, glob
  12. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  13. from objfmt import Obj, pn
  14. # ------------------------------------------------------------------ 6502 table
  15. _T = """
  16. 00 brk imp;01 ora izx;05 ora zp;06 asl zp;08 php imp;09 ora imm;0a asl acc;0d ora abs;0e asl abs;
  17. 10 bpl rel;11 ora izy;15 ora zpx;16 asl zpx;18 clc imp;19 ora aby;1d ora abx;1e asl abx;
  18. 20 jsr abs;21 and izx;24 bit zp;25 and zp;26 rol zp;28 plp imp;29 and imm;2a rol acc;2c bit abs;2d and abs;2e rol abs;
  19. 30 bmi rel;31 and izy;35 and zpx;36 rol zpx;38 sec imp;39 and aby;3d and abx;3e rol abx;
  20. 40 rti imp;41 eor izx;45 eor zp;46 lsr zp;48 pha imp;49 eor imm;4a lsr acc;4c jmp abs;4d eor abs;4e lsr abs;
  21. 50 bvc rel;51 eor izy;55 eor zpx;56 lsr zpx;58 cli imp;59 eor aby;5d eor abx;5e lsr abx;
  22. 60 rts imp;61 adc izx;65 adc zp;66 ror zp;68 pla imp;69 adc imm;6a ror acc;6c jmp ind;6d adc abs;6e ror abs;
  23. 70 bvs rel;71 adc izy;75 adc zpx;76 ror zpx;78 sei imp;79 adc aby;7d adc abx;7e ror abx;
  24. 81 sta izx;84 sty zp;85 sta zp;86 stx zp;88 dey imp;8a txa imp;8c sty abs;8d sta abs;8e stx abs;
  25. 90 bcc rel;91 sta izy;94 sty zpx;95 sta zpx;96 stx zpy;98 tya imp;99 sta aby;9a txs imp;9d sta abx;
  26. a0 ldy imm;a1 lda izx;a2 ldx imm;a4 ldy zp;a5 lda zp;a6 ldx zp;a8 tay imp;a9 lda imm;aa tax imp;ac ldy abs;ad lda abs;ae ldx abs;
  27. b0 bcs rel;b1 lda izy;b4 ldy zpx;b5 lda zpx;b6 ldx zpy;b8 clv imp;b9 lda aby;ba tsx imp;bc ldy abx;bd lda abx;be ldx aby;
  28. c0 cpy imm;c1 cmp izx;c4 cpy zp;c5 cmp zp;c6 dec zp;c8 iny imp;c9 cmp imm;ca dex imp;cc cpy abs;cd cmp abs;ce dec abs;
  29. d0 bne rel;d1 cmp izy;d5 cmp zpx;d6 dec zpx;d8 cld imp;d9 cmp aby;dd cmp abx;de dec abx;
  30. e0 cpx imm;e1 sbc izx;e4 cpx zp;e5 sbc zp;e6 inc zp;e8 inx imp;e9 sbc imm;ea nop imp;ec cpx abs;ed sbc abs;ee inc abs;
  31. f0 beq rel;f1 sbc izy;f5 sbc zpx;f6 inc zpx;f8 sed imp;f9 sbc aby;fd sbc abx;fe inc abx
  32. """
  33. DEC = {} # opcode -> (mnemonic, mode)
  34. ENC = {} # (mnemonic, mode) -> opcode
  35. for _item in _T.replace('\n', '').split(';'):
  36. _item = _item.strip()
  37. if not _item:
  38. continue
  39. _c, _mn, _md = _item.split()
  40. DEC[int(_c, 16)] = (_mn, _md)
  41. ENC[(_mn, _md)] = int(_c, 16)
  42. SIZE = {'imp': 1, 'acc': 1, 'imm': 2, 'zp': 2, 'zpx': 2, 'zpy': 2, 'izx': 2, 'izy': 2, 'rel': 2,
  43. 'abs': 3, 'abx': 3, 'aby': 3, 'ind': 3}
  44. BRANCH = {'bpl', 'bmi', 'bvc', 'bvs', 'bcc', 'bcs', 'bne', 'beq'}
  45. INVERT = {'bpl': 'bmi', 'bmi': 'bpl', 'bvc': 'bvs', 'bvs': 'bvc', 'bcc': 'bcs', 'bcs': 'bcc', 'bne': 'beq', 'beq': 'bne'}
  46. REGS = {'A', 'X', 'Y', 'C', 'N', 'Z', 'V'}
  47. NZ = {'N', 'Z'}
  48. class I:
  49. """one instruction. tgt = target instruction for branches and local jmp/jsr"""
  50. __slots__ = ('mn', 'md', 'val', 'ref', 'reloc', 'tgt', 'old', 'new')
  51. def __init__(s, mn, md, val=0, ref=None, reloc=False, tgt=None, old=None):
  52. s.mn, s.md, s.val, s.ref, s.reloc, s.tgt, s.old = mn, md, val, ref, reloc, tgt, old
  53. s.new = None
  54. @property
  55. def size(s):
  56. return SIZE[s.md]
  57. def __repr__(s):
  58. return '%s %s %s%s%s' % (s.mn, s.md, hex(s.val), ' ref=%s' % (s.ref,) if s.ref else '', ' R' if s.reloc else '')
  59. def replace(insts, k, n, new):
  60. """replace insts[k:k+n] by the list new, retargeting anything that pointed at the first old one"""
  61. first = insts[k]
  62. if new:
  63. for x in insts:
  64. if x.tgt is first:
  65. x.tgt = new[0]
  66. else:
  67. nxt = insts[k + n] if k + n < len(insts) else None
  68. for x in insts:
  69. if x.tgt is first:
  70. x.tgt = nxt
  71. insts[k:k + n] = new
  72. # ------------------------------------------------------------------ effects
  73. def effects(i):
  74. """(reads, writes) over A X Y C N Z V for one non-flow-ending instruction"""
  75. mn, md = i.mn, i.md
  76. r, w = set(), set()
  77. if md in ('abx', 'zpx', 'izx'):
  78. r.add('X')
  79. if md in ('aby', 'zpy', 'izy'):
  80. r.add('Y')
  81. if mn == 'lda': w |= {'A', 'N', 'Z'}
  82. elif mn == 'ldx': w |= {'X', 'N', 'Z'}
  83. elif mn == 'ldy': w |= {'Y', 'N', 'Z'}
  84. elif mn == 'sta': r.add('A')
  85. elif mn == 'stx': r.add('X')
  86. elif mn == 'sty': r.add('Y')
  87. elif mn == 'tax': r.add('A'); w |= {'X', 'N', 'Z'}
  88. elif mn == 'tay': r.add('A'); w |= {'Y', 'N', 'Z'}
  89. elif mn == 'txa': r.add('X'); w |= {'A', 'N', 'Z'}
  90. elif mn == 'tya': r.add('Y'); w |= {'A', 'N', 'Z'}
  91. elif mn in ('adc', 'sbc'): r |= {'A', 'C'}; w |= {'A', 'C', 'N', 'Z', 'V'}
  92. elif mn in ('and', 'ora', 'eor'): r.add('A'); w |= {'A', 'N', 'Z'}
  93. elif mn == 'cmp': r.add('A'); w |= {'C', 'N', 'Z'}
  94. elif mn == 'cpx': r.add('X'); w |= {'C', 'N', 'Z'}
  95. elif mn == 'cpy': r.add('Y'); w |= {'C', 'N', 'Z'}
  96. elif mn == 'bit': r.add('A'); w |= {'N', 'Z', 'V'}
  97. elif mn in ('asl', 'lsr', 'rol', 'ror'):
  98. if md == 'acc':
  99. r.add('A'); w.add('A')
  100. if mn in ('rol', 'ror'):
  101. r.add('C')
  102. w |= {'C', 'N', 'Z'}
  103. elif mn in ('inc', 'dec'): w |= NZ
  104. elif mn in ('inx', 'dex'): r.add('X'); w |= {'X', 'N', 'Z'}
  105. elif mn in ('iny', 'dey'): r.add('Y'); w |= {'Y', 'N', 'Z'}
  106. elif mn in ('clc', 'sec'): w.add('C')
  107. elif mn in ('bne', 'beq'): r.add('Z')
  108. elif mn in ('bpl', 'bmi'): r.add('N')
  109. elif mn in ('bcc', 'bcs'): r.add('C')
  110. elif mn in ('bvc', 'bvs'): r.add('V')
  111. elif mn == 'pha': r.add('A')
  112. elif mn == 'pla': w |= {'A', 'N', 'Z'}
  113. elif mn == 'nop': pass
  114. else:
  115. raise ValueError('effects: unhandled ' + mn)
  116. return r, w
  117. TEMP = 0x033C
  118. HELPER_READS = {} # runtime symbol -> registers it may read on entry
  119. CALL_READS = {'A', 'X', 'Y'}
  120. def helper_reads(code, start, refs_at=None, reloc=None):
  121. """registers a runtime helper may read (on some path) before writing them.
  122. Explores all paths through the helper; jsr/jmp to unknown code counts as a read of A,X,Y."""
  123. result = set()
  124. for r in REGS:
  125. seen = set()
  126. stack = [start]
  127. live = False
  128. steps = 0
  129. while stack and not live:
  130. p = stack.pop()
  131. while True:
  132. steps += 1
  133. if steps > 3000 or p >= len(code) or p in seen:
  134. live = live or (steps > 3000 or p >= len(code))
  135. break
  136. seen.add(p)
  137. op = code[p]
  138. if op not in DEC:
  139. live = True
  140. break
  141. mn, md = DEC[op]
  142. n = SIZE[md]
  143. if mn == 'rts':
  144. break
  145. if mn in ('jmp', 'jsr'):
  146. if md == 'ind':
  147. live = True
  148. break
  149. tgt = code[p + 1] | code[p + 2] << 8
  150. if refs_at is not None and p in refs_at:
  151. if r in CALL_READS:
  152. live = True
  153. if mn == 'jmp':
  154. break
  155. p += n
  156. continue
  157. if reloc is not None and p in reloc: # a jump/call inside this object
  158. if mn == 'jmp':
  159. p = tgt
  160. continue
  161. if mn == 'jsr': # nested call: A,X,Y are its arguments, flags never are
  162. if r in CALL_READS:
  163. live = True
  164. break
  165. p += n
  166. continue
  167. live = True # unresolved target: be conservative
  168. break
  169. if mn in ('rti', 'brk', 'php', 'plp', 'tsx', 'txs', 'sei', 'cli', 'sed', 'cld') or md == 'ind':
  170. live = True
  171. break
  172. try:
  173. rr, ww = effects(I(mn, md))
  174. except ValueError:
  175. live = True
  176. break
  177. if r in rr:
  178. live = True
  179. break
  180. if r in ww:
  181. break
  182. if mn in BRANCH:
  183. off = code[p + 1]
  184. stack.append(p + 2 + (off - 256 if off > 127 else off))
  185. p += n
  186. if live:
  187. result.add(r)
  188. return result
  189. def load_lib(libdir):
  190. for f in glob.glob(os.path.join(libdir, '*.obj')):
  191. try:
  192. o = Obj.parse(open(f, 'rb').read())
  193. except Exception:
  194. continue
  195. for nm, kind, off in o.defs:
  196. if nm.startswith(b'C$') and kind == 1 and off < len(o.code):
  197. HELPER_READS[bytes(nm)] = helper_reads(o.code, off, set(x[2] for x in o.refs), set(o.reloc))
  198. # ------------------------------------------------------------------ function discovery / decode
  199. def find_functions(o):
  200. code = o.code
  201. starts = sorted(set(off for nm, k, off in o.defs
  202. if off + 1 < len(code) and code[off] == 0x85 and code[off + 1] == 0xfb))
  203. ret_sites = sorted(off for nm, k, off in o.refs if bytes(nm) == b'C$106' and k == 0)
  204. funcs = []
  205. for n, s in enumerate(starts):
  206. limit = starts[n + 1] if n + 1 < len(starts) else len(code)
  207. cand = [r for r in ret_sites if s <= r < limit]
  208. if not cand:
  209. continue
  210. end = cand[-1] + 3
  211. if end > limit:
  212. continue
  213. funcs.append((s, end))
  214. return funcs
  215. def decode_func(o, start, end):
  216. code = o.code
  217. refs_at = {}
  218. for nm, kind, off in o.refs:
  219. if start <= off < end:
  220. refs_at.setdefault(off, []).append((bytes(nm), kind))
  221. reloc_at = set(x for x in o.reloc if start <= x < end)
  222. insts, pos, at = [], start, {}
  223. while pos < end:
  224. op = code[pos]
  225. if op not in DEC:
  226. return None
  227. mn, md = DEC[op]
  228. n = SIZE[md]
  229. if pos + n > end:
  230. return None
  231. v = 0
  232. if n == 2: v = code[pos + 1]
  233. elif n == 3: v = code[pos + 1] | code[pos + 2] << 8
  234. refs = refs_at.get(pos, [])
  235. if len(refs) > 1:
  236. return None
  237. ref = refs[0] if refs else None
  238. if md == 'ind' or mn in ('rti', 'brk', 'php', 'plp', 'pha', 'pla', 'txs', 'tsx', 'sei', 'cli', 'sed', 'cld'):
  239. return None
  240. i = I(mn, md, v, ref, pos in reloc_at, None, pos)
  241. if ref and md not in ('abs', 'abx', 'aby', 'imm'):
  242. return None
  243. if i.reloc and md not in ('abs', 'abx', 'aby'):
  244. return None
  245. at[pos] = i
  246. insts.append(i)
  247. pos += n
  248. if pos != end:
  249. return None
  250. for p in refs_at:
  251. if p not in at: return None
  252. for p in reloc_at:
  253. if p not in at: return None
  254. for i in insts:
  255. if i.md == 'rel':
  256. t = i.old + 2 + (i.val - 256 if i.val > 127 else i.val)
  257. if t not in at: return None
  258. i.tgt = at[t]
  259. elif i.reloc and i.mn in ('jmp', 'jsr'):
  260. if start <= i.val < end:
  261. if i.val not in at: return None
  262. i.tgt = at[i.val]
  263. elif i.reloc and start <= i.val < end:
  264. return None
  265. return insts
  266. # ------------------------------------------------------------------ analysis
  267. def targets_of(insts):
  268. return set(id(i.tgt) for i in insts if i.tgt is not None)
  269. def dead_after(insts, k, regs, tset=None):
  270. """subset of regs that no path leaving insts[k] can read before it is rewritten (conservative:
  271. unknown control flow counts as a read)"""
  272. return dead_from(insts, [k + 1], regs)
  273. def dead_from(insts, starts, regs):
  274. """like dead_after but from explicit start indices (all of them must be dead)"""
  275. index = {id(x): n for n, x in enumerate(insts)}
  276. out = set()
  277. for r in regs:
  278. seen = set()
  279. stack = list(starts)
  280. live = False
  281. steps = 0
  282. while stack and not live:
  283. j = stack.pop()
  284. while True:
  285. steps += 1
  286. if steps > 4000:
  287. live = True
  288. break
  289. if j >= len(insts):
  290. live = True
  291. break
  292. if j in seen:
  293. break
  294. seen.add(j)
  295. i = insts[j]
  296. mn = i.mn
  297. if mn == 'rts':
  298. live = True
  299. break
  300. if mn == 'jmp':
  301. if i.tgt is not None:
  302. j = index[id(i.tgt)]
  303. continue
  304. rd = HELPER_READS.get(i.ref[0]) if (i.ref and i.ref[1] == 0) else None
  305. if rd is None:
  306. rd = CALL_READS # unknown callee: A,X,Y are arguments, flags never are
  307. if r in rd:
  308. live = True
  309. break
  310. if mn == 'jsr':
  311. rd = HELPER_READS.get(i.ref[0]) if (i.ref and i.ref[1] == 0) else None
  312. if rd is None:
  313. rd = CALL_READS
  314. if r in rd:
  315. live = True
  316. break
  317. j += 1
  318. continue
  319. rr, ww = effects(i)
  320. if r in rr:
  321. live = True
  322. break
  323. if r in ww:
  324. break # killed on this path
  325. if mn in BRANCH:
  326. stack.append(index[id(i.tgt)])
  327. j += 1
  328. if not live:
  329. out.add(r)
  330. return out
  331. def temp_dead_after(insts, k, lo, hi):
  332. """is temp-stack memory [lo,hi) provably not read on any path after insts[k]?
  333. Reaching the function epilogue (jmp C$106) rewrites the temp stack, so that ends a path."""
  334. return temp_dead_from(insts, [k + 1], lo, hi)
  335. def temp_dead_from(insts, starts, lo, hi):
  336. index = {id(x): n for n, x in enumerate(insts)}
  337. seen = set()
  338. stack = list(starts)
  339. steps = 0
  340. while stack:
  341. j = stack.pop()
  342. while True:
  343. steps += 1
  344. if steps > 4000 or j >= len(insts):
  345. return False
  346. if j in seen:
  347. break
  348. seen.add(j)
  349. i = insts[j]
  350. mn = i.mn
  351. if mn == 'jmp':
  352. if i.tgt is not None:
  353. j = index[id(i.tgt)]
  354. continue
  355. if i.ref and i.ref[0] == b'C$106' and i.ref[1] == 0:
  356. break
  357. return False
  358. if mn == 'rts':
  359. return False
  360. if mn == 'jsr':
  361. # a helper/function reads only the top A bytes of the temp stack
  362. prev = insts[j - 1] if j > 0 else None
  363. if (prev is not None and prev.mn == 'lda' and prev.md == 'imm' and not prev.ref
  364. and TEMP + prev.val <= lo):
  365. j += 1
  366. continue
  367. return False
  368. if i.md in ('abs', 'abx', 'aby') and not i.ref and not i.reloc and lo <= i.val < hi:
  369. if i.mn in ('sta', 'stx', 'sty') and i.md == 'abs':
  370. break # overwritten before any read on this path
  371. return False
  372. if i.md in ('abx', 'aby') and not i.ref and not i.reloc and i.val < hi and i.val + 255 >= lo:
  373. return False
  374. if mn in BRANCH:
  375. stack.append(index[id(i.tgt)])
  376. j += 1
  377. return True
  378. def mem_key(i):
  379. return (i.md, i.val, i.ref, i.reloc)
  380. # ------------------------------------------------------------------ rules
  381. def rule_forward(insts, stats):
  382. """X/Y already hold the value of a memory slot: drop the reload / use txa,tya"""
  383. tset = targets_of(insts)
  384. xmem, ymem = set(), set()
  385. for k, i in enumerate(insts):
  386. if id(i) in tset:
  387. xmem.clear(); ymem.clear()
  388. mn, md = i.mn, i.md
  389. if md in ('abs', 'zp') and not i.ref and not i.reloc:
  390. key = mem_key(i)
  391. if mn == 'ldx' and key in xmem or mn == 'ldy' and key in ymem:
  392. if dead_after(insts, k, NZ, tset) == NZ:
  393. replace(insts, k, 1, [])
  394. stats['reload'] += 1
  395. return True
  396. if mn == 'lda' and (key in xmem or key in ymem):
  397. n = I('txa' if key in xmem else 'tya', 'imp', old=i.old)
  398. replace(insts, k, 1, [n])
  399. stats['lda->t'] += 1
  400. return True
  401. if mn in ('jsr', 'jmp'):
  402. xmem.clear(); ymem.clear()
  403. continue
  404. if mn in ('sta', 'stx', 'sty') and md in ('izx', 'izy', 'abx', 'aby', 'zpx', 'zpy'):
  405. xmem.clear(); ymem.clear() # may alias anything
  406. continue
  407. r, w = effects(i)
  408. if 'X' in w: xmem.clear()
  409. if 'Y' in w: ymem.clear()
  410. if mn in ('stx', 'sty', 'sta', 'inc', 'dec', 'asl', 'lsr', 'rol', 'ror') and md in ('abs', 'zp'):
  411. key = mem_key(i)
  412. xmem.discard(key); ymem.discard(key)
  413. if not i.ref and not i.reloc:
  414. if mn == 'stx': xmem.add(key)
  415. if mn == 'sty': ymem.add(key)
  416. elif mn in ('stx', 'sty') or i.reloc or i.ref:
  417. # a relocated store to a variable: the slot is still a fixed address
  418. if mn == 'stx': xmem.add(key)
  419. if mn == 'sty': ymem.add(key)
  420. return False
  421. def rule_jmp_next(insts, stats):
  422. for k in range(len(insts) - 1):
  423. i = insts[k]
  424. if i.mn == 'jmp' and i.tgt is not None and i.tgt is insts[k + 1] and not i.ref:
  425. replace(insts, k, 1, [])
  426. stats['jmp-next'] += 1
  427. return True
  428. return False
  429. def rule_zero_pair(insts, stats):
  430. """ldx #K; ldy #K; stx M; sty N (X,Y,A dead after) -> lda #K; sta M; sta N"""
  431. tset = targets_of(insts)
  432. for k in range(len(insts) - 3):
  433. a, b, c, d = insts[k:k + 4]
  434. if not (a.mn == 'ldx' and a.md == 'imm' and b.mn == 'ldy' and b.md == 'imm' and not a.ref and not b.ref):
  435. continue
  436. if a.val != b.val:
  437. continue
  438. if not (c.mn == 'stx' and d.mn == 'sty' and c.md in ('abs', 'zp') and d.md in ('abs', 'zp')):
  439. continue
  440. if any(id(x) in tset for x in (b, c, d)):
  441. continue
  442. want = {'A', 'X', 'Y', 'N', 'Z'}
  443. if dead_after(insts, k + 3, want, tset) != want:
  444. continue
  445. new = [I('lda', 'imm', a.val, old=a.old),
  446. I('sta', c.md, c.val, c.ref, c.reloc, old=c.old),
  447. I('sta', d.md, d.val, d.ref, d.reloc, old=d.old)]
  448. # c and d may be branch targets only if they were in tset (checked); a was handled by replace()
  449. replace(insts, k, 4, new)
  450. stats['zero-pair'] += 1
  451. return True
  452. return False
  453. def rule_inc16(insts, stats):
  454. """clc;txa;adc #1;tax;tya;adc #0;tay -> inx;bne +1;iny (A,C,N,Z,V dead after)"""
  455. pat = [('clc', 'imp'), ('txa', 'imp'), ('adc', 'imm'), ('tax', 'imp'), ('tya', 'imp'), ('adc', 'imm'), ('tay', 'imp')]
  456. tset = targets_of(insts)
  457. for k in range(len(insts) - 7):
  458. w = insts[k:k + 7]
  459. if any((x.mn, x.md) != p for x, p in zip(w, pat)):
  460. continue
  461. if w[2].val != 1 or w[5].val != 0 or any(x.ref or x.reloc for x in w):
  462. continue
  463. if any(id(x) in tset for x in w[1:]):
  464. continue
  465. want = {'A', 'C', 'N', 'Z', 'V'}
  466. if dead_after(insts, k + 6, want, tset) != want:
  467. continue
  468. nxt = insts[k + 7]
  469. inx = I('inx', 'imp', old=w[0].old)
  470. iny = I('iny', 'imp')
  471. bne = I('bne', 'rel', 0, tgt=nxt)
  472. replace(insts, k, 7, [inx, bne, iny])
  473. stats['inc16'] += 1
  474. return True
  475. return False
  476. def rule_dup_ldy(insts, stats):
  477. tset = targets_of(insts)
  478. for k in range(len(insts) - 1):
  479. a, b = insts[k], insts[k + 1]
  480. if (a.mn == b.mn and a.mn in ('ldy', 'ldx') and a.md == b.md == 'imm' and a.val == b.val
  481. and not a.ref and not b.ref and id(b) not in tset):
  482. replace(insts, k + 1, 1, [])
  483. stats['dup-ld'] += 1
  484. return True
  485. return False
  486. def layout_of(insts, base):
  487. lay, p = {}, base
  488. for i in insts:
  489. lay[id(i)] = p
  490. p += i.size
  491. return lay
  492. def rule_branch_over_jmp(insts, base, stats):
  493. """bXX L; jmp T; L: -> b!XX T when T is in branch range"""
  494. tset = targets_of(insts)
  495. lay = layout_of(insts, base)
  496. for k in range(len(insts) - 2):
  497. b, j, l = insts[k], insts[k + 1], insts[k + 2]
  498. if b.mn in BRANCH and b.tgt is l and j.mn == 'jmp' and j.tgt is not None and not j.ref and id(j) not in tset:
  499. rel = lay[id(j.tgt)] - (lay[id(b)] + 2)
  500. if -120 <= rel <= 120:
  501. nb = I(INVERT[b.mn], 'rel', 0, tgt=j.tgt, old=b.old)
  502. replace(insts, k, 2, [nb])
  503. stats['bra-over-jmp'] += 1
  504. return True
  505. return False
  506. # ------------------------------------------------------------------ inline the compare helpers
  507. CMP = {b'C$0063': ('s', 'lt'), b'C$0067': ('s', 'gt'), b'C$0071': ('s', 'le'), b'C$0075': ('s', 'ge'),
  508. b'C$0065': ('u', 'lt'), b'C$0069': ('u', 'gt'), b'C$0073': ('u', 'le'), b'C$0077': ('u', 'ge'),
  509. b'C$0079': ('e', 'eq'), b'C$0082': ('e', 'ne')}
  510. ALLREGS = {'A', 'X', 'Y', 'C', 'N', 'Z', 'V'}
  511. def opd(i):
  512. """operand descriptor of a load instruction"""
  513. return (i.md, i.val, i.ref, i.reloc)
  514. def E(mn, d):
  515. return I(mn, d[0], d[1], d[2], d[3])
  516. def imm_src(i):
  517. """a source that is stable (not temp-stack memory)"""
  518. if i.md == 'imm':
  519. return True
  520. if i.md in ('abs', 'zp'):
  521. return i.ref is not None or i.reloc or not (TEMP <= i.val < 0x0400)
  522. return False
  523. def emit_compare(kind, op, A, B, T, F):
  524. """inline code: falls into T when the comparison holds, else jumps to F"""
  525. def jf():
  526. return I('jmp', 'abs', 0, None, True, F)
  527. if kind == 'e':
  528. if op == 'eq':
  529. j = jf()
  530. return [E('lda', A[0]), E('cmp', B[0]), I('bne', 'rel', 0, tgt=j), E('lda', A[1]), E('cmp', B[1]),
  531. I('beq', 'rel', 0, tgt=T), j]
  532. return [E('lda', A[0]), E('cmp', B[0]), I('bne', 'rel', 0, tgt=T), E('lda', A[1]), E('cmp', B[1]),
  533. I('bne', 'rel', 0, tgt=T), jf()]
  534. swap = op in ('gt', 'le')
  535. p, q = (B, A) if swap else (A, B)
  536. out = [E('lda', p[0]), E('cmp', q[0]), E('lda', p[1]), E('sbc', q[1])]
  537. if kind == 's':
  538. br = I('bmi' if op in ('lt', 'gt') else 'bpl', 'rel', 0, tgt=T)
  539. out += [I('bvc', 'rel', 0, tgt=br), I('eor', 'imm', 0x80), br, jf()]
  540. else:
  541. br = I('bcc' if op in ('lt', 'gt') else 'bcs', 'rel', 0, tgt=T)
  542. out += [br, jf()]
  543. return out
  544. def match_consumer(insts, c):
  545. """the ldx/ldy/txa/bne.. idioms that test a 0/1 result stored at some base; (base, length) or None"""
  546. def slot(i, mn):
  547. return i.mn == mn and i.md == 'abs' and not i.ref and not i.reloc
  548. n = len(insts)
  549. if c + 7 < n:
  550. w = insts[c:c + 7]
  551. if (slot(w[0], 'ldx') and slot(w[1], 'ldy') and w[1].val == w[0].val + 1 and w[2].mn == 'txa'
  552. and w[3].mn == 'bne' and w[4].mn == 'tya' and w[5].mn == 'bne' and w[6].mn == 'jmp'
  553. and w[3].tgt is w[5].tgt and w[6].tgt is not None and not w[6].ref
  554. and w[3].tgt is insts[c + 7]):
  555. return w[0].val, 7
  556. if c + 5 < n:
  557. w = insts[c:c + 5]
  558. if (slot(w[0], 'lda') and slot(w[2], 'lda') and w[0].val == w[2].val + 1 and w[1].mn == 'bne'
  559. and w[3].mn == 'bne' and w[4].mn == 'jmp' and w[1].tgt is w[3].tgt and w[4].tgt is not None
  560. and not w[4].ref and w[1].tgt is insts[c + 5]):
  561. return w[2].val, 5
  562. return None
  563. def push_pair(w, s0):
  564. """ldx src; ldy src; stx s0; sty s0+1 with stable sources -> (lo, hi) operand descriptors"""
  565. if not (w[0].mn == 'ldx' and w[1].mn == 'ldy' and w[2].mn == 'stx' and w[3].mn == 'sty'):
  566. return None
  567. if not (w[2].md == 'abs' and not w[2].ref and not w[2].reloc and w[2].val == s0 and
  568. w[3].md == 'abs' and not w[3].ref and not w[3].reloc and w[3].val == s0 + 1):
  569. return None
  570. if not (imm_src(w[0]) and imm_src(w[1])):
  571. return None
  572. return opd(w[0]), opd(w[1])
  573. def rule_compare(insts, stats):
  574. tset = targets_of(insts)
  575. for j, jm in enumerate(insts):
  576. if jm.mn != 'jsr' or not jm.ref or jm.ref[1] != 0 or jm.ref[0] not in CMP or j == 0:
  577. continue
  578. ld = insts[j - 1]
  579. if not (ld.mn == 'lda' and ld.md == 'imm' and not ld.ref and ld.val >= 4):
  580. continue
  581. base = TEMP + ld.val - 4
  582. m = match_consumer(insts, j + 1)
  583. if not m or m[0] != base:
  584. continue
  585. last = j + m[1] # index of the final jmp F
  586. T = insts[last + 1]
  587. F = insts[last].tgt
  588. if id(jm) in tset or any(id(x) in tset for x in insts[j + 2:last + 1]):
  589. continue
  590. keep = id(insts[j + 1]) in tset # the result test is a join point (&&, ||): keep it
  591. kind, op = CMP[jm.ref[0]]
  592. start = j - 1 # the lda #n
  593. A = [('abs', base, None, False), ('abs', base + 1, None, False)]
  594. B = [('abs', base + 2, None, False), ('abs', base + 3, None, False)]
  595. combined = False
  596. if j - 1 >= 8:
  597. pre = insts[j - 9:j - 1]
  598. pa = push_pair(pre[0:4], base)
  599. pb = push_pair(pre[4:8], base + 2)
  600. if pa and pb and not any(id(x) in tset for x in pre[1:]):
  601. A, B = list(pa), list(pb)
  602. start = j - 9
  603. combined = True
  604. new = emit_compare(kind, op, A, B, T, F)
  605. idxT = last + 1
  606. idxF = [n for n, x in enumerate(insts) if x is F][0]
  607. if dead_from(insts, [idxT, idxF], ALLREGS) != ALLREGS:
  608. continue
  609. # the 0/1 result is no longer stored in the temp stack: nobody may read it afterwards
  610. if not temp_dead_from(insts, [idxT, idxF], base, base + 2):
  611. continue
  612. replace(insts, start, (j if keep else last) + 1 - start, new)
  613. stats['cmp-inline' if combined else 'cmp-slots'] += 1
  614. if keep:
  615. stats['cmp-keep'] += 1
  616. return True
  617. return False
  618. RET_SYM = b'C$RETURN'
  619. def rule_return_copy(insts, stats):
  620. """stx T; sty T+1; lda #2; ldx #2; jsr C$0112 (copy to the return slot) -> stx RET; sty RET+1"""
  621. tset = targets_of(insts)
  622. for k in range(len(insts) - 4):
  623. w = insts[k:k + 5]
  624. if not (w[0].mn == 'stx' and w[1].mn == 'sty' and w[2].mn == 'lda' and w[3].mn == 'ldx' and w[4].mn == 'jsr'):
  625. continue
  626. if not (w[4].ref and w[4].ref[0] == b'C$0112' and w[4].ref[1] == 0):
  627. continue
  628. if not (w[2].md == 'imm' and w[2].val == 2 and not w[2].ref and w[3].md == 'imm' and w[3].val == 2 and not w[3].ref):
  629. continue
  630. if not all(x.md == 'abs' and not x.ref and not x.reloc for x in (w[0], w[1])):
  631. continue
  632. if w[0].val != TEMP or w[1].val != TEMP + 1:
  633. continue
  634. if any(id(x) in tset for x in w[1:]):
  635. continue
  636. if dead_after(insts, k + 4, ALLREGS) != ALLREGS:
  637. continue
  638. if not temp_dead_after(insts, k + 4, TEMP, TEMP + 0x40):
  639. continue
  640. replace(insts, k, 5, [I('stx', 'abs', 0, (RET_SYM, 0), False), I('sty', 'abs', 0, (RET_SYM, 4), False)])
  641. stats['ret-copy'] += 1
  642. return True
  643. return False
  644. def rule_eq_branch(insts, base, stats):
  645. """16 bit ==/!= against a constant: shorten
  646. cx; bne J; cy; beq T; J: jmp F; T: -> cx; bne F; cy; bne F; T:
  647. cx; bne T; cy; bne T; jmp F; T: -> cx; bne T; cy; beq F; T: (F within branch range)"""
  648. tset = targets_of(insts)
  649. lay = layout_of(insts, base)
  650. CMPS = ('cpx', 'cpy', 'cmp')
  651. for k in range(len(insts) - 5):
  652. w = insts[k:k + 6]
  653. if not (w[0].mn in CMPS and w[1].mn == 'bne' and w[2].mn in CMPS and w[3].mn in ('beq', 'bne')
  654. and w[4].mn == 'jmp' and w[4].tgt is not None and not w[4].ref):
  655. continue
  656. F = w[4].tgt
  657. if id(w[2]) in tset or id(w[4]) in tset and w[1].tgt is not w[4]:
  658. continue
  659. rel_ok = lambda src: -120 <= lay[id(F)] - (lay[id(src)] + 2) <= 120
  660. if w[3].mn == 'beq' and w[1].tgt is w[4] and w[3].tgt is w[5]:
  661. # the jmp is only reachable through the first bne
  662. if sum(1 for x in insts if x.tgt is w[4]) != 1 or not (rel_ok(w[1]) and rel_ok(w[3])):
  663. continue
  664. nb1 = I('bne', 'rel', 0, tgt=F, old=w[1].old)
  665. nb3 = I('bne', 'rel', 0, tgt=F, old=w[3].old)
  666. replace(insts, k + 1, 4, [nb1, w[2], nb3])
  667. # w[2] kept in place; fix up the replaced slice: [bne, cy, bne]
  668. stats['eq-branch'] += 1
  669. return True
  670. if w[3].mn == 'bne' and w[1].tgt is w[5] and w[3].tgt is w[5] and id(w[4]) not in tset:
  671. if not rel_ok(w[3]):
  672. continue
  673. nb3 = I('beq', 'rel', 0, tgt=F, old=w[3].old)
  674. replace(insts, k + 3, 2, [nb3])
  675. stats['ne-branch'] += 1
  676. return True
  677. return False
  678. def rule_add_const(insts, stats):
  679. """clc;txa;adc #K;tax;tya;adc #0;tay -> clc;txa;adc #K;tax;bcc +1;iny (A,C,N,Z,V dead after)"""
  680. pat = [('clc', 'imp'), ('txa', 'imp'), ('adc', 'imm'), ('tax', 'imp'), ('tya', 'imp'), ('adc', 'imm'), ('tay', 'imp')]
  681. tset = targets_of(insts)
  682. for k in range(len(insts) - 7):
  683. w = insts[k:k + 7]
  684. if any((x.mn, x.md) != p for x, p in zip(w, pat)):
  685. continue
  686. if w[5].val != 0 or w[5].ref or w[2].ref or w[2].reloc or w[2].val == 1:
  687. continue
  688. if any(id(x) in tset for x in w[1:]):
  689. continue
  690. if dead_after(insts, k + 6, {'A', 'C', 'N', 'Z', 'V'}) != {'A', 'C', 'N', 'Z', 'V'}:
  691. continue
  692. nxt = insts[k + 7]
  693. bcc = I('bcc', 'rel', 0, tgt=nxt)
  694. iny = I('iny', 'imp')
  695. replace(insts, k + 4, 3, [bcc, iny])
  696. stats['add-const'] += 1
  697. return True
  698. return False
  699. def optimize_func(insts, base, stats):
  700. while True:
  701. if rule_compare(insts, stats): continue
  702. if rule_return_copy(insts, stats): continue
  703. if rule_forward(insts, stats): continue
  704. if rule_jmp_next(insts, stats): continue
  705. if rule_zero_pair(insts, stats): continue
  706. if rule_inc16(insts, stats): continue
  707. if rule_dup_ldy(insts, stats): continue
  708. if rule_eq_branch(insts, base, stats): continue
  709. if rule_add_const(insts, stats): continue
  710. if rule_branch_over_jmp(insts, base, stats): continue
  711. break
  712. # ------------------------------------------------------------------ object rewrite
  713. def map_addr(newpos, v):
  714. """new value of a code-relative address operand; tolerates small negative/positive addends (array-1)"""
  715. if v in newpos:
  716. return newpos[v]
  717. for d in (1, 2, 3, 4): # address = target - d (e.g. a[0]-1 indexed with x)
  718. if (v + d) & 0xFFFF in newpos:
  719. return (newpos[(v + d) & 0xFFFF] - d) & 0xFFFF
  720. for d in (1, 2, 3, 4): # address = target + d
  721. if v - d in newpos:
  722. return newpos[v - d] + d
  723. raise ValueError('cannot map address %x' % v)
  724. def optimize_obj(o, rules=True, verbose=False):
  725. stats = {k: 0 for k in ('reload', 'lda->t', 'jmp-next', 'zero-pair', 'inc16', 'dup-ld', 'bra-over-jmp',
  726. 'cmp-inline', 'cmp-slots', 'cmp-keep', 'ret-copy', 'eq-branch', 'ne-branch', 'add-const')}
  727. code = o.code
  728. plan_in = []
  729. for s, e in find_functions(o):
  730. insts = decode_func(o, s, e)
  731. if insts is None:
  732. if verbose: print(' cannot decode function at', hex(s))
  733. continue
  734. plan_in.append((s, e, insts))
  735. if not plan_in:
  736. return o, stats
  737. for s, e, insts in plan_in:
  738. if rules:
  739. optimize_func(insts, s, stats)
  740. # lay out the new code
  741. newcode = bytearray()
  742. newpos = {}
  743. plan = []
  744. pos = 0
  745. for s, e, insts in plan_in:
  746. for b in range(pos, s):
  747. newpos[b] = len(newcode) + (b - pos)
  748. newcode += code[pos:s]
  749. base = len(newcode)
  750. p = base
  751. for i in insts:
  752. i.new = p
  753. p += i.size
  754. for i in insts:
  755. if i.old is not None:
  756. newpos.setdefault(i.old, i.new)
  757. plan.append((s, e, insts, base))
  758. newcode += bytes(p - base)
  759. pos = e
  760. for b in range(pos, len(code)):
  761. newpos[b] = len(newcode) + (b - pos)
  762. newcode += code[pos:]
  763. newpos[len(code)] = len(newcode)
  764. # old instruction starts whose instruction vanished map to the next surviving one
  765. for s, e, insts, base in plan:
  766. pass
  767. inside = lambda off: any(s <= off < e for s, e, _, _ in plan)
  768. new_reloc, new_refs = [], []
  769. for s, e, insts, base in plan:
  770. for i in insts:
  771. b = bytearray([ENC[(i.mn, i.md)]])
  772. if i.md == 'rel':
  773. rel = i.tgt.new - (i.new + 2)
  774. if not -128 <= rel <= 127:
  775. raise ValueError('branch out of range')
  776. b.append(rel & 255)
  777. elif i.size == 2:
  778. b.append(i.val & 255)
  779. elif i.size == 3:
  780. v = i.val
  781. if i.reloc:
  782. v = i.tgt.new if i.tgt is not None else map_addr(newpos, v)
  783. b += bytes((v & 255, v >> 8 & 255))
  784. newcode[i.new:i.new + len(b)] = b
  785. if i.reloc:
  786. new_reloc.append(i.new)
  787. if i.ref:
  788. new_refs.append([i.ref[0], i.ref[1], i.new])
  789. # relocated words that live in data (pointers into the code)
  790. for off in o.reloc:
  791. if inside(off):
  792. continue
  793. n_off = newpos[off]
  794. old_v = code[off + 1] | code[off + 2] << 8
  795. nv = newpos[old_v]
  796. newcode[n_off + 1] = nv & 255
  797. newcode[n_off + 2] = nv >> 8
  798. new_reloc.append(n_off)
  799. for nm, kind, off in o.refs:
  800. if not inside(off):
  801. new_refs.append([nm, kind, newpos[off]])
  802. n = Obj()
  803. n.code = newcode
  804. n.reloc = sorted(new_reloc)
  805. n.defs = [[nm, k, newpos[off]] for nm, k, off in o.defs]
  806. n.refs = sorted(new_refs, key=lambda r: r[2])
  807. n.res = o.res
  808. n.trailing = o.trailing
  809. return n, stats
  810. def main():
  811. args = sys.argv[1:]
  812. verbose = '-v' in args
  813. args = [a for a in args if a != '-v']
  814. rules = '--no-rules' not in args
  815. args = [a for a in args if a != '--no-rules']
  816. if '--lib' in args:
  817. k = args.index('--lib')
  818. load_lib(args[k + 1])
  819. del args[k:k + 2]
  820. src, dst = args[0], args[1]
  821. o = Obj.parse(open(src, 'rb').read())
  822. try:
  823. n, st = optimize_obj(o, rules, verbose)
  824. except Exception as ex:
  825. print('%s: UNCHANGED (%s)' % (os.path.basename(src), ex))
  826. open(dst, 'wb').write(open(src, 'rb').read())
  827. return
  828. open(dst, 'wb').write(n.build())
  829. tot = {k: v for k, v in st.items() if v}
  830. print('%s: code %d -> %d bytes (%d saved) %s' % (os.path.basename(src), len(o.code), len(n.code),
  831. len(o.code) - len(n.code), tot))
  832. if __name__ == '__main__':
  833. main()

Powered by TurnKey Linux.