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