PageRenderTime 85ms CodeModel.GetById 38ms RepoModel.GetById 0ms app.codeStats 0ms

/diff.py

https://gitlab.com/kholdfuzion/goldeneye_src
Python | 1507 lines | 1445 code | 45 blank | 17 comment | 56 complexity | cf73552a92c71ef2e7f740fce300679f MD5 | raw file
  1. #!/usr/bin/env python3
  2. # PYTHON_ARGCOMPLETE_OK
  3. import argparse
  4. import sys
  5. from typing import (
  6. Any,
  7. Dict,
  8. List,
  9. Match,
  10. NamedTuple,
  11. NoReturn,
  12. Optional,
  13. Set,
  14. Tuple,
  15. Union,
  16. )
  17. def fail(msg: str) -> NoReturn:
  18. print(msg, file=sys.stderr)
  19. sys.exit(1)
  20. # Prefer to use diff_settings.py from the current working directory
  21. sys.path.insert(0, ".")
  22. try:
  23. import diff_settings
  24. except ModuleNotFoundError:
  25. fail("Unable to find diff_settings.py in the same directory.")
  26. sys.path.pop(0)
  27. # ==== COMMAND-LINE ====
  28. try:
  29. import argcomplete # type: ignore
  30. except ModuleNotFoundError:
  31. argcomplete = None
  32. parser = argparse.ArgumentParser(description="Diff MIPS or AArch64 assembly.")
  33. start_argument = parser.add_argument(
  34. "start",
  35. help="Function name or address to start diffing from.",
  36. )
  37. if argcomplete:
  38. def complete_symbol(
  39. prefix: str, parsed_args: argparse.Namespace, **kwargs: object
  40. ) -> List[str]:
  41. if not prefix or prefix.startswith("-"):
  42. # skip reading the map file, which would
  43. # result in a lot of useless completions
  44. return []
  45. config: Dict[str, Any] = {}
  46. diff_settings.apply(config, parsed_args) # type: ignore
  47. mapfile = config.get("mapfile")
  48. if not mapfile:
  49. return []
  50. completes = []
  51. with open(mapfile) as f:
  52. data = f.read()
  53. # assume symbols are prefixed by a space character
  54. search = f" {prefix}"
  55. pos = data.find(search)
  56. while pos != -1:
  57. # skip the space character in the search string
  58. pos += 1
  59. # assume symbols are suffixed by either a space
  60. # character or a (unix-style) line return
  61. spacePos = data.find(" ", pos)
  62. lineReturnPos = data.find("\n", pos)
  63. if lineReturnPos == -1:
  64. endPos = spacePos
  65. elif spacePos == -1:
  66. endPos = lineReturnPos
  67. else:
  68. endPos = min(spacePos, lineReturnPos)
  69. if endPos == -1:
  70. match = data[pos:]
  71. pos = -1
  72. else:
  73. match = data[pos:endPos]
  74. pos = data.find(search, endPos)
  75. completes.append(match)
  76. return completes
  77. setattr(start_argument, "completer", complete_symbol)
  78. parser.add_argument(
  79. "end",
  80. nargs="?",
  81. help="Address to end diff at.",
  82. )
  83. parser.add_argument(
  84. "-o",
  85. dest="diff_obj",
  86. action="store_true",
  87. help="Diff .o files rather than a whole binary. This makes it possible to "
  88. "see symbol names. (Recommended)",
  89. )
  90. parser.add_argument(
  91. "-e",
  92. "--elf",
  93. dest="diff_elf_symbol",
  94. metavar="SYMBOL",
  95. help="Diff a given function in two ELFs, one being stripped and the other "
  96. "one non-stripped. Requires objdump from binutils 2.33+.",
  97. )
  98. parser.add_argument(
  99. "--source",
  100. action="store_true",
  101. help="Show source code (if possible). Only works with -o and -e.",
  102. )
  103. parser.add_argument(
  104. "--inlines",
  105. action="store_true",
  106. help="Show inline function calls (if possible). Only works with -o and -e.",
  107. )
  108. parser.add_argument(
  109. "--base-asm",
  110. dest="base_asm",
  111. metavar="FILE",
  112. help="Read assembly from given file instead of configured base img.",
  113. )
  114. parser.add_argument(
  115. "--write-asm",
  116. dest="write_asm",
  117. metavar="FILE",
  118. help="Write the current assembly output to file, e.g. for use with --base-asm.",
  119. )
  120. parser.add_argument(
  121. "-m",
  122. "--make",
  123. dest="make",
  124. action="store_true",
  125. help="Automatically run 'make' on the .o file or binary before diffing.",
  126. )
  127. parser.add_argument(
  128. "-l",
  129. "--skip-lines",
  130. dest="skip_lines",
  131. type=int,
  132. default=0,
  133. metavar="LINES",
  134. help="Skip the first N lines of output.",
  135. )
  136. parser.add_argument(
  137. "-s",
  138. "--stop-jr-ra",
  139. dest="stop_jrra",
  140. action="store_true",
  141. help="Stop disassembling at the first 'jr ra'. Some functions have multiple return points, so use with care!",
  142. )
  143. parser.add_argument(
  144. "-i",
  145. "--ignore-large-imms",
  146. dest="ignore_large_imms",
  147. action="store_true",
  148. help="Pretend all large enough immediates are the same.",
  149. )
  150. parser.add_argument(
  151. "-I",
  152. "--ignore-addr-diffs",
  153. action="store_true",
  154. help="Ignore address differences. Currently only affects AArch64.",
  155. )
  156. parser.add_argument(
  157. "-B",
  158. "--no-show-branches",
  159. dest="show_branches",
  160. action="store_false",
  161. help="Don't visualize branches/branch targets.",
  162. )
  163. parser.add_argument(
  164. "-S",
  165. "--base-shift",
  166. dest="base_shift",
  167. type=str,
  168. default="0",
  169. help="Diff position X in our img against position X + shift in the base img. "
  170. 'Arithmetic is allowed, so e.g. |-S "0x1234 - 0x4321"| is a reasonable '
  171. "flag to pass if it is known that position 0x1234 in the base img syncs "
  172. "up with position 0x4321 in our img. Not supported together with -o.",
  173. )
  174. parser.add_argument(
  175. "-w",
  176. "--watch",
  177. dest="watch",
  178. action="store_true",
  179. help="Automatically update when source/object files change. "
  180. "Recommended in combination with -m.",
  181. )
  182. parser.add_argument(
  183. "-3",
  184. "--threeway=prev",
  185. dest="threeway",
  186. action="store_const",
  187. const="prev",
  188. help="Show a three-way diff between target asm, current asm, and asm "
  189. "prior to -w rebuild. Requires -w.",
  190. )
  191. parser.add_argument(
  192. "-b",
  193. "--threeway=base",
  194. dest="threeway",
  195. action="store_const",
  196. const="base",
  197. help="Show a three-way diff between target asm, current asm, and asm "
  198. "when diff.py was started. Requires -w.",
  199. )
  200. parser.add_argument(
  201. "--width",
  202. dest="column_width",
  203. type=int,
  204. default=50,
  205. help="Sets the width of the left and right view column.",
  206. )
  207. parser.add_argument(
  208. "--algorithm",
  209. dest="algorithm",
  210. default="levenshtein",
  211. choices=["levenshtein", "difflib"],
  212. help="Diff algorithm to use. Levenshtein gives the minimum diff, while difflib "
  213. "aims for long sections of equal opcodes. Defaults to %(default)s.",
  214. )
  215. parser.add_argument(
  216. "--max-size",
  217. "--max-lines",
  218. dest="max_lines",
  219. type=int,
  220. default=1024,
  221. help="The maximum length of the diff, in lines.",
  222. )
  223. # Project-specific flags, e.g. different versions/make arguments.
  224. add_custom_arguments_fn = getattr(diff_settings, "add_custom_arguments", None)
  225. if add_custom_arguments_fn:
  226. add_custom_arguments_fn(parser)
  227. if argcomplete:
  228. argcomplete.autocomplete(parser)
  229. # ==== IMPORTS ====
  230. # (We do imports late to optimize auto-complete performance.)
  231. import re
  232. import os
  233. import ast
  234. import subprocess
  235. import difflib
  236. import string
  237. import itertools
  238. import threading
  239. import queue
  240. import time
  241. MISSING_PREREQUISITES = (
  242. "Missing prerequisite python module {}. "
  243. "Run `python3 -m pip install --user colorama ansiwrap watchdog python-Levenshtein cxxfilt` to install prerequisites (cxxfilt only needed with --source)."
  244. )
  245. try:
  246. from colorama import Fore, Style, Back # type: ignore
  247. import ansiwrap # type: ignore
  248. import watchdog # type: ignore
  249. except ModuleNotFoundError as e:
  250. fail(MISSING_PREREQUISITES.format(e.name))
  251. # ==== CONFIG ====
  252. args = parser.parse_args()
  253. # Set imgs, map file and make flags in a project-specific manner.
  254. config: Dict[str, Any] = {}
  255. diff_settings.apply(config, args) # type: ignore
  256. arch: str = config.get("arch", "mips")
  257. baseimg: Optional[str] = config.get("baseimg")
  258. myimg: Optional[str] = config.get("myimg")
  259. mapfile: Optional[str] = config.get("mapfile")
  260. makeflags: List[str] = config.get("makeflags", [])
  261. source_directories: Optional[List[str]] = config.get("source_directories")
  262. objdump_executable: Optional[str] = config.get("objdump_executable")
  263. MAX_FUNCTION_SIZE_LINES: int = args.max_lines
  264. MAX_FUNCTION_SIZE_BYTES: int = MAX_FUNCTION_SIZE_LINES * 4
  265. COLOR_ROTATION: List[str] = [
  266. Fore.MAGENTA,
  267. Fore.CYAN,
  268. Fore.GREEN,
  269. Fore.RED,
  270. Fore.LIGHTYELLOW_EX,
  271. Fore.LIGHTMAGENTA_EX,
  272. Fore.LIGHTCYAN_EX,
  273. Fore.LIGHTGREEN_EX,
  274. Fore.LIGHTBLACK_EX,
  275. ]
  276. BUFFER_CMD: List[str] = ["tail", "-c", str(10 ** 9)]
  277. LESS_CMD: List[str] = ["less", "-SRic", "-#6"]
  278. DEBOUNCE_DELAY: float = 0.1
  279. FS_WATCH_EXTENSIONS: List[str] = [".c", ".h"]
  280. # ==== LOGIC ====
  281. ObjdumpCommand = Tuple[List[str], str, Optional[str]]
  282. if args.algorithm == "levenshtein":
  283. try:
  284. import Levenshtein # type: ignore
  285. except ModuleNotFoundError as e:
  286. fail(MISSING_PREREQUISITES.format(e.name))
  287. if args.source:
  288. try:
  289. import cxxfilt # type: ignore
  290. except ModuleNotFoundError as e:
  291. fail(MISSING_PREREQUISITES.format(e.name))
  292. if args.threeway and not args.watch:
  293. fail("Threeway diffing requires -w.")
  294. if objdump_executable is None:
  295. for objdump_cand in ["mips-linux-gnu-objdump", "mips64-elf-objdump"]:
  296. try:
  297. subprocess.check_call(
  298. [objdump_cand, "--version"],
  299. stdout=subprocess.DEVNULL,
  300. stderr=subprocess.DEVNULL,
  301. )
  302. objdump_executable = objdump_cand
  303. break
  304. except subprocess.CalledProcessError:
  305. pass
  306. except FileNotFoundError:
  307. pass
  308. if not objdump_executable:
  309. fail(
  310. "Missing binutils; please ensure mips-linux-gnu-objdump or mips64-elf-objdump exist, or configure objdump_executable."
  311. )
  312. def maybe_eval_int(expr: str) -> Optional[int]:
  313. try:
  314. ret = ast.literal_eval(expr)
  315. if not isinstance(ret, int):
  316. raise Exception("not an integer")
  317. return ret
  318. except Exception:
  319. return None
  320. def eval_int(expr: str, emsg: str) -> int:
  321. ret = maybe_eval_int(expr)
  322. if ret is None:
  323. fail(emsg)
  324. return ret
  325. def eval_line_num(expr: str) -> int:
  326. return int(expr.strip().replace(":", ""), 16)
  327. def run_make(target: str) -> None:
  328. subprocess.check_call(["make"] + makeflags + [target])
  329. def run_make_capture_output(target: str) -> "subprocess.CompletedProcess[bytes]":
  330. return subprocess.run(
  331. ["make"] + makeflags + [target],
  332. stderr=subprocess.PIPE,
  333. stdout=subprocess.PIPE,
  334. )
  335. def restrict_to_function(dump: str, fn_name: str) -> str:
  336. out: List[str] = []
  337. search = f"<{fn_name}>:"
  338. found = False
  339. for line in dump.split("\n"):
  340. if found:
  341. if len(out) >= MAX_FUNCTION_SIZE_LINES:
  342. break
  343. out.append(line)
  344. elif search in line:
  345. found = True
  346. return "\n".join(out)
  347. def maybe_get_objdump_source_flags() -> List[str]:
  348. if not args.source:
  349. return []
  350. flags = [
  351. "--source",
  352. "--source-comment=│ ",
  353. "-l",
  354. ]
  355. if args.inlines:
  356. flags.append("--inlines")
  357. return flags
  358. def run_objdump(cmd: ObjdumpCommand) -> str:
  359. flags, target, restrict = cmd
  360. assert objdump_executable, "checked previously"
  361. out = subprocess.check_output(
  362. [objdump_executable] + arch_flags + flags + [target], universal_newlines=True
  363. )
  364. if restrict is not None:
  365. return restrict_to_function(out, restrict)
  366. return out
  367. base_shift: int = eval_int(
  368. args.base_shift, "Failed to parse --base-shift (-S) argument as an integer."
  369. )
  370. def search_map_file(fn_name: str) -> Tuple[Optional[str], Optional[int]]:
  371. if not mapfile:
  372. fail(f"No map file configured; cannot find function {fn_name}.")
  373. try:
  374. with open(mapfile) as f:
  375. lines = f.read().split("\n")
  376. except Exception:
  377. fail(f"Failed to open map file {mapfile} for reading.")
  378. try:
  379. cur_objfile = None
  380. ram_to_rom = None
  381. cands = []
  382. last_line = ""
  383. for line in lines:
  384. if line.startswith(" .text"):
  385. cur_objfile = line.split()[3]
  386. if "load address" in line:
  387. tokens = last_line.split() + line.split()
  388. ram = int(tokens[1], 0)
  389. rom = int(tokens[5], 0)
  390. ram_to_rom = rom - ram
  391. if line.endswith(" " + fn_name):
  392. ram = int(line.split()[0], 0)
  393. if cur_objfile is not None and ram_to_rom is not None:
  394. cands.append((cur_objfile, ram + ram_to_rom))
  395. last_line = line
  396. except Exception as e:
  397. import traceback
  398. traceback.print_exc()
  399. fail(f"Internal error while parsing map file")
  400. if len(cands) > 1:
  401. fail(f"Found multiple occurrences of function {fn_name} in map file.")
  402. if len(cands) == 1:
  403. return cands[0]
  404. return None, None
  405. def dump_elf() -> Tuple[str, ObjdumpCommand, ObjdumpCommand]:
  406. if not baseimg or not myimg:
  407. fail("Missing myimg/baseimg in config.")
  408. if base_shift:
  409. fail("--base-shift not compatible with -e")
  410. start_addr = eval_int(args.start, "Start address must be an integer expression.")
  411. if args.end is not None:
  412. end_addr = eval_int(args.end, "End address must be an integer expression.")
  413. else:
  414. end_addr = start_addr + MAX_FUNCTION_SIZE_BYTES
  415. flags1 = [
  416. f"--start-address={start_addr}",
  417. f"--stop-address={end_addr}",
  418. ]
  419. flags2 = [
  420. f"--disassemble={args.diff_elf_symbol}",
  421. ]
  422. objdump_flags = ["-drz", "-j", ".text"]
  423. return (
  424. myimg,
  425. (objdump_flags + flags1, baseimg, None),
  426. (objdump_flags + flags2 + maybe_get_objdump_source_flags(), myimg, None),
  427. )
  428. def dump_objfile() -> Tuple[str, ObjdumpCommand, ObjdumpCommand]:
  429. if base_shift:
  430. fail("--base-shift not compatible with -o")
  431. if args.end is not None:
  432. fail("end address not supported together with -o")
  433. if args.start.startswith("0"):
  434. fail("numerical start address not supported with -o; pass a function name")
  435. objfile, _ = search_map_file(args.start)
  436. if not objfile:
  437. fail("Not able to find .o file for function.")
  438. if args.make:
  439. run_make(objfile)
  440. if not os.path.isfile(objfile):
  441. fail(f"Not able to find .o file for function: {objfile} is not a file.")
  442. refobjfile = "expected/" + objfile
  443. if not os.path.isfile(refobjfile):
  444. fail(f'Please ensure an OK .o file exists at "{refobjfile}".')
  445. objdump_flags = ["-drz"]
  446. return (
  447. objfile,
  448. (objdump_flags, refobjfile, args.start),
  449. (objdump_flags + maybe_get_objdump_source_flags(), objfile, args.start),
  450. )
  451. def dump_binary() -> Tuple[str, ObjdumpCommand, ObjdumpCommand]:
  452. if not baseimg or not myimg:
  453. fail("Missing myimg/baseimg in config.")
  454. if args.make:
  455. run_make(myimg)
  456. start_addr = maybe_eval_int(args.start)
  457. if start_addr is None:
  458. _, start_addr = search_map_file(args.start)
  459. if start_addr is None:
  460. fail("Not able to find function in map file.")
  461. if args.end is not None:
  462. end_addr = eval_int(args.end, "End address must be an integer expression.")
  463. else:
  464. end_addr = start_addr + MAX_FUNCTION_SIZE_BYTES
  465. objdump_flags = ["-Dz", "-bbinary", "-EB"]
  466. flags1 = [
  467. f"--start-address={start_addr + base_shift}",
  468. f"--stop-address={end_addr + base_shift}",
  469. ]
  470. flags2 = [f"--start-address={start_addr}", f"--stop-address={end_addr}"]
  471. return (
  472. myimg,
  473. (objdump_flags + flags1, baseimg, None),
  474. (objdump_flags + flags2, myimg, None),
  475. )
  476. def ansi_ljust(s: str, width: int) -> str:
  477. """Like s.ljust(width), but accounting for ANSI colors."""
  478. needed: int = width - ansiwrap.ansilen(s)
  479. if needed > 0:
  480. return s + " " * needed
  481. else:
  482. return s
  483. if arch == "mips":
  484. re_int = re.compile(r"[0-9]+")
  485. re_comment = re.compile(r"<.*?>")
  486. re_reg = re.compile(
  487. r"\$?\b(a[0-3]|t[0-9]|s[0-8]|at|v[01]|f[12]?[0-9]|f3[01]|k[01]|fp|ra)\b"
  488. )
  489. re_sprel = re.compile(r"(?<=,)([0-9]+|0x[0-9a-f]+)\(sp\)")
  490. re_large_imm = re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}")
  491. re_imm = re.compile(r"(\b|-)([0-9]+|0x[0-9a-fA-F]+)\b(?!\(sp)|%(lo|hi)\([^)]*\)")
  492. forbidden = set(string.ascii_letters + "_")
  493. arch_flags = ["-m", "mips:4300"]
  494. branch_likely_instructions = {
  495. "beql",
  496. "bnel",
  497. "beqzl",
  498. "bnezl",
  499. "bgezl",
  500. "bgtzl",
  501. "blezl",
  502. "bltzl",
  503. "bc1tl",
  504. "bc1fl",
  505. }
  506. branch_instructions = branch_likely_instructions.union(
  507. {
  508. "b",
  509. "beq",
  510. "bne",
  511. "beqz",
  512. "bnez",
  513. "bgez",
  514. "bgtz",
  515. "blez",
  516. "bltz",
  517. "bc1t",
  518. "bc1f",
  519. }
  520. )
  521. instructions_with_address_immediates = branch_instructions.union({"jal", "j"})
  522. elif arch == "aarch64":
  523. re_int = re.compile(r"[0-9]+")
  524. re_comment = re.compile(r"(<.*?>|//.*$)")
  525. # GPRs and FP registers: X0-X30, W0-W30, [DSHQ]0..31
  526. # The zero registers and SP should not be in this list.
  527. re_reg = re.compile(r"\$?\b([dshq][12]?[0-9]|[dshq]3[01]|[xw][12]?[0-9]|[xw]30)\b")
  528. re_sprel = re.compile(r"sp, #-?(0x[0-9a-fA-F]+|[0-9]+)\b")
  529. re_large_imm = re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}")
  530. re_imm = re.compile(r"(?<!sp, )#-?(0x[0-9a-fA-F]+|[0-9]+)\b")
  531. arch_flags = []
  532. forbidden = set(string.ascii_letters + "_")
  533. branch_likely_instructions = set()
  534. branch_instructions = {
  535. "bl",
  536. "b",
  537. "b.eq",
  538. "b.ne",
  539. "b.cs",
  540. "b.hs",
  541. "b.cc",
  542. "b.lo",
  543. "b.mi",
  544. "b.pl",
  545. "b.vs",
  546. "b.vc",
  547. "b.hi",
  548. "b.ls",
  549. "b.ge",
  550. "b.lt",
  551. "b.gt",
  552. "b.le",
  553. "cbz",
  554. "cbnz",
  555. "tbz",
  556. "tbnz",
  557. }
  558. instructions_with_address_immediates = branch_instructions.union({"adrp"})
  559. else:
  560. fail("Unknown architecture.")
  561. def hexify_int(row: str, pat: Match[str]) -> str:
  562. full = pat.group(0)
  563. if len(full) <= 1:
  564. # leave one-digit ints alone
  565. return full
  566. start, end = pat.span()
  567. if start and row[start - 1] in forbidden:
  568. return full
  569. if end < len(row) and row[end] in forbidden:
  570. return full
  571. return hex(int(full))
  572. def parse_relocated_line(line: str) -> Tuple[str, str, str]:
  573. try:
  574. ind2 = line.rindex(",")
  575. except ValueError:
  576. ind2 = line.rindex("\t")
  577. before = line[: ind2 + 1]
  578. after = line[ind2 + 1 :]
  579. ind2 = after.find("(")
  580. if ind2 == -1:
  581. imm, after = after, ""
  582. else:
  583. imm, after = after[:ind2], after[ind2:]
  584. if imm == "0x0":
  585. imm = "0"
  586. return before, imm, after
  587. def process_mips_reloc(row: str, prev: str) -> str:
  588. before, imm, after = parse_relocated_line(prev)
  589. repl = row.split()[-1]
  590. if imm != "0":
  591. # MIPS uses relocations with addends embedded in the code as immediates.
  592. # If there is an immediate, show it as part of the relocation. Ideally
  593. # we'd show this addend in both %lo/%hi, but annoyingly objdump's output
  594. # doesn't include enough information to pair up %lo's and %hi's...
  595. # TODO: handle unambiguous cases where all addends for a symbol are the
  596. # same, or show "+???".
  597. mnemonic = prev.split()[0]
  598. if mnemonic in instructions_with_address_immediates and not imm.startswith("0x"):
  599. imm = "0x" + imm
  600. repl += "+" + imm if int(imm, 0) > 0 else imm
  601. if "R_MIPS_LO16" in row:
  602. repl = f"%lo({repl})"
  603. elif "R_MIPS_HI16" in row:
  604. # Ideally we'd pair up R_MIPS_LO16 and R_MIPS_HI16 to generate a
  605. # correct addend for each, but objdump doesn't give us the order of
  606. # the relocations, so we can't find the right LO16. :(
  607. repl = f"%hi({repl})"
  608. else:
  609. assert "R_MIPS_26" in row, f"unknown relocation type '{row}'"
  610. return before + repl + after
  611. def pad_mnemonic(line: str) -> str:
  612. if "\t" not in line:
  613. return line
  614. mn, args = line.split("\t", 1)
  615. return f"{mn:<7s} {args}"
  616. class Line(NamedTuple):
  617. mnemonic: str
  618. diff_row: str
  619. original: str
  620. normalized_original: str
  621. line_num: str
  622. branch_target: Optional[str]
  623. source_lines: List[str]
  624. comment: Optional[str]
  625. class DifferenceNormalizer:
  626. def normalize(self, mnemonic: str, row: str) -> str:
  627. """This should be called exactly once for each line."""
  628. row = self._normalize_arch_specific(mnemonic, row)
  629. if args.ignore_large_imms:
  630. row = re.sub(re_large_imm, "<imm>", row)
  631. return row
  632. def _normalize_arch_specific(self, mnemonic: str, row: str) -> str:
  633. return row
  634. class DifferenceNormalizerAArch64(DifferenceNormalizer):
  635. def __init__(self) -> None:
  636. super().__init__()
  637. self._adrp_pair_registers: Set[str] = set()
  638. def _normalize_arch_specific(self, mnemonic: str, row: str) -> str:
  639. if args.ignore_addr_diffs:
  640. row = self._normalize_adrp_differences(mnemonic, row)
  641. row = self._normalize_bl(mnemonic, row)
  642. return row
  643. def _normalize_bl(self, mnemonic: str, row: str) -> str:
  644. if mnemonic != "bl":
  645. return row
  646. row, _ = split_off_branch(row)
  647. return row
  648. def _normalize_adrp_differences(self, mnemonic: str, row: str) -> str:
  649. """Identifies ADRP + LDR/ADD pairs that are used to access the GOT and
  650. suppresses any immediate differences.
  651. Whenever an ADRP is seen, the destination register is added to the set of registers
  652. that are part of an ADRP + LDR/ADD pair. Registers are removed from the set as soon
  653. as they are used for an LDR or ADD instruction which completes the pair.
  654. This method is somewhat crude but should manage to detect most such pairs.
  655. """
  656. row_parts = row.split("\t", 1)
  657. if mnemonic == "adrp":
  658. self._adrp_pair_registers.add(row_parts[1].strip().split(",")[0])
  659. row, _ = split_off_branch(row)
  660. elif mnemonic == "ldr":
  661. for reg in self._adrp_pair_registers:
  662. # ldr xxx, [reg]
  663. # ldr xxx, [reg, <imm>]
  664. if f", [{reg}" in row_parts[1]:
  665. self._adrp_pair_registers.remove(reg)
  666. return normalize_imms(row)
  667. elif mnemonic == "add":
  668. for reg in self._adrp_pair_registers:
  669. # add reg, reg, <imm>
  670. if row_parts[1].startswith(f"{reg}, {reg}, "):
  671. self._adrp_pair_registers.remove(reg)
  672. return normalize_imms(row)
  673. return row
  674. def make_difference_normalizer() -> DifferenceNormalizer:
  675. if arch == "aarch64":
  676. return DifferenceNormalizerAArch64()
  677. return DifferenceNormalizer()
  678. def process(lines: List[str]) -> List[Line]:
  679. normalizer = make_difference_normalizer()
  680. skip_next = False
  681. source_lines = []
  682. if not args.diff_obj:
  683. lines = lines[7:]
  684. if lines and not lines[-1]:
  685. lines.pop()
  686. output: List[Line] = []
  687. stop_after_delay_slot = False
  688. for row in lines:
  689. if args.diff_obj and (">:" in row or not row):
  690. continue
  691. if args.source and (row and row[0] != " "):
  692. source_lines.append(row)
  693. continue
  694. if "R_AARCH64_" in row:
  695. # TODO: handle relocation
  696. continue
  697. if "R_MIPS_" in row:
  698. # N.B. Don't transform the diff rows, they already ignore immediates
  699. # if output[-1].diff_row != "<delay-slot>":
  700. # output[-1] = output[-1].replace(diff_row=process_mips_reloc(row, output[-1].row_with_imm))
  701. new_original = process_mips_reloc(row, output[-1].original)
  702. output[-1] = output[-1]._replace(original=new_original)
  703. continue
  704. m_comment = re.search(re_comment, row)
  705. comment = m_comment[0] if m_comment else None
  706. row = re.sub(re_comment, "", row)
  707. row = row.rstrip()
  708. tabs = row.split("\t")
  709. row = "\t".join(tabs[2:])
  710. line_num = tabs[0].strip()
  711. row_parts = row.split("\t", 1)
  712. mnemonic = row_parts[0].strip()
  713. if mnemonic not in instructions_with_address_immediates:
  714. row = re.sub(re_int, lambda m: hexify_int(row, m), row)
  715. original = row
  716. normalized_original = normalizer.normalize(mnemonic, original)
  717. if skip_next:
  718. skip_next = False
  719. row = "<delay-slot>"
  720. mnemonic = "<delay-slot>"
  721. if mnemonic in branch_likely_instructions:
  722. skip_next = True
  723. row = re.sub(re_reg, "<reg>", row)
  724. row = re.sub(re_sprel, "addr(sp)", row)
  725. row_with_imm = row
  726. if mnemonic in instructions_with_address_immediates:
  727. row = row.strip()
  728. row, _ = split_off_branch(row)
  729. row += "<imm>"
  730. else:
  731. row = normalize_imms(row)
  732. branch_target = None
  733. if mnemonic in branch_instructions:
  734. target = row_parts[1].strip().split(",")[-1]
  735. if mnemonic in branch_likely_instructions:
  736. target = hex(int(target, 16) - 4)[2:]
  737. branch_target = target.strip()
  738. output.append(
  739. Line(
  740. mnemonic=mnemonic,
  741. diff_row=row,
  742. original=original,
  743. normalized_original=normalized_original,
  744. line_num=line_num,
  745. branch_target=branch_target,
  746. source_lines=source_lines,
  747. comment=comment,
  748. )
  749. )
  750. source_lines = []
  751. if args.stop_jrra and mnemonic == "jr" and row_parts[1].strip() == "ra":
  752. stop_after_delay_slot = True
  753. elif stop_after_delay_slot:
  754. break
  755. return output
  756. def format_single_line_diff(line1: str, line2: str, column_width: int) -> str:
  757. return ansi_ljust(line1, column_width) + line2
  758. class SymbolColorer:
  759. symbol_colors: Dict[str, str]
  760. def __init__(self, base_index: int) -> None:
  761. self.color_index = base_index
  762. self.symbol_colors = {}
  763. def color_symbol(self, s: str, t: Optional[str] = None) -> str:
  764. try:
  765. color = self.symbol_colors[s]
  766. except:
  767. color = COLOR_ROTATION[self.color_index % len(COLOR_ROTATION)]
  768. self.color_index += 1
  769. self.symbol_colors[s] = color
  770. t = t or s
  771. return f"{color}{t}{Fore.RESET}"
  772. def normalize_imms(row: str) -> str:
  773. return re.sub(re_imm, "<imm>", row)
  774. def normalize_stack(row: str) -> str:
  775. return re.sub(re_sprel, "addr(sp)", row)
  776. def split_off_branch(line: str) -> Tuple[str, str]:
  777. parts = line.split(",")
  778. if len(parts) < 2:
  779. parts = line.split(None, 1)
  780. off = len(line) - len(parts[-1])
  781. return line[:off], line[off:]
  782. def color_imms(out1: str, out2: str) -> Tuple[str, str]:
  783. # Call re_imm.sub() and record the calls made to the callback.
  784. # We will assume they happen in the same order further down, when
  785. # we do re_imm.sub() on the same input but with a different callback.
  786. # (Swithing to re_imm.finditer would be cleaner...)
  787. g1 = []
  788. g2 = []
  789. re_imm.sub(lambda m: g1.append(m.group()) or "", out1) # type: ignore
  790. re_imm.sub(lambda m: g2.append(m.group()) or "", out2) # type: ignore
  791. if len(g1) == len(g2):
  792. diffs = [x != y for (x, y) in zip(g1, g2)]
  793. it = iter(diffs)
  794. def maybe_color(s: str) -> str:
  795. return f"{Fore.LIGHTBLUE_EX}{s}{Style.RESET_ALL}" if next(it) else s
  796. out1 = re_imm.sub(lambda m: maybe_color(m.group()), out1)
  797. it = iter(diffs)
  798. out2 = re_imm.sub(lambda m: maybe_color(m.group()), out2)
  799. return out1, out2
  800. def color_branch_imms(br1: str, br2: str) -> Tuple[str, str]:
  801. if br1 != br2:
  802. br1 = f"{Fore.LIGHTBLUE_EX}{br1}{Style.RESET_ALL}"
  803. br2 = f"{Fore.LIGHTBLUE_EX}{br2}{Style.RESET_ALL}"
  804. return br1, br2
  805. def diff_sequences_difflib(
  806. seq1: List[str], seq2: List[str]
  807. ) -> List[Tuple[str, int, int, int, int]]:
  808. differ = difflib.SequenceMatcher(a=seq1, b=seq2, autojunk=False)
  809. return differ.get_opcodes()
  810. def diff_sequences(
  811. seq1: List[str], seq2: List[str]
  812. ) -> List[Tuple[str, int, int, int, int]]:
  813. if (
  814. args.algorithm != "levenshtein"
  815. or len(seq1) * len(seq2) > 4 * 10 ** 8
  816. or len(seq1) + len(seq2) >= 0x110000
  817. ):
  818. return diff_sequences_difflib(seq1, seq2)
  819. # The Levenshtein library assumes that we compare strings, not lists. Convert.
  820. # (Per the check above we know we have fewer than 0x110000 unique elements, so chr() works.)
  821. remapping: Dict[str, str] = {}
  822. def remap(seq: List[str]) -> str:
  823. seq = seq[:]
  824. for i in range(len(seq)):
  825. val = remapping.get(seq[i])
  826. if val is None:
  827. val = chr(len(remapping))
  828. remapping[seq[i]] = val
  829. seq[i] = val
  830. return "".join(seq)
  831. rem1 = remap(seq1)
  832. rem2 = remap(seq2)
  833. return Levenshtein.opcodes(rem1, rem2) # type: ignore
  834. def diff_lines(
  835. lines1: List[Line],
  836. lines2: List[Line],
  837. ) -> List[Tuple[Optional[Line], Optional[Line]]]:
  838. ret = []
  839. for (tag, i1, i2, j1, j2) in diff_sequences(
  840. [line.mnemonic for line in lines1],
  841. [line.mnemonic for line in lines2],
  842. ):
  843. for line1, line2 in itertools.zip_longest(lines1[i1:i2], lines2[j1:j2]):
  844. if tag == "replace":
  845. if line1 is None:
  846. tag = "insert"
  847. elif line2 is None:
  848. tag = "delete"
  849. elif tag == "insert":
  850. assert line1 is None
  851. elif tag == "delete":
  852. assert line2 is None
  853. ret.append((line1, line2))
  854. return ret
  855. class OutputLine:
  856. base: Optional[str]
  857. fmt2: str
  858. key2: Optional[str]
  859. def __init__(self, base: Optional[str], fmt2: str, key2: Optional[str]) -> None:
  860. self.base = base
  861. self.fmt2 = fmt2
  862. self.key2 = key2
  863. def __eq__(self, other: object) -> bool:
  864. if not isinstance(other, OutputLine):
  865. return NotImplemented
  866. return self.key2 == other.key2
  867. def __hash__(self) -> int:
  868. return hash(self.key2)
  869. def do_diff(basedump: str, mydump: str) -> List[OutputLine]:
  870. output: List[OutputLine] = []
  871. lines1 = process(basedump.split("\n"))
  872. lines2 = process(mydump.split("\n"))
  873. sc1 = SymbolColorer(0)
  874. sc2 = SymbolColorer(0)
  875. sc3 = SymbolColorer(4)
  876. sc4 = SymbolColorer(4)
  877. sc5 = SymbolColorer(0)
  878. sc6 = SymbolColorer(0)
  879. bts1: Set[str] = set()
  880. bts2: Set[str] = set()
  881. if args.show_branches:
  882. for (lines, btset, sc) in [
  883. (lines1, bts1, sc5),
  884. (lines2, bts2, sc6),
  885. ]:
  886. for line in lines:
  887. bt = line.branch_target
  888. if bt is not None:
  889. btset.add(bt + ":")
  890. sc.color_symbol(bt + ":")
  891. for (line1, line2) in diff_lines(lines1, lines2):
  892. line_color1 = line_color2 = sym_color = Fore.RESET
  893. line_prefix = " "
  894. if line1 and line2 and line1.diff_row == line2.diff_row:
  895. if line1.normalized_original == line2.normalized_original:
  896. out1 = line1.original
  897. out2 = line2.original
  898. elif line1.diff_row == "<delay-slot>":
  899. out1 = f"{Style.BRIGHT}{Fore.LIGHTBLACK_EX}{line1.original}"
  900. out2 = f"{Style.BRIGHT}{Fore.LIGHTBLACK_EX}{line2.original}"
  901. else:
  902. mnemonic = line1.original.split()[0]
  903. out1, out2 = line1.original, line2.original
  904. branch1 = branch2 = ""
  905. if mnemonic in instructions_with_address_immediates:
  906. out1, branch1 = split_off_branch(line1.original)
  907. out2, branch2 = split_off_branch(line2.original)
  908. branchless1 = out1
  909. branchless2 = out2
  910. out1, out2 = color_imms(out1, out2)
  911. same_relative_target = False
  912. if line1.branch_target is not None and line2.branch_target is not None:
  913. relative_target1 = eval_line_num(line1.branch_target) - eval_line_num(line1.line_num)
  914. relative_target2 = eval_line_num(line2.branch_target) - eval_line_num(line2.line_num)
  915. same_relative_target = relative_target1 == relative_target2
  916. if not same_relative_target:
  917. branch1, branch2 = color_branch_imms(branch1, branch2)
  918. out1 += branch1
  919. out2 += branch2
  920. if normalize_imms(branchless1) == normalize_imms(branchless2):
  921. if not same_relative_target:
  922. # only imms differences
  923. sym_color = Fore.LIGHTBLUE_EX
  924. line_prefix = "i"
  925. else:
  926. out1 = re.sub(
  927. re_sprel, lambda m: sc3.color_symbol(m.group()), out1,
  928. )
  929. out2 = re.sub(
  930. re_sprel, lambda m: sc4.color_symbol(m.group()), out2,
  931. )
  932. if normalize_stack(branchless1) == normalize_stack(branchless2):
  933. # only stack differences (luckily stack and imm
  934. # differences can't be combined in MIPS, so we
  935. # don't have to think about that case)
  936. sym_color = Fore.YELLOW
  937. line_prefix = "s"
  938. else:
  939. # regs differences and maybe imms as well
  940. out1 = re.sub(
  941. re_reg, lambda m: sc1.color_symbol(m.group()), out1
  942. )
  943. out2 = re.sub(
  944. re_reg, lambda m: sc2.color_symbol(m.group()), out2
  945. )
  946. line_color1 = line_color2 = sym_color = Fore.YELLOW
  947. line_prefix = "r"
  948. elif line1 and line2:
  949. line_prefix = "|"
  950. line_color1 = Fore.LIGHTBLUE_EX
  951. line_color2 = Fore.LIGHTBLUE_EX
  952. sym_color = Fore.LIGHTBLUE_EX
  953. out1 = line1.original
  954. out2 = line2.original
  955. elif line1:
  956. line_prefix = "<"
  957. line_color1 = sym_color = Fore.RED
  958. out1 = line1.original
  959. out2 = ""
  960. elif line2:
  961. line_prefix = ">"
  962. line_color2 = sym_color = Fore.GREEN
  963. out1 = ""
  964. out2 = line2.original
  965. if args.source and line2 and line2.comment:
  966. out2 += f" {line2.comment}"
  967. def format_part(
  968. out: str,
  969. line: Optional[Line],
  970. line_color: str,
  971. btset: Set[str],
  972. sc: SymbolColorer,
  973. ) -> Optional[str]:
  974. if line is None:
  975. return None
  976. in_arrow = " "
  977. out_arrow = ""
  978. if args.show_branches:
  979. if line.line_num in btset:
  980. in_arrow = sc.color_symbol(line.line_num, "~>") + line_color
  981. if line.branch_target is not None:
  982. out_arrow = " " + sc.color_symbol(line.branch_target + ":", "~>")
  983. out = pad_mnemonic(out)
  984. return f"{line_color}{line.line_num} {in_arrow} {out}{Style.RESET_ALL}{out_arrow}"
  985. part1 = format_part(out1, line1, line_color1, bts1, sc5)
  986. part2 = format_part(out2, line2, line_color2, bts2, sc6)
  987. key2 = line2.original if line2 else None
  988. mid = f"{sym_color}{line_prefix}"
  989. if line2:
  990. for source_line in line2.source_lines:
  991. color = Style.DIM
  992. # File names and function names
  993. if source_line and source_line[0] != "│":
  994. color += Style.BRIGHT
  995. # Function names
  996. if source_line.endswith("():"):
  997. # Underline. Colorama does not provide this feature, unfortunately.
  998. color += "\u001b[4m"
  999. try:
  1000. source_line = cxxfilt.demangle(
  1001. source_line[:-3], external_only=False
  1002. )
  1003. except:
  1004. pass
  1005. output.append(
  1006. OutputLine(
  1007. None,
  1008. f" {color}{source_line}{Style.RESET_ALL}",
  1009. source_line,
  1010. )
  1011. )
  1012. fmt2 = mid + " " + (part2 or "")
  1013. output.append(OutputLine(part1, fmt2, key2))
  1014. return output
  1015. def chunk_diff(diff: List[OutputLine]) -> List[Union[List[OutputLine], OutputLine]]:
  1016. cur_right: List[OutputLine] = []
  1017. chunks: List[Union[List[OutputLine], OutputLine]] = []
  1018. for output_line in diff:
  1019. if output_line.base is not None:
  1020. chunks.append(cur_right)
  1021. chunks.append(output_line)
  1022. cur_right = []
  1023. else:
  1024. cur_right.append(output_line)
  1025. chunks.append(cur_right)
  1026. return chunks
  1027. def format_diff(
  1028. old_diff: List[OutputLine], new_diff: List[OutputLine]
  1029. ) -> Tuple[str, List[str]]:
  1030. old_chunks = chunk_diff(old_diff)
  1031. new_chunks = chunk_diff(new_diff)
  1032. output: List[Tuple[str, OutputLine, OutputLine]] = []
  1033. assert len(old_chunks) == len(new_chunks), "same target"
  1034. empty = OutputLine("", "", None)
  1035. for old_chunk, new_chunk in zip(old_chunks, new_chunks):
  1036. if isinstance(old_chunk, list):
  1037. assert isinstance(new_chunk, list)
  1038. if not old_chunk and not new_chunk:
  1039. # Most of the time lines sync up without insertions/deletions,
  1040. # and there's no interdiffing to be done.
  1041. continue
  1042. differ = difflib.SequenceMatcher(a=old_chunk, b=new_chunk, autojunk=False)
  1043. for (tag, i1, i2, j1, j2) in differ.get_opcodes():
  1044. if tag in ["equal", "replace"]:
  1045. for i, j in zip(range(i1, i2), range(j1, j2)):
  1046. output.append(("", old_chunk[i], new_chunk[j]))
  1047. if tag in ["insert", "replace"]:
  1048. for j in range(j1 + i2 - i1, j2):
  1049. output.append(("", empty, new_chunk[j]))
  1050. if tag in ["delete", "replace"]:
  1051. for i in range(i1 + j2 - j1, i2):
  1052. output.append(("", old_chunk[i], empty))
  1053. else:
  1054. assert isinstance(new_chunk, OutputLine)
  1055. assert new_chunk.base
  1056. # old_chunk.base and new_chunk.base have the same text since
  1057. # both diffs are based on the same target, but they might
  1058. # differ in color. Use the new version.
  1059. output.append((new_chunk.base, old_chunk, new_chunk))
  1060. # TODO: status line, with e.g. approximate permuter score?
  1061. width = args.column_width
  1062. if args.threeway:
  1063. header_line = "TARGET".ljust(width) + " CURRENT".ljust(width) + " PREVIOUS"
  1064. diff_lines = [
  1065. ansi_ljust(base, width)
  1066. + ansi_ljust(new.fmt2, width)
  1067. + (old.fmt2 or "-" if old != new else "")
  1068. for (base, old, new) in output
  1069. ]
  1070. else:
  1071. header_line = ""
  1072. diff_lines = [
  1073. ansi_ljust(base, width) + new.fmt2
  1074. for (base, old, new) in output
  1075. if base or new.key2 is not None
  1076. ]
  1077. return header_line, diff_lines
  1078. def debounced_fs_watch(
  1079. targets: List[str],
  1080. outq: "queue.Queue[Optional[float]]",
  1081. debounce_delay: float,
  1082. ) -> None:
  1083. import watchdog.events # type: ignore
  1084. import watchdog.observers # type: ignore
  1085. class WatchEventHandler(watchdog.events.FileSystemEventHandler): # type: ignore
  1086. def __init__(
  1087. self, queue: "queue.Queue[float]", file_targets: List[str]
  1088. ) -> None:
  1089. self.queue = queue
  1090. self.file_targets = file_targets
  1091. def on_modified(self, ev: object) -> None:
  1092. if isinstance(ev, watchdog.events.FileModifiedEvent):
  1093. self.changed(ev.src_path)
  1094. def on_moved(self, ev: object) -> None:
  1095. if isinstance(ev, watchdog.events.FileMovedEvent):
  1096. self.changed(ev.dest_path)
  1097. def should_notify(self, path: str) -> bool:
  1098. for target in self.file_targets:
  1099. if path == target:
  1100. return True
  1101. if args.make and any(
  1102. path.endswith(suffix) for suffix in FS_WATCH_EXTENSIONS
  1103. ):
  1104. return True
  1105. return False
  1106. def changed(self, path: str) -> None:
  1107. if self.should_notify(path):
  1108. self.queue.put(time.time())
  1109. def debounce_thread() -> NoReturn:
  1110. listenq: "queue.Queue[float]" = queue.Queue()
  1111. file_targets: List[str] = []
  1112. event_handler = WatchEventHandler(listenq, file_targets)
  1113. observer = watchdog.observers.Observer()
  1114. observed = set()
  1115. for target in targets:
  1116. if os.path.isdir(target):
  1117. observer.schedule(event_handler, target, recursive=True)
  1118. else:
  1119. file_targets.append(target)
  1120. target = os.path.dirname(target) or "."
  1121. if target not in observed:
  1122. observed.add(target)
  1123. observer.schedule(event_handler, target)
  1124. observer.start()
  1125. while True:
  1126. t = listenq.get()
  1127. more = True
  1128. while more:
  1129. delay = t + debounce_delay - time.time()
  1130. if delay > 0:
  1131. time.sleep(delay)
  1132. # consume entire queue
  1133. more = False
  1134. try:
  1135. while True:
  1136. t = listenq.get(block=False)
  1137. more = True
  1138. except queue.Empty:
  1139. pass
  1140. outq.put(t)
  1141. th = threading.Thread(target=debounce_thread, daemon=True)
  1142. th.start()
  1143. class Display:
  1144. basedump: str
  1145. mydump: str
  1146. emsg: Optional[str]
  1147. last_diff_output: Optional[List[OutputLine]]
  1148. pending_update: Optional[Tuple[str, bool]]
  1149. ready_queue: "queue.Queue[None]"
  1150. watch_queue: "queue.Queue[Optional[float]]"
  1151. less_proc: "Optional[subprocess.Popen[bytes]]"
  1152. def __init__(self, basedump: str, mydump: str) -> None:
  1153. self.basedump = basedump
  1154. self.mydump = mydump
  1155. self.emsg = None
  1156. self.last_diff_output = None
  1157. def run_less(self) -> "Tuple[subprocess.Popen[bytes], subprocess.Popen[bytes]]":
  1158. if self.emsg is not None:
  1159. output = self.emsg
  1160. else:
  1161. diff_output = do_diff(self.basedump, self.mydump)
  1162. last_diff_output = self.last_diff_output or diff_output
  1163. if args.threeway != "base" or not self.last_diff_output:
  1164. self.last_diff_output = diff_output
  1165. header, diff_lines = format_diff(last_diff_output, diff_output)
  1166. header_lines = [header] if header else []
  1167. output = "\n".join(header_lines + diff_lines[args.skip_lines :])
  1168. # Pipe the output through 'tail' and only then to less, to ensure the
  1169. # write call doesn't block. ('tail' has to buffer all its input before
  1170. # it starts writing.) This also means we don't have to deal with pipe
  1171. # closure errors.
  1172. buffer_proc = subprocess.Popen(
  1173. BUFFER_CMD, stdin=subprocess.PIPE, stdout=subprocess.PIPE
  1174. )
  1175. less_proc = subprocess.Popen(LESS_CMD, stdin=buffer_proc.stdout)
  1176. assert buffer_proc.stdin
  1177. assert buffer_proc.stdout
  1178. buffer_proc.stdin.write(output.encode())
  1179. buffer_proc.stdin.close()
  1180. buffer_proc.stdout.close()
  1181. return (buffer_proc, less_proc)
  1182. def run_sync(self) -> None:
  1183. proca, procb = self.run_less()
  1184. procb.wait()
  1185. proca.wait()
  1186. def run_async(self, watch_queue: "queue.Queue[Optional[float]]") -> None:
  1187. self.watch_queue = watch_queue
  1188. self.ready_queue = queue.Queue()
  1189. self.pending_update = None
  1190. dthread = threading.Thread(target=self.display_thread)
  1191. dthread.start()
  1192. self.ready_queue.get()
  1193. def display_thread(self) -> None:
  1194. proca, procb = self.run_less()
  1195. self.less_proc = procb
  1196. self.ready_queue.put(None)
  1197. while True:
  1198. ret = procb.wait()
  1199. proca.wait()
  1200. self.less_proc = None
  1201. if ret != 0:
  1202. # fix the terminal
  1203. os.system("tput reset")
  1204. if ret != 0 and self.pending_update is not None:
  1205. # killed by program with the intent to refresh
  1206. msg, error = self.pending_update
  1207. self.pending_update = None
  1208. if not error:
  1209. self.mydump = msg
  1210. self.emsg = None
  1211. else:
  1212. self.emsg = msg
  1213. proca, procb = self.run_less()
  1214. self.less_proc = procb
  1215. self.ready_queue.put(None)
  1216. else:
  1217. # terminated by user, or killed
  1218. self.watch_queue.put(None)
  1219. self.ready_queue.put(None)
  1220. break
  1221. def progress(self, msg: str) -> None:
  1222. # Write message to top-left corner
  1223. sys.stdout.write("\x1b7\x1b[1;1f{}\x1b8".format(msg + " "))
  1224. sys.stdout.flush()
  1225. def update(self, text: str, error: bool) -> None:
  1226. if not error and not self.emsg and text == self.mydump:
  1227. self.progress("Unchanged. ")
  1228. return
  1229. self.pending_update = (text, error)
  1230. if not self.less_proc:
  1231. return
  1232. self.less_proc.kill()
  1233. self.ready_queue.get()
  1234. def terminate(self) -> None:
  1235. if not self.less_proc:
  1236. return
  1237. self.less_proc.kill()
  1238. self.ready_queue.get()
  1239. def main() -> None:
  1240. if args.diff_elf_symbol:
  1241. make_target, basecmd, mycmd = dump_elf()
  1242. elif args.diff_obj:
  1243. make_target, basecmd, mycmd = dump_objfile()
  1244. else:
  1245. make_target, basecmd, mycmd = dump_binary()
  1246. if args.write_asm is not None:
  1247. mydump = run_objdump(mycmd)
  1248. with open(args.write_asm, "w") as f:
  1249. f.write(mydump)
  1250. print(f"Wrote assembly to {args.write_asm}.")
  1251. sys.exit(0)
  1252. if args.base_asm is not None:
  1253. with open(args.base_asm) as f:
  1254. basedump = f.read()
  1255. else:
  1256. basedump = run_objdump(basecmd)
  1257. mydump = run_objdump(mycmd)
  1258. display = Display(basedump, mydump)
  1259. if not args.watch:
  1260. display.run_sync()
  1261. else:
  1262. if not args.make:
  1263. yn = input(
  1264. "Warning: watch-mode (-w) enabled without auto-make (-m). "
  1265. "You will have to run make manually. Ok? (Y/n) "
  1266. )
  1267. if yn.lower() == "n":
  1268. return
  1269. if args.make:
  1270. watch_sources = None
  1271. watch_sources_for_target_fn = getattr(
  1272. diff_settings, "watch_sources_for_target", None
  1273. )
  1274. if watch_sources_for_target_fn:
  1275. watch_sources = watch_sources_for_target_fn(make_target)
  1276. watch_sources = watch_sources or source_directories
  1277. if not watch_sources:
  1278. fail("Missing source_directories config, don't know what to watch.")
  1279. else:
  1280. watch_sources = [make_target]
  1281. q: "queue.Queue[Optional[float]]" = queue.Queue()
  1282. debounced_fs_watch(watch_sources, q, DEBOUNCE_DELAY)
  1283. display.run_async(q)
  1284. last_build = 0.0
  1285. try:
  1286. while True:
  1287. t = q.get()
  1288. if t is None:
  1289. break
  1290. if t < last_build:
  1291. continue
  1292. last_build = time.time()
  1293. if args.make:
  1294. display.progress("Building...")
  1295. ret = run_make_capture_output(make_target)
  1296. if ret.returncode != 0:
  1297. display.update(
  1298. ret.stderr.decode("utf-8-sig", "replace")
  1299. or ret.stdout.decode("utf-8-sig", "replace"),
  1300. error=True,
  1301. )
  1302. continue
  1303. mydump = run_objdump(mycmd)
  1304. display.update(mydump, error=False)
  1305. except KeyboardInterrupt:
  1306. display.terminate()
  1307. main()