From 97e066f23f35b5bc65e31093fd074a6c1e61195b Mon Sep 17 00:00:00 2001 From: Anghelo Carvajal Date: Wed, 25 Aug 2021 01:11:41 -0400 Subject: [PATCH] Update asm-processor and diff.py (#278) * fix asm differ branch * git subrepo pull --force tools/asm-differ subrepo: subdir: "tools/asm-differ" merged: "fd0984c97" upstream: origin: "https://github.com/simonlindholm/asm-differ.git" branch: "main" commit: "fd0984c97" git-subrepo: version: "0.4.3" origin: "???" commit: "???" * delete asm-processor * git subrepo clone git@github.com:simonlindholm/asm-processor.git tools/asm-processor subrepo: subdir: "tools/asm-processor" merged: "755f734fb" upstream: origin: "git@github.com:simonlindholm/asm-processor.git" branch: "main" commit: "755f734fb" git-subrepo: version: "0.4.3" origin: "???" commit: "???" * re-add build.py * remove subrepo * git subrepo pull --force tools/asm-differ subrepo: subdir: "tools/asm-differ" merged: "1dfba80e1" upstream: origin: "https://github.com/simonlindholm/asm-differ.git" branch: "main" commit: "1dfba80e1" git-subrepo: version: "0.4.3" origin: "???" commit: "???" --- tools/asm-differ/.gitignore | 1 + tools/asm-differ/.gitrepo | 6 +- tools/asm-differ/LICENSE | 24 + tools/asm-differ/README.md | 6 +- tools/asm-differ/diff-stylesheet.css | 64 + tools/asm-differ/diff.py | 2289 ++++++++++++++++++-------- tools/asm-differ/diff_settings.py | 15 +- tools/asm-differ/mypy.ini | 1 + tools/asm-differ/screenshot.png | Bin 0 -> 99842 bytes tools/asm-processor/asm_processor.py | 96 +- 10 files changed, 1762 insertions(+), 740 deletions(-) create mode 100644 tools/asm-differ/LICENSE create mode 100644 tools/asm-differ/diff-stylesheet.css create mode 100644 tools/asm-differ/screenshot.png diff --git a/tools/asm-differ/.gitignore b/tools/asm-differ/.gitignore index e5daf0d15..eb176dc56 100644 --- a/tools/asm-differ/.gitignore +++ b/tools/asm-differ/.gitignore @@ -1 +1,2 @@ .mypy_cache/ +__pycache__/ diff --git a/tools/asm-differ/.gitrepo b/tools/asm-differ/.gitrepo index 619ae708c..72e41e4e6 100644 --- a/tools/asm-differ/.gitrepo +++ b/tools/asm-differ/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/simonlindholm/asm-differ.git - branch = master - commit = eaf72269cf7329bc061e50d8788229575f656f06 - parent = fa02cf86ffdb88253181cda15d047b08576d3f99 + branch = main + commit = 1dfba80e1b36bb31c9ea64cb04583e7b36d839a6 + parent = f14e8c9d9e68107609c9eeb4408bbfd1cd850eee method = merge cmdver = 0.4.3 diff --git a/tools/asm-differ/LICENSE b/tools/asm-differ/LICENSE new file mode 100644 index 000000000..cf1ab25da --- /dev/null +++ b/tools/asm-differ/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/tools/asm-differ/README.md b/tools/asm-differ/README.md index d7c56187d..44ab2e6e4 100644 --- a/tools/asm-differ/README.md +++ b/tools/asm-differ/README.md @@ -1,17 +1,17 @@ # asm-differ -Nice differ for assembly code (currently MIPS, but should be easy to hack to support other instruction sets). +Nice differ for assembly code (MIPS and AArch64; should be easy to hack to support other instruction sets). ![](screenshot.png) ## Dependencies - Python >= 3.6 -- `python3 -m pip install --user colorama ansiwrap attrs watchdog` +- `python3 -m pip install --user colorama watchdog python-Levenshtein` (also `dataclasses` if on 3.6) ## Usage -Create a file `diff-settings.sh` in some directory (see the one in this repo for an example). Then from that directory, run +Create a file `diff_settings.sh` in some directory (see the one in this repo for an example). Then from that directory, run ``` /path/to/diff.sh [flags] (function|rom addr) diff --git a/tools/asm-differ/diff-stylesheet.css b/tools/asm-differ/diff-stylesheet.css new file mode 100644 index 000000000..099c7af47 --- /dev/null +++ b/tools/asm-differ/diff-stylesheet.css @@ -0,0 +1,64 @@ +table.diff { + border: none; + font-family: Monospace; + white-space: pre; +} +.immediate { + color: lightblue; +} +.stack { + color: yellow; +} +.register { + color: yellow; +} +.delay-slot { + font-weight: bold; + color: gray; +} +.diff-change { + color: lightblue; +} +.diff-add { + color: green; +} +.diff-remove { + color: red; +} +.source-filename { + font-weight: bold; +} +.source-function { + font-weight: bold; + text-decoration: underline; +} +.source-other { + font-style: italic; +} +.rotation-0 { + color: magenta; +} +.rotation-1 { + color: cyan; +} +.rotation-2 { + color: green; +} +.rotation-3 { + color: red; +} +.rotation-4 { + color: yellow; +} +.rotation-5 { + color: pink; +} +.rotation-6 { + color: blue; +} +.rotation-7 { + color: lime; +} +.rotation-8 { + color: gray; +} diff --git a/tools/asm-differ/diff.py b/tools/asm-differ/diff.py index 0e1da09de..f2064e395 100755 --- a/tools/asm-differ/diff.py +++ b/tools/asm-differ/diff.py @@ -1,278 +1,450 @@ #!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK +import argparse import sys +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Match, + NoReturn, + Optional, + Pattern, + Set, + Tuple, + Type, + Union, +) -def fail(msg): + +def fail(msg: str) -> NoReturn: print(msg, file=sys.stderr) sys.exit(1) -# Prefer to use diff_settings.py from the current working directory -sys.path.insert(0, ".") -try: - import diff_settings -except ModuleNotFoundError: - fail("Unable to find diff_settings.py in the same directory.") -sys.path.pop(0) + +def static_assert_unreachable(x: NoReturn) -> NoReturn: + raise Exception("Unreachable! " + repr(x)) + # ==== COMMAND-LINE ==== -try: - import argcomplete # type: ignore -except ModuleNotFoundError: - argcomplete = None -import argparse +if __name__ == "__main__": + # Prefer to use diff_settings.py from the current working directory + sys.path.insert(0, ".") + try: + import diff_settings + except ModuleNotFoundError: + fail("Unable to find diff_settings.py in the same directory.") + sys.path.pop(0) -parser = argparse.ArgumentParser(description="Diff MIPS assembly.") + try: + import argcomplete # type: ignore + except ModuleNotFoundError: + argcomplete = None -start_argument = parser.add_argument("start", help="Function name or address to start diffing from.") -if argcomplete: - def complete_symbol(**kwargs): - prefix = kwargs["prefix"] - if prefix == "": - # skip reading the map file, which would - # result in a lot of useless completions - return [] - parsed_args = kwargs["parsed_args"] - config = {} - diff_settings.apply(config, parsed_args) - mapfile = config.get("mapfile") - if not mapfile: - return [] - completes = [] - with open(mapfile) as f: - data = f.read() - # assume symbols are prefixed by a space character - search = f" {prefix}" - pos = data.find(search) - while pos != -1: - # skip the space character in the search string - pos += 1 - # assume symbols are suffixed by either a space - # character or a (unix-style) line return - spacePos = data.find(" ", pos) - lineReturnPos = data.find("\n", pos) - if lineReturnPos == -1: - endPos = spacePos - elif spacePos == -1: - endPos = lineReturnPos - else: - endPos = min(spacePos, lineReturnPos) - if endPos == -1: - match = data[pos:] - pos = -1 - else: - match = data[pos:endPos] - pos = data.find(search, endPos) - completes.append(match) - return completes - start_argument.completer = complete_symbol + parser = argparse.ArgumentParser(description="Diff MIPS, PPC or AArch64 assembly.") -parser.add_argument("end", nargs="?", help="Address to end diff at.") -parser.add_argument( - "-o", - dest="diff_obj", - action="store_true", - help="Diff .o files rather than a whole binary. This makes it possible to see symbol names. (Recommended)", -) -parser.add_argument( - "-e", - "--elf", - dest="diff_elf_symbol", - help="Diff a given function in two ELFs, one being stripped and the other one non-stripped. Requires objdump from binutils 2.33+.", -) -parser.add_argument( - "--source", - action="store_true", - help="Show source code (if possible). Only works with -o and -e.", -) -parser.add_argument( - "--inlines", - action="store_true", - help="Show inline function calls (if possible). Only works with -o and -e.", -) -parser.add_argument( - "--base-asm", - dest="base_asm", - metavar="FILE", - help="Read assembly from given file instead of configured base img.", -) -parser.add_argument( - "--write-asm", - dest="write_asm", - metavar="FILE", - help="Write the current assembly output to file, e.g. for use with --base-asm.", -) -parser.add_argument( - "-m", - "--make", - dest="make", - action="store_true", - help="Automatically run 'make' on the .o file or binary before diffing.", -) -parser.add_argument( - "-l", - "--skip-lines", - dest="skip_lines", - type=int, - default=0, - help="Skip the first N lines of output.", -) -parser.add_argument( - "-s", - "--stop-jr-ra", - dest="stop_jrra", - action="store_true", - help="Stop disassembling at the first 'jr ra'. Some functions have multiple return points, so use with care!", -) -parser.add_argument( - "-i", - "--ignore-large-imms", - dest="ignore_large_imms", - action="store_true", - help="Pretend all large enough immediates are the same.", -) -parser.add_argument( - "-B", - "--no-show-branches", - dest="show_branches", - action="store_false", - help="Don't visualize branches/branch targets.", -) -parser.add_argument( - "-S", - "--base-shift", - dest="base_shift", - type=str, - default="0", - help="Diff position X in our img against position X + shift in the base img. " - 'Arithmetic is allowed, so e.g. |-S "0x1234 - 0x4321"| is a reasonable ' - "flag to pass if it is known that position 0x1234 in the base img syncs " - "up with position 0x4321 in our img. Not supported together with -o.", -) -parser.add_argument( - "-w", - "--watch", - dest="watch", - action="store_true", - help="Automatically update when source/object files change. " - "Recommended in combination with -m.", -) -parser.add_argument( - "-3", - "--threeway", - dest="threeway", - action="store_true", - help="Show a three-way diff between target asm, current asm, and asm " - "prior to -w rebuild. Requires -w.", -) -parser.add_argument( - "--width", - dest="column_width", - type=int, - default=50, - help="Sets the width of the left and right view column.", -) -parser.add_argument( - "--algorithm", - dest="algorithm", - default="levenshtein", - choices=["levenshtein", "difflib"], - help="Diff algorithm to use.", -) -parser.add_argument( - "--max-size", - "--max-lines", - dest="max_lines", - type=int, - default=1024, - help="The maximum length of the diff, in lines.", -) + start_argument = parser.add_argument( + "start", + help="Function name or address to start diffing from.", + ) -# Project-specific flags, e.g. different versions/make arguments. -if hasattr(diff_settings, "add_custom_arguments"): - diff_settings.add_custom_arguments(parser) # type: ignore + if argcomplete: -if argcomplete: - argcomplete.autocomplete(parser) + def complete_symbol( + prefix: str, parsed_args: argparse.Namespace, **kwargs: object + ) -> List[str]: + if not prefix or prefix.startswith("-"): + # skip reading the map file, which would + # result in a lot of useless completions + return [] + config: Dict[str, Any] = {} + diff_settings.apply(config, parsed_args) # type: ignore + mapfile = config.get("mapfile") + if not mapfile: + return [] + completes = [] + with open(mapfile) as f: + data = f.read() + # assume symbols are prefixed by a space character + search = f" {prefix}" + pos = data.find(search) + while pos != -1: + # skip the space character in the search string + pos += 1 + # assume symbols are suffixed by either a space + # character or a (unix-style) line return + spacePos = data.find(" ", pos) + lineReturnPos = data.find("\n", pos) + if lineReturnPos == -1: + endPos = spacePos + elif spacePos == -1: + endPos = lineReturnPos + else: + endPos = min(spacePos, lineReturnPos) + if endPos == -1: + match = data[pos:] + pos = -1 + else: + match = data[pos:endPos] + pos = data.find(search, endPos) + completes.append(match) + return completes + + setattr(start_argument, "completer", complete_symbol) + + parser.add_argument( + "end", + nargs="?", + help="Address to end diff at.", + ) + parser.add_argument( + "-o", + dest="diff_obj", + action="store_true", + help="""Diff .o files rather than a whole binary. This makes it possible to + see symbol names. (Recommended)""", + ) + parser.add_argument( + "-e", + "--elf", + dest="diff_elf_symbol", + metavar="SYMBOL", + help="""Diff a given function in two ELFs, one being stripped and the other + one non-stripped. Requires objdump from binutils 2.33+.""", + ) + parser.add_argument( + "--source", + "-c", + dest="source", + action="store_true", + help="Show source code (if possible). Only works with -o or -e.", + ) + parser.add_argument( + "--source-old-binutils", + "-C", + dest="source_old_binutils", + action="store_true", + help="Tweak --source handling to make it work with binutils < 2.33. Implies --source.", + ) + parser.add_argument( + "--inlines", + dest="inlines", + action="store_true", + help="Show inline function calls (if possible). Only works with -o or -e.", + ) + parser.add_argument( + "--base-asm", + dest="base_asm", + metavar="FILE", + help="Read assembly from given file instead of configured base img.", + ) + parser.add_argument( + "--write-asm", + dest="write_asm", + metavar="FILE", + help="Write the current assembly output to file, e.g. for use with --base-asm.", + ) + parser.add_argument( + "-m", + "--make", + dest="make", + action="store_true", + help="Automatically run 'make' on the .o file or binary before diffing.", + ) + parser.add_argument( + "-l", + "--skip-lines", + dest="skip_lines", + metavar="LINES", + type=int, + default=0, + help="Skip the first LINES lines of output.", + ) + parser.add_argument( + "-s", + "--stop-jr-ra", + dest="stop_jrra", + action="store_true", + help="""Stop disassembling at the first 'jr ra'. Some functions have + multiple return points, so use with care!""", + ) + parser.add_argument( + "-i", + "--ignore-large-imms", + dest="ignore_large_imms", + action="store_true", + help="Pretend all large enough immediates are the same.", + ) + parser.add_argument( + "-I", + "--ignore-addr-diffs", + dest="ignore_addr_diffs", + action="store_true", + help="Ignore address differences. Currently only affects AArch64.", + ) + parser.add_argument( + "-B", + "--no-show-branches", + dest="show_branches", + action="store_false", + help="Don't visualize branches/branch targets.", + ) + parser.add_argument( + "-S", + "--base-shift", + dest="base_shift", + metavar="N", + type=str, + default="0", + help="""Diff position N in our img against position N + shift in the base img. + Arithmetic is allowed, so e.g. |-S "0x1234 - 0x4321"| is a reasonable + flag to pass if it is known that position 0x1234 in the base img syncs + up with position 0x4321 in our img. Not supported together with -o.""", + ) + parser.add_argument( + "-w", + "--watch", + dest="watch", + action="store_true", + help="""Automatically update when source/object files change. + Recommended in combination with -m.""", + ) + parser.add_argument( + "-3", + "--threeway=prev", + dest="threeway", + action="store_const", + const="prev", + help="""Show a three-way diff between target asm, current asm, and asm + prior to -w rebuild. Requires -w.""", + ) + parser.add_argument( + "-b", + "--threeway=base", + dest="threeway", + action="store_const", + const="base", + help="""Show a three-way diff between target asm, current asm, and asm + when diff.py was started. Requires -w.""", + ) + parser.add_argument( + "--width", + dest="column_width", + metavar="COLS", + type=int, + default=50, + help="Sets the width of the left and right view column.", + ) + parser.add_argument( + "--algorithm", + dest="algorithm", + default="levenshtein", + choices=["levenshtein", "difflib"], + help="""Diff algorithm to use. Levenshtein gives the minimum diff, while difflib + aims for long sections of equal opcodes. Defaults to %(default)s.""", + ) + parser.add_argument( + "--max-size", + "--max-lines", + metavar="LINES", + dest="max_lines", + type=int, + default=1024, + help="The maximum length of the diff, in lines.", + ) + parser.add_argument( + "--no-pager", + dest="no_pager", + action="store_true", + help="""Disable the pager; write output directly to stdout, then exit. + Incompatible with --watch.""", + ) + parser.add_argument( + "--format", + choices=("color", "plain", "html"), + default="color", + help="Output format, default is color. --format=html implies --no-pager.", + ) + parser.add_argument( + "-U", + "--compress-matching", + metavar="N", + dest="compress_matching", + type=int, + help="""Compress streaks of matching lines, leaving N lines of context + around non-matching parts.""", + ) + parser.add_argument( + "-V", + "--compress-sameinstr", + metavar="N", + dest="compress_sameinstr", + type=int, + help="""Compress streaks of lines with same instructions (but possibly + different regalloc), leaving N lines of context around other parts.""", + ) + + # Project-specific flags, e.g. different versions/make arguments. + add_custom_arguments_fn = getattr(diff_settings, "add_custom_arguments", None) + if add_custom_arguments_fn: + add_custom_arguments_fn(parser) + + if argcomplete: + argcomplete.autocomplete(parser) # ==== IMPORTS ==== -import re -import os +# (We do imports late to optimize auto-complete performance.) + +import abc import ast -import subprocess +from dataclasses import dataclass, field, replace import difflib -import string +import enum +import html import itertools -import threading +import os import queue +import re +import string +import subprocess +import threading import time -from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, Union MISSING_PREREQUISITES = ( "Missing prerequisite python module {}. " - "Run `python3 -m pip install --user colorama ansiwrap watchdog python-Levenshtein cxxfilt` to install prerequisites (cxxfilt only needed with --source)." + "Run `python3 -m pip install --user colorama watchdog python-Levenshtein cxxfilt` to install prerequisites (cxxfilt only needed with --source)." ) try: - from colorama import Fore, Style, Back # type: ignore - import ansiwrap # type: ignore + from colorama import Fore, Style # type: ignore import watchdog # type: ignore except ModuleNotFoundError as e: fail(MISSING_PREREQUISITES.format(e.name)) # ==== CONFIG ==== -args = parser.parse_args() -# Set imgs, map file and make flags in a project-specific manner. -config: Dict[str, Any] = {} -diff_settings.apply(config, args) +@dataclass +class ProjectSettings: + arch_str: str + objdump_executable: str + build_command: List[str] + map_format: str + mw_build_dir: str + baseimg: Optional[str] + myimg: Optional[str] + mapfile: Optional[str] + source_directories: Optional[List[str]] + source_extensions: List[str] -arch = config.get("arch", "mips") -baseimg = config.get("baseimg", None) -myimg = config.get("myimg", None) -mapfile = config.get("mapfile", None) -makeflags = config.get("makeflags", []) -source_directories = config.get("source_directories", None) -objdump_executable = config.get("objdump_executable", None) -MAX_FUNCTION_SIZE_LINES = args.max_lines -MAX_FUNCTION_SIZE_BYTES = MAX_FUNCTION_SIZE_LINES * 4 +@dataclass +class Compress: + context: int + same_instr: bool -COLOR_ROTATION = [ - Fore.MAGENTA, - Fore.CYAN, - Fore.GREEN, - Fore.RED, - Fore.LIGHTYELLOW_EX, - Fore.LIGHTMAGENTA_EX, - Fore.LIGHTCYAN_EX, - Fore.LIGHTGREEN_EX, - Fore.LIGHTBLACK_EX, -] -BUFFER_CMD = ["tail", "-c", str(10 ** 9)] -LESS_CMD = ["less", "-SRic", "-#6"] +@dataclass +class Config: + arch: "ArchSettings" -DEBOUNCE_DELAY = 0.1 -FS_WATCH_EXTENSIONS = [".c", ".h"] + # Build/objdump options + diff_obj: bool + make: bool + source: bool + source_old_binutils: bool + inlines: bool + max_function_size_lines: int + max_function_size_bytes: int -# ==== LOGIC ==== + # Display options + formatter: "Formatter" + threeway: Optional[str] + base_shift: int + skip_lines: int + compress: Optional[Compress] + show_branches: bool + stop_jrra: bool + ignore_large_imms: bool + ignore_addr_diffs: bool + algorithm: str -if args.algorithm == "levenshtein": - try: - import Levenshtein # type: ignore - except ModuleNotFoundError as e: - fail(MISSING_PREREQUISITES.format(e.name)) -if args.source: - try: - import cxxfilt # type: ignore - except ModuleNotFoundError as e: - fail(MISSING_PREREQUISITES.format(e.name)) +def create_project_settings(settings: Dict[str, Any]) -> ProjectSettings: + return ProjectSettings( + arch_str=settings.get("arch", "mips"), + baseimg=settings.get("baseimg"), + myimg=settings.get("myimg"), + mapfile=settings.get("mapfile"), + build_command=settings.get( + "make_command", ["make", *settings.get("makeflags", [])] + ), + source_directories=settings.get("source_directories"), + source_extensions=settings.get( + "source_extensions", [".c", ".h", ".cpp", ".hpp", ".s"] + ), + objdump_executable=get_objdump_executable(settings.get("objdump_executable")), + map_format=settings.get("map_format", "gnu"), + mw_build_dir=settings.get("mw_build_dir", "build/"), + ) + + +def create_config(args: argparse.Namespace, project: ProjectSettings) -> Config: + formatter: Formatter + if args.format == "plain": + formatter = PlainFormatter(column_width=args.column_width) + elif args.format == "color": + formatter = AnsiFormatter(column_width=args.column_width) + elif args.format == "html": + formatter = HtmlFormatter() + else: + raise ValueError(f"Unsupported --format: {args.format}") + + compress = None + if args.compress_matching is not None: + compress = Compress(args.compress_matching, False) + if args.compress_sameinstr is not None: + if compress is not None: + raise ValueError( + "Cannot pass both --compress-matching and --compress-sameinstr" + ) + compress = Compress(args.compress_sameinstr, True) + + return Config( + arch=get_arch(project.arch_str), + # Build/objdump options + diff_obj=args.diff_obj, + make=args.make, + source=args.source or args.source_old_binutils, + source_old_binutils=args.source_old_binutils, + inlines=args.inlines, + max_function_size_lines=args.max_lines, + max_function_size_bytes=args.max_lines * 4, + # Display options + formatter=formatter, + threeway=args.threeway, + base_shift=eval_int( + args.base_shift, "Failed to parse --base-shift (-S) argument as an integer." + ), + skip_lines=args.skip_lines, + compress=compress, + show_branches=args.show_branches, + stop_jrra=args.stop_jrra, + ignore_large_imms=args.ignore_large_imms, + ignore_addr_diffs=args.ignore_addr_diffs, + algorithm=args.algorithm, + ) + + +def get_objdump_executable(objdump_executable: Optional[str]) -> str: + if objdump_executable is not None: + return objdump_executable -if objdump_executable is None: for objdump_cand in ["mips-linux-gnu-objdump", "mips64-elf-objdump"]: try: subprocess.check_call( @@ -280,53 +452,351 @@ if objdump_executable is None: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - objdump_executable = objdump_cand - break + return objdump_cand except subprocess.CalledProcessError: pass except FileNotFoundError: pass -if not objdump_executable: - fail( + return fail( "Missing binutils; please ensure mips-linux-gnu-objdump or mips64-elf-objdump exist, or configure objdump_executable." ) -def eval_int(expr, emsg=None): +def get_arch(arch_str: str) -> "ArchSettings": + if arch_str == "mips": + return MIPS_SETTINGS + if arch_str == "aarch64": + return AARCH64_SETTINGS + if arch_str == "ppc": + return PPC_SETTINGS + return fail(f"Unknown architecture: {arch_str}") + + +BUFFER_CMD: List[str] = ["tail", "-c", str(10 ** 9)] + +# -S truncates long lines instead of wrapping them +# -R interprets color escape sequences +# -i ignores case when searching +# -c something about how the screen gets redrawn; I don't remember the purpose +# -#6 makes left/right arrow keys scroll by 6 characters +LESS_CMD: List[str] = ["less", "-SRic", "-#6"] + +DEBOUNCE_DELAY: float = 0.1 + +# ==== FORMATTING ==== + + +@enum.unique +class BasicFormat(enum.Enum): + NONE = enum.auto() + IMMEDIATE = enum.auto() + STACK = enum.auto() + REGISTER = enum.auto() + DELAY_SLOT = enum.auto() + DIFF_CHANGE = enum.auto() + DIFF_ADD = enum.auto() + DIFF_REMOVE = enum.auto() + SOURCE_FILENAME = enum.auto() + SOURCE_FUNCTION = enum.auto() + SOURCE_OTHER = enum.auto() + + +@dataclass(frozen=True) +class RotationFormat: + group: str + index: int + key: str + + +Format = Union[BasicFormat, RotationFormat] +FormatFunction = Callable[[str], Format] + + +class Text: + segments: List[Tuple[str, Format]] + + def __init__(self, line: str = "", f: Format = BasicFormat.NONE) -> None: + self.segments = [(line, f)] if line else [] + + def reformat(self, f: Format) -> "Text": + return Text(self.plain(), f) + + def plain(self) -> str: + return "".join(s for s, f in self.segments) + + def __repr__(self) -> str: + return f"" + + def __bool__(self) -> bool: + return any(s for s, f in self.segments) + + def __str__(self) -> str: + # Use Formatter.apply(...) instead + return NotImplemented + + def __eq__(self, other: object) -> bool: + return NotImplemented + + def __add__(self, other: Union["Text", str]) -> "Text": + if isinstance(other, str): + other = Text(other) + result = Text() + result.segments = self.segments + other.segments + return result + + def __radd__(self, other: Union["Text", str]) -> "Text": + if isinstance(other, str): + other = Text(other) + result = Text() + result.segments = other.segments + self.segments + return result + + def finditer(self, pat: Pattern[str]) -> Iterator[Match[str]]: + """Replacement for `pat.finditer(text)` that operates on the inner text, + and returns the exact same matches as `Text.sub(pat, ...)`.""" + for chunk, f in self.segments: + for match in pat.finditer(chunk): + yield match + + def sub(self, pat: Pattern[str], sub_fn: Callable[[Match[str]], "Text"]) -> "Text": + result = Text() + for chunk, f in self.segments: + i = 0 + for match in pat.finditer(chunk): + start, end = match.start(), match.end() + assert i <= start <= end <= len(chunk) + sub = sub_fn(match) + result.segments.append((chunk[i:start], f)) + result.segments.extend(sub.segments) + i = end + result.segments.append((chunk[i:], f)) + return result + + def ljust(self, column_width: int) -> "Text": + length = sum(len(x) for x, _ in self.segments) + return self + " " * max(column_width - length, 0) + + +class Formatter(abc.ABC): + @abc.abstractmethod + def apply_format(self, chunk: str, f: Format) -> str: + """Apply the formatting `f` to `chunk` and escape the contents.""" + ... + + @abc.abstractmethod + def table( + self, header: Optional[Tuple[Text, ...]], lines: List[Tuple[Text, ...]] + ) -> str: + """Format a multi-column table with an optional `header`""" + ... + + def apply(self, text: Text) -> str: + return "".join(self.apply_format(chunk, f) for chunk, f in text.segments) + + +@dataclass +class PlainFormatter(Formatter): + column_width: int + + def apply_format(self, chunk: str, f: Format) -> str: + return chunk + + def table( + self, header: Optional[Tuple[Text, ...]], lines: List[Tuple[Text, ...]] + ) -> str: + if header: + lines = [header] + lines + return "\n".join( + "".join(self.apply(x.ljust(self.column_width)) for x in line) + for line in lines + ) + + +@dataclass +class AnsiFormatter(Formatter): + BASIC_ANSI_CODES = { + BasicFormat.NONE: "", + BasicFormat.IMMEDIATE: Fore.LIGHTBLUE_EX, + BasicFormat.STACK: Fore.YELLOW, + BasicFormat.REGISTER: Fore.YELLOW, + BasicFormat.DELAY_SLOT: Fore.LIGHTBLACK_EX, + BasicFormat.DIFF_CHANGE: Fore.LIGHTBLUE_EX, + BasicFormat.DIFF_ADD: Fore.GREEN, + BasicFormat.DIFF_REMOVE: Fore.RED, + BasicFormat.SOURCE_FILENAME: Style.DIM + Style.BRIGHT, + # Underline (not in colorama) + bright + dim + BasicFormat.SOURCE_FUNCTION: Style.DIM + Style.BRIGHT + "\u001b[4m", + BasicFormat.SOURCE_OTHER: Style.DIM, + } + + ROTATION_ANSI_COLORS = [ + Fore.MAGENTA, + Fore.CYAN, + Fore.GREEN, + Fore.RED, + Fore.LIGHTYELLOW_EX, + Fore.LIGHTMAGENTA_EX, + Fore.LIGHTCYAN_EX, + Fore.LIGHTGREEN_EX, + Fore.LIGHTBLACK_EX, + ] + + column_width: int + + def apply_format(self, chunk: str, f: Format) -> str: + if f == BasicFormat.NONE: + return chunk + if isinstance(f, BasicFormat): + ansi_code = self.BASIC_ANSI_CODES[f] + elif isinstance(f, RotationFormat): + ansi_code = self.ROTATION_ANSI_COLORS[ + f.index % len(self.ROTATION_ANSI_COLORS) + ] + else: + static_assert_unreachable(f) + return f"{ansi_code}{chunk}{Style.RESET_ALL}" + + def table( + self, header: Optional[Tuple[Text, ...]], lines: List[Tuple[Text, ...]] + ) -> str: + if header: + lines = [header] + lines + return "\n".join( + "".join(self.apply(x.ljust(self.column_width)) for x in line) + for line in lines + ) + + +@dataclass +class HtmlFormatter(Formatter): + rotation_formats: int = 9 + + def apply_format(self, chunk: str, f: Format) -> str: + chunk = html.escape(chunk) + if f == BasicFormat.NONE: + return chunk + if isinstance(f, BasicFormat): + class_name = f.name.lower().replace("_", "-") + data_attr = "" + elif isinstance(f, RotationFormat): + class_name = f"rotation-{f.index % self.rotation_formats}" + rotation_key = html.escape(f"{f.group};{f.key}", quote=True) + data_attr = f'data-rotation="{rotation_key}"' + else: + static_assert_unreachable(f) + return f"{chunk}" + + def table( + self, header: Optional[Tuple[Text, ...]], lines: List[Tuple[Text, ...]] + ) -> str: + def table_row(line: Tuple[Text, ...], cell_el: str) -> str: + output_row = " " + for cell in line: + cell_html = self.apply(cell) + output_row += f"<{cell_el}>{cell_html}" + output_row += "\n" + return output_row + + output = "\n" + if header: + output += " \n" + output += table_row(header, "th") + output += " \n" + output += " \n" + output += "".join(table_row(line, "td") for line in lines) + output += " \n" + output += "
\n" + return output + + +def format_fields( + pat: Pattern[str], + out1: Text, + out2: Text, + color1: FormatFunction, + color2: Optional[FormatFunction] = None, +) -> Tuple[Text, Text]: + diffs = [ + of.group() != nf.group() + for (of, nf) in zip(out1.finditer(pat), out2.finditer(pat)) + ] + + it = iter(diffs) + + def maybe_color(color: FormatFunction, s: str) -> Text: + return Text(s, color(s)) if next(it, False) else Text(s) + + out1 = out1.sub(pat, lambda m: maybe_color(color1, m.group())) + it = iter(diffs) + out2 = out2.sub(pat, lambda m: maybe_color(color2 or color1, m.group())) + + return out1, out2 + + +def symbol_formatter(group: str, base_index: int) -> FormatFunction: + symbol_formats: Dict[str, Format] = {} + + def symbol_format(s: str) -> Format: + # TODO: it would be nice to use a unique Format for each symbol, so we could + # add extra UI elements in the HTML version + f = symbol_formats.get(s) + if f is None: + index = len(symbol_formats) + base_index + f = RotationFormat(key=s, index=index, group=group) + symbol_formats[s] = f + return f + + return symbol_format + + +# ==== LOGIC ==== + +ObjdumpCommand = Tuple[List[str], str, Optional[str]] + + +def maybe_eval_int(expr: str) -> Optional[int]: try: ret = ast.literal_eval(expr) if not isinstance(ret, int): raise Exception("not an integer") return ret except Exception: - if emsg is not None: - fail(emsg) return None -def eval_line_num(expr): +def eval_int(expr: str, emsg: str) -> int: + ret = maybe_eval_int(expr) + if ret is None: + fail(emsg) + return ret + + +def eval_line_num(expr: str) -> int: return int(expr.strip().replace(":", ""), 16) -def run_make(target, capture_output=False): - if capture_output: - return subprocess.run( - ["make"] + makeflags + [target], - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - ) - else: - subprocess.check_call(["make"] + makeflags + [target]) +def run_make(target: str, project: ProjectSettings) -> None: + subprocess.check_call(project.build_command + [target]) -def restrict_to_function(dump, fn_name): - out = [] +def run_make_capture_output( + target: str, project: ProjectSettings +) -> "subprocess.CompletedProcess[bytes]": + return subprocess.run( + project.build_command + [target], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + +def restrict_to_function(dump: str, fn_name: str, config: Config) -> str: + out: List[str] = [] search = f"<{fn_name}>:" found = False for line in dump.split("\n"): if found: - if len(out) >= MAX_FUNCTION_SIZE_LINES: + if len(out) >= config.max_function_size_lines: break out.append(line) elif search in line: @@ -334,90 +804,149 @@ def restrict_to_function(dump, fn_name): return "\n".join(out) -def maybe_get_objdump_source_flags(): - if not args.source: +def maybe_get_objdump_source_flags(config: Config) -> List[str]: + if not config.source: return [] flags = [ "--source", - "--source-comment=| ", "-l", ] - if args.inlines: + if not config.source_old_binutils: + flags.append("--source-comment=│ ") + + if config.inlines: flags.append("--inlines") return flags -def run_objdump(cmd): +def run_objdump(cmd: ObjdumpCommand, config: Config, project: ProjectSettings) -> str: flags, target, restrict = cmd - out = subprocess.check_output( - [objdump_executable] + arch_flags + flags + [target], universal_newlines=True - ) + try: + out = subprocess.run( + [project.objdump_executable] + config.arch.arch_flags + flags + [target], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ).stdout + except subprocess.CalledProcessError as e: + print(e.stdout) + print(e.stderr) + if "unrecognized option '--source-comment" in e.stderr: + fail("** Try using --source-old-binutils instead of --source **") + raise e + if restrict is not None: - return restrict_to_function(out, restrict) + return restrict_to_function(out, restrict, config) return out -base_shift = eval_int( - args.base_shift, "Failed to parse --base-shift (-S) argument as an integer." -) - - -def search_map_file(fn_name): - if not mapfile: +def search_map_file( + fn_name: str, project: ProjectSettings +) -> Tuple[Optional[str], Optional[int]]: + if not project.mapfile: fail(f"No map file configured; cannot find function {fn_name}.") try: - with open(mapfile) as f: - lines = f.read().split("\n") + with open(project.mapfile) as f: + contents = f.read() except Exception: - fail(f"Failed to open map file {mapfile} for reading.") + fail(f"Failed to open map file {project.mapfile} for reading.") - try: - cur_objfile = None - ram_to_rom = None - cands = [] - last_line = "" - for line in lines: - if line.startswith(" .text"): - cur_objfile = line.split()[3] - if "load address" in line: - tokens = last_line.split() + line.split() - ram = int(tokens[1], 0) - rom = int(tokens[5], 0) - ram_to_rom = rom - ram - if line.endswith(" " + fn_name): - ram = int(line.split()[0], 0) - if cur_objfile is not None and ram_to_rom is not None: - cands.append((cur_objfile, ram + ram_to_rom)) - last_line = line - except Exception as e: - import traceback + if project.map_format == "gnu": + lines = contents.split("\n") - traceback.print_exc() - fail(f"Internal error while parsing map file") + try: + cur_objfile = None + ram_to_rom = None + cands = [] + last_line = "" + for line in lines: + if line.startswith(" .text"): + cur_objfile = line.split()[3] + if "load address" in line: + tokens = last_line.split() + line.split() + ram = int(tokens[1], 0) + rom = int(tokens[5], 0) + ram_to_rom = rom - ram + if line.endswith(" " + fn_name): + ram = int(line.split()[0], 0) + if cur_objfile is not None and ram_to_rom is not None: + cands.append((cur_objfile, ram + ram_to_rom)) + last_line = line + except Exception as e: + import traceback - if len(cands) > 1: - fail(f"Found multiple occurrences of function {fn_name} in map file.") - if len(cands) == 1: - return cands[0] + traceback.print_exc() + fail(f"Internal error while parsing map file") + + if len(cands) > 1: + fail(f"Found multiple occurrences of function {fn_name} in map file.") + if len(cands) == 1: + return cands[0] + elif project.map_format == "mw": + find = re.findall( + re.compile( + # ram elf rom + r" \S+ \S+ (\S+) (\S+) . " + + fn_name + # object name + + r"(?: \(entry of \.(?:init|text)\))? \t(\S+)" + ), + contents, + ) + if len(find) > 1: + fail(f"Found multiple occurrences of function {fn_name} in map file.") + if len(find) == 1: + rom = int(find[0][1], 16) + objname = find[0][2] + # The metrowerks linker map format does not contain the full object path, + # so we must complete it manually. + objfiles = [ + os.path.join(dirpath, f) + for dirpath, _, filenames in os.walk(project.mw_build_dir) + for f in filenames + if f == objname + ] + if len(objfiles) > 1: + all_objects = "\n".join(objfiles) + fail( + f"Found multiple objects of the same name {objname} in {project.mw_build_dir}, " + f"cannot determine which to diff against: \n{all_objects}" + ) + if len(objfiles) == 1: + objfile = objfiles[0] + # TODO Currently the ram-rom conversion only works for diffing ELF + # executables, but it would likely be more convenient to diff DOLs. + # At this time it is recommended to always use -o when running the diff + # script as this mode does not make use of the ram-rom conversion. + return objfile, rom + else: + fail(f"Linker map format {project.map_format} unrecognised.") return None, None -def dump_elf(): - if not baseimg or not myimg: +def dump_elf( + start: str, + end: Optional[str], + diff_elf_symbol: str, + config: Config, + project: ProjectSettings, +) -> Tuple[str, ObjdumpCommand, ObjdumpCommand]: + if not project.baseimg or not project.myimg: fail("Missing myimg/baseimg in config.") - if base_shift: + if config.base_shift: fail("--base-shift not compatible with -e") - start_addr = eval_int(args.start, "Start address must be an integer expression.") + start_addr = eval_int(start, "Start address must be an integer expression.") - if args.end is not None: - end_addr = eval_int(args.end, "End address must be an integer expression.") + if end is not None: + end_addr = eval_int(end, "End address must be an integer expression.") else: - end_addr = start_addr + MAX_FUNCTION_SIZE_BYTES + end_addr = start_addr + config.max_function_size_bytes flags1 = [ f"--start-address={start_addr}", @@ -425,31 +954,37 @@ def dump_elf(): ] flags2 = [ - f"--disassemble={args.diff_elf_symbol}", + f"--disassemble={diff_elf_symbol}", ] objdump_flags = ["-drz", "-j", ".text"] return ( - myimg, - (objdump_flags + flags1, baseimg, None), - (objdump_flags + flags2 + maybe_get_objdump_source_flags(), myimg, None), + project.myimg, + (objdump_flags + flags1, project.baseimg, None), + ( + objdump_flags + flags2 + maybe_get_objdump_source_flags(config), + project.myimg, + None, + ), ) -def dump_objfile(): - if base_shift: +def dump_objfile( + start: str, end: Optional[str], config: Config, project: ProjectSettings +) -> Tuple[str, ObjdumpCommand, ObjdumpCommand]: + if config.base_shift: fail("--base-shift not compatible with -o") - if args.end is not None: + if end is not None: fail("end address not supported together with -o") - if args.start.startswith("0"): + if start.startswith("0"): fail("numerical start address not supported with -o; pass a function name") - objfile, _ = search_map_file(args.start) + objfile, _ = search_map_file(start, project) if not objfile: fail("Not able to find .o file for function.") - if args.make: - run_make(objfile) + if config.make: + run_make(objfile, project) if not os.path.isfile(objfile): fail(f"Not able to find .o file for function: {objfile} is not a file.") @@ -461,108 +996,259 @@ def dump_objfile(): objdump_flags = ["-drz"] return ( objfile, - (objdump_flags, refobjfile, args.start), - (objdump_flags + maybe_get_objdump_source_flags(), objfile, args.start), + (objdump_flags, refobjfile, start), + (objdump_flags + maybe_get_objdump_source_flags(config), objfile, start), ) -def dump_binary(): - if not baseimg or not myimg: +def dump_binary( + start: str, end: Optional[str], config: Config, project: ProjectSettings +) -> Tuple[str, ObjdumpCommand, ObjdumpCommand]: + if not project.baseimg or not project.myimg: fail("Missing myimg/baseimg in config.") - if args.make: - run_make(myimg) - start_addr = eval_int(args.start) + if config.make: + run_make(project.myimg, project) + start_addr = maybe_eval_int(start) if start_addr is None: - _, start_addr = search_map_file(args.start) + _, start_addr = search_map_file(start, project) if start_addr is None: fail("Not able to find function in map file.") - if args.end is not None: - end_addr = eval_int(args.end, "End address must be an integer expression.") + if end is not None: + end_addr = eval_int(end, "End address must be an integer expression.") else: - end_addr = start_addr + MAX_FUNCTION_SIZE_BYTES - objdump_flags = ["-Dz", "-bbinary", "-mmips", "-EB"] + end_addr = start_addr + config.max_function_size_bytes + objdump_flags = ["-Dz", "-bbinary", "-EB"] flags1 = [ - f"--start-address={start_addr + base_shift}", - f"--stop-address={end_addr + base_shift}", + f"--start-address={start_addr + config.base_shift}", + f"--stop-address={end_addr + config.base_shift}", ] flags2 = [f"--start-address={start_addr}", f"--stop-address={end_addr}"] return ( - myimg, - (objdump_flags + flags1, baseimg, None), - (objdump_flags + flags2, myimg, None), + project.myimg, + (objdump_flags + flags1, project.baseimg, None), + (objdump_flags + flags2, project.myimg, None), ) -# Alignment with ANSI colors is broken, let's fix it. -def ansi_ljust(s, width): - needed = width - ansiwrap.ansilen(s) - if needed > 0: - return s + " " * needed - else: - return s +class DifferenceNormalizer: + def __init__(self, config: Config) -> None: + self.config = config + + def normalize(self, mnemonic: str, row: str) -> str: + """This should be called exactly once for each line.""" + row = self._normalize_arch_specific(mnemonic, row) + if self.config.ignore_large_imms: + row = re.sub(self.config.arch.re_large_imm, "", row) + return row + + def _normalize_arch_specific(self, mnemonic: str, row: str) -> str: + return row -if arch == "mips": - re_int = re.compile(r"[0-9]+") - re_comment = re.compile(r"<.*?>") - re_reg = re.compile(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") - re_sprel = re.compile(r"(?<=,)([0-9]+|0x[0-9a-f]+)\(sp\)") - re_large_imm = re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}") - re_imm = re.compile(r"(\b|-)([0-9]+|0x[0-9a-fA-F]+)\b(?!\(sp)|%(lo|hi)\([^)]*\)") - forbidden = set(string.ascii_letters + "_") - arch_flags = ["-m", "mips:4300"] - branch_likely_instructions = { - "beql", - "bnel", - "beqzl", - "bnezl", - "bgezl", - "bgtzl", - "blezl", - "bltzl", - "bc1tl", - "bc1fl", +class DifferenceNormalizerAArch64(DifferenceNormalizer): + def __init__(self, config: Config) -> None: + super().__init__(config) + self._adrp_pair_registers: Set[str] = set() + + def _normalize_arch_specific(self, mnemonic: str, row: str) -> str: + if self.config.ignore_addr_diffs: + row = self._normalize_adrp_differences(mnemonic, row) + row = self._normalize_bl(mnemonic, row) + return row + + def _normalize_bl(self, mnemonic: str, row: str) -> str: + if mnemonic != "bl": + return row + + row, _ = split_off_branch(row) + return row + + def _normalize_adrp_differences(self, mnemonic: str, row: str) -> str: + """Identifies ADRP + LDR/ADD pairs that are used to access the GOT and + suppresses any immediate differences. + + Whenever an ADRP is seen, the destination register is added to the set of registers + that are part of an ADRP + LDR/ADD pair. Registers are removed from the set as soon + as they are used for an LDR or ADD instruction which completes the pair. + + This method is somewhat crude but should manage to detect most such pairs. + """ + row_parts = row.split("\t", 1) + if mnemonic == "adrp": + self._adrp_pair_registers.add(row_parts[1].strip().split(",")[0]) + row, _ = split_off_branch(row) + elif mnemonic == "ldr": + for reg in self._adrp_pair_registers: + # ldr xxx, [reg] + # ldr xxx, [reg, ] + if f", [{reg}" in row_parts[1]: + self._adrp_pair_registers.remove(reg) + return normalize_imms(row, AARCH64_SETTINGS) + elif mnemonic == "add": + for reg in self._adrp_pair_registers: + # add reg, reg, + if row_parts[1].startswith(f"{reg}, {reg}, "): + self._adrp_pair_registers.remove(reg) + return normalize_imms(row, AARCH64_SETTINGS) + + return row + + +@dataclass +class ArchSettings: + re_int: Pattern[str] + re_comment: Pattern[str] + re_reg: Pattern[str] + re_sprel: Pattern[str] + re_large_imm: Pattern[str] + re_imm: Pattern[str] + branch_instructions: Set[str] + instructions_with_address_immediates: Set[str] + forbidden: Set[str] = field(default_factory=lambda: set(string.ascii_letters + "_")) + arch_flags: List[str] = field(default_factory=list) + branch_likely_instructions: Set[str] = field(default_factory=set) + difference_normalizer: Type[DifferenceNormalizer] = DifferenceNormalizer + + +MIPS_BRANCH_LIKELY_INSTRUCTIONS = { + "beql", + "bnel", + "beqzl", + "bnezl", + "bgezl", + "bgtzl", + "blezl", + "bltzl", + "bc1tl", + "bc1fl", +} +MIPS_BRANCH_INSTRUCTIONS = MIPS_BRANCH_LIKELY_INSTRUCTIONS.union( + { + "b", + "beq", + "bne", + "beqz", + "bnez", + "bgez", + "bgtz", + "blez", + "bltz", + "bc1t", + "bc1f", } - branch_instructions = branch_likely_instructions.union( - {"b", "beq", "bne", "beqz", "bnez", "bgez", "bgtz", "blez", "bltz", "bc1t", "bc1f"} - ) - instructions_with_address_immediates = branch_instructions.union({"jal", "j"}) -elif arch == "aarch64": - re_int = re.compile(r"[0-9]+") - re_comment = re.compile(r"(<.*?>|//.*$)") +) + +AARCH64_BRANCH_INSTRUCTIONS = { + "bl", + "b", + "b.eq", + "b.ne", + "b.cs", + "b.hs", + "b.cc", + "b.lo", + "b.mi", + "b.pl", + "b.vs", + "b.vc", + "b.hi", + "b.ls", + "b.ge", + "b.lt", + "b.gt", + "b.le", + "cbz", + "cbnz", + "tbz", + "tbnz", +} + +PPC_BRANCH_INSTRUCTIONS = { + "b", + "beq", + "beq+", + "beq-", + "bne", + "bne+", + "bne-", + "blt", + "blt+", + "blt-", + "ble", + "ble+", + "ble-", + "bdnz", + "bdnz+", + "bdnz-", + "bge", + "bge+", + "bge-", + "bgt", + "bgt+", + "bgt-", +} + +MIPS_SETTINGS = ArchSettings( + re_int=re.compile(r"[0-9]+"), + re_comment=re.compile(r"<.*?>"), + re_reg=re.compile( + 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" + ), + re_sprel=re.compile(r"(?<=,)([0-9]+|0x[0-9a-f]+)\(sp\)"), + re_large_imm=re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}"), + re_imm=re.compile(r"(\b|-)([0-9]+|0x[0-9a-fA-F]+)\b(?!\(sp)|%(lo|hi)\([^)]*\)"), + arch_flags=["-m", "mips:4300"], + branch_likely_instructions=MIPS_BRANCH_LIKELY_INSTRUCTIONS, + branch_instructions=MIPS_BRANCH_INSTRUCTIONS, + instructions_with_address_immediates=MIPS_BRANCH_INSTRUCTIONS.union({"jal", "j"}), +) + +AARCH64_SETTINGS = ArchSettings( + re_int=re.compile(r"[0-9]+"), + re_comment=re.compile(r"(<.*?>|//.*$)"), # GPRs and FP registers: X0-X30, W0-W30, [DSHQ]0..31 # The zero registers and SP should not be in this list. - re_reg = re.compile(r"\$?\b([dshq][12]?[0-9]|[dshq]3[01]|[xw][12]?[0-9]|[xw]30)\b") - re_sprel = re.compile(r"sp, #-?(0x[0-9a-fA-F]+|[0-9]+)\b") - re_large_imm = re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}") - re_imm = re.compile(r"(?|//.*$)"), + re_reg=re.compile(r"\$?\b([rf][0-9]+)\b"), + re_sprel=re.compile(r"(?<=,)(-?[0-9]+|-?0x[0-9a-f]+)\(r1\)"), + re_large_imm=re.compile(r"-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}"), + re_imm=re.compile(r"(\b|-)([0-9]+|0x[0-9a-fA-F]+)\b(?!\(r1)|[^@]*@(ha|h|lo)"), + branch_instructions=PPC_BRANCH_INSTRUCTIONS, + instructions_with_address_immediates=PPC_BRANCH_INSTRUCTIONS.union({"bl"}), +) -def hexify_int(row, pat): +def hexify_int(row: str, pat: Match[str], arch: ArchSettings) -> str: full = pat.group(0) if len(full) <= 1: # leave one-digit ints alone return full start, end = pat.span() - if start and row[start - 1] in forbidden: + if start and row[start - 1] in arch.forbidden: return full - if end < len(row) and row[end] in forbidden: + if end < len(row) and row[end] in arch.forbidden: return full return hex(int(full)) -def parse_relocated_line(line): - try: - ind2 = line.rindex(",") - except ValueError: - ind2 = line.rindex("\t") +def parse_relocated_line(line: str) -> Tuple[str, str, str]: + for c in ",\t ": + if c in line: + ind2 = line.rindex(c) + break + else: + raise Exception(f"failed to parse relocated line: {line}") before = line[: ind2 + 1] after = line[ind2 + 1 :] ind2 = after.find("(") @@ -575,7 +1261,7 @@ def parse_relocated_line(line): return before, imm, after -def process_mips_reloc(row, prev): +def process_mips_reloc(row: str, prev: str, arch: ArchSettings) -> str: before, imm, after = parse_relocated_line(prev) repl = row.split()[-1] if imm != "0": @@ -586,7 +1272,10 @@ def process_mips_reloc(row, prev): # TODO: handle unambiguous cases where all addends for a symbol are the # same, or show "+???". mnemonic = prev.split()[0] - if mnemonic in instructions_with_address_immediates and not imm.startswith("0x"): + if ( + mnemonic in arch.instructions_with_address_immediates + and not imm.startswith("0x") + ): imm = "0x" + imm repl += "+" + imm if int(imm, 0) > 0 else imm if "R_MIPS_LO16" in row: @@ -596,98 +1285,166 @@ def process_mips_reloc(row, prev): # correct addend for each, but objdump doesn't give us the order of # the relocations, so we can't find the right LO16. :( repl = f"%hi({repl})" + elif "R_MIPS_26" in row: + # Function calls + pass + elif "R_MIPS_PC16" in row: + # Branch to glabel. This gives confusing output, but there's not much + # we can do here. + pass else: - assert "R_MIPS_26" in row, f"unknown relocation type '{row}'" + assert False, f"unknown relocation type '{row}' for line '{prev}'" return before + repl + after -def pad_mnemonic(line): +def process_ppc_reloc(row: str, prev: str) -> str: + assert any( + r in row for r in ["R_PPC_REL24", "R_PPC_ADDR16", "R_PPC_EMB_SDA21"] + ), f"unknown relocation type '{row}' for line '{prev}'" + before, imm, after = parse_relocated_line(prev) + repl = row.split()[-1] + if "R_PPC_REL24" in row: + # function calls + pass + elif "R_PPC_ADDR16_HI" in row: + # absolute hi of addr + repl = f"{repl}@h" + elif "R_PPC_ADDR16_HA" in row: + # adjusted hi of addr + repl = f"{repl}@ha" + elif "R_PPC_ADDR16_LO" in row: + # lo of addr + repl = f"{repl}@l" + elif "R_PPC_ADDR16" in row: + # 16-bit absolute addr + if "+0x7" in repl: + # remove the very large addends as they are an artifact of (label-_SDA(2)_BASE_) + # computations and are unimportant in a diff setting. + if int(repl.split("+")[1], 16) > 0x70000000: + repl = repl.split("+")[0] + elif "R_PPC_EMB_SDA21" in row: + # small data area + pass + return before + repl + after + + +def pad_mnemonic(line: str) -> str: if "\t" not in line: return line mn, args = line.split("\t", 1) return f"{mn:<7s} {args}" -class Line(NamedTuple): +@dataclass +class Line: mnemonic: str diff_row: str original: str + normalized_original: str line_num: str branch_target: Optional[str] source_lines: List[str] comment: Optional[str] -def process(lines): +def process(lines: List[str], config: Config) -> List[Line]: + arch = config.arch + normalizer = arch.difference_normalizer(config) skip_next = False source_lines = [] - if not args.diff_obj: + if not config.diff_obj: lines = lines[7:] if lines and not lines[-1]: lines.pop() - output = [] + i = 0 + output: List[Line] = [] stop_after_delay_slot = False - for row in lines: - if args.diff_obj and (">:" in row or not row): + while i < len(lines): + row = lines[i] + i += 1 + + if config.diff_obj and (">:" in row or not row): continue - if args.source and (row and row[0] != " "): + if config.source and not config.source_old_binutils and (row and row[0] != " "): source_lines.append(row) continue - if "R_AARCH64_" in row: - # TODO: handle relocation + if ( + config.source + and config.source_old_binutils + and (row and not re.match(r"^ +[0-9a-f]+:\t", row)) + ): + source_lines.append(row) continue - if "R_MIPS_" in row: - # N.B. Don't transform the diff rows, they already ignore immediates - # if output[-1].diff_row != "": - # output[-1] = output[-1].replace(diff_row=process_mips_reloc(row, output[-1].row_with_imm)) - new_original = process_mips_reloc(row, output[-1].original) - output[-1] = output[-1]._replace(original=new_original) - continue - - m_comment = re.search(re_comment, row) + m_comment = re.search(arch.re_comment, row) comment = m_comment[0] if m_comment else None - row = re.sub(re_comment, "", row) + row = re.sub(arch.re_comment, "", row) row = row.rstrip() tabs = row.split("\t") row = "\t".join(tabs[2:]) line_num = tabs[0].strip() - row_parts = row.split("\t", 1) + + if "\t" in row: + row_parts = row.split("\t", 1) + else: + # powerpc-eabi-objdump doesn't use tabs + row_parts = [part.lstrip() for part in row.split(" ", 1)] mnemonic = row_parts[0].strip() - if mnemonic not in instructions_with_address_immediates: - row = re.sub(re_int, lambda s: hexify_int(row, s), row) + + if mnemonic not in arch.instructions_with_address_immediates: + row = re.sub(arch.re_int, lambda m: hexify_int(row, m, arch), row) + + # Let 'original' be 'row' with relocations applied, while we continue + # transforming 'row' into a coarser version that ignores registers and + # immediates. original = row + + while i < len(lines): + reloc_row = lines[i] + if "R_AARCH64_" in reloc_row: + # TODO: handle relocation + pass + elif "R_MIPS_" in reloc_row: + original = process_mips_reloc(reloc_row, original, arch) + elif "R_PPC_" in reloc_row: + original = process_ppc_reloc(reloc_row, original) + else: + break + i += 1 + + normalized_original = normalizer.normalize(mnemonic, original) if skip_next: skip_next = False row = "" mnemonic = "" - if mnemonic in branch_likely_instructions: + if mnemonic in arch.branch_likely_instructions: skip_next = True - row = re.sub(re_reg, "", row) - row = re.sub(re_sprel, "addr(sp)", row) + row = re.sub(arch.re_reg, "", row) + row = re.sub(arch.re_sprel, "addr(sp)", row) row_with_imm = row - if mnemonic in instructions_with_address_immediates: + if mnemonic in arch.instructions_with_address_immediates: row = row.strip() row, _ = split_off_branch(row) row += "" else: - row = normalize_imms(row) + row = normalize_imms(row, arch) branch_target = None - if mnemonic in branch_instructions: - target = row_parts[1].strip().split(",")[-1] - if mnemonic in branch_likely_instructions: - target = hex(int(target, 16) - 4)[2:] - branch_target = target.strip() + if mnemonic in arch.branch_instructions: + target = int(row_parts[1].strip().split(",")[-1], 16) + if mnemonic in arch.branch_likely_instructions: + target -= 4 + branch_target = hex(target)[2:] output.append( Line( mnemonic=mnemonic, diff_row=row, original=original, + normalized_original=normalized_original, line_num=line_num, branch_target=branch_target, source_lines=source_lines, @@ -696,7 +1453,7 @@ def process(lines): ) source_lines = [] - if args.stop_jrra and mnemonic == "jr" and row_parts[1].strip() == "ra": + if config.stop_jrra and mnemonic == "jr" and row_parts[1].strip() == "ra": stop_after_delay_slot = True elif stop_after_delay_slot: break @@ -704,41 +1461,20 @@ def process(lines): return output -def format_single_line_diff(line1, line2, column_width): - return f"{ansi_ljust(line1,column_width)}{line2}" +def normalize_imms(row: str, arch: ArchSettings) -> str: + return re.sub(arch.re_imm, "", row) -class SymbolColorer: - def __init__(self, base_index): - self.color_index = base_index - self.symbol_colors = {} - - def color_symbol(self, s, t=None): - try: - color = self.symbol_colors[s] - except: - color = COLOR_ROTATION[self.color_index % len(COLOR_ROTATION)] - self.color_index += 1 - self.symbol_colors[s] = color - t = t or s - return f"{color}{t}{Fore.RESET}" +def normalize_stack(row: str, arch: ArchSettings) -> str: + return re.sub(arch.re_sprel, "addr(sp)", row) -def maybe_normalize_large_imms(row): - if args.ignore_large_imms: - row = re.sub(re_large_imm, "", row) - return row +def imm_matches_everything(row: str, arch: ArchSettings) -> bool: + # (this should probably be arch-specific) + return "(." in row -def normalize_imms(row): - return re.sub(re_imm, "", row) - - -def normalize_stack(row): - return re.sub(re_sprel, "addr(sp)", row) - - -def split_off_branch(line): +def split_off_branch(line: str) -> Tuple[str, str]: parts = line.split(",") if len(parts) < 2: parts = line.split(None, 1) @@ -746,39 +1482,18 @@ def split_off_branch(line): return line[:off], line[off:] -def color_imms(out1, out2): - g1 = [] - g2 = [] - re.sub(re_imm, lambda s: g1.append(s.group()), out1) - re.sub(re_imm, lambda s: g2.append(s.group()), out2) - if len(g1) == len(g2): - diffs = [x != y for (x, y) in zip(g1, g2)] - it = iter(diffs) - - def maybe_color(s): - return f"{Fore.LIGHTBLUE_EX}{s}{Style.RESET_ALL}" if next(it) else s - - out1 = re.sub(re_imm, lambda s: maybe_color(s.group()), out1) - it = iter(diffs) - out2 = re.sub(re_imm, lambda s: maybe_color(s.group()), out2) - return out1, out2 - - -def color_branch_imms(br1, br2): - if br1 != br2: - br1 = f"{Fore.LIGHTBLUE_EX}{br1}{Style.RESET_ALL}" - br2 = f"{Fore.LIGHTBLUE_EX}{br2}{Style.RESET_ALL}" - return br1, br2 - - -def diff_sequences_difflib(seq1, seq2): +def diff_sequences_difflib( + seq1: List[str], seq2: List[str] +) -> List[Tuple[str, int, int, int, int]]: differ = difflib.SequenceMatcher(a=seq1, b=seq2, autojunk=False) return differ.get_opcodes() -def diff_sequences(seq1, seq2): +def diff_sequences( + seq1: List[str], seq2: List[str], algorithm: str +) -> List[Tuple[str, int, int, int, int]]: if ( - args.algorithm != "levenshtein" + algorithm != "levenshtein" or len(seq1) * len(seq2) > 4 * 10 ** 8 or len(seq1) + len(seq2) >= 0x110000 ): @@ -786,9 +1501,9 @@ def diff_sequences(seq1, seq2): # The Levenshtein library assumes that we compare strings, not lists. Convert. # (Per the check above we know we have fewer than 0x110000 unique elements, so chr() works.) - remapping = {} + remapping: Dict[str, str] = {} - def remap(seq): + def remap(seq: List[str]) -> str: seq = seq[:] for i in range(len(seq)): val = remapping.get(seq[i]) @@ -798,58 +1513,23 @@ def diff_sequences(seq1, seq2): seq[i] = val return "".join(seq) - seq1 = remap(seq1) - seq2 = remap(seq2) - return Levenshtein.opcodes(seq1, seq2) + rem1 = remap(seq1) + rem2 = remap(seq2) + import Levenshtein # type: ignore + + return Levenshtein.opcodes(rem1, rem2) # type: ignore -class OutputLine: - base: Optional[str] - fmt2: str - key2: str - - def __init__(self, base: Optional[str], fmt2: str, key2: str) -> None: - self.base = base - self.fmt2 = fmt2 - self.key2 = key2 - - def __eq__(self, other: object) -> bool: - if not isinstance(other, OutputLine): - return NotImplemented - return self.key2 == other.key2 - - def __hash__(self) -> int: - return hash(self.key2) - - -def do_diff(basedump: str, mydump: str) -> List[OutputLine]: - output: List[OutputLine] = [] - - lines1 = process(basedump.split("\n")) - lines2 = process(mydump.split("\n")) - - sc1 = SymbolColorer(0) - sc2 = SymbolColorer(0) - sc3 = SymbolColorer(4) - sc4 = SymbolColorer(4) - sc5 = SymbolColorer(0) - sc6 = SymbolColorer(0) - bts1: Set[str] = set() - bts2: Set[str] = set() - - if args.show_branches: - for (lines, btset, sc) in [ - (lines1, bts1, sc5), - (lines2, bts2, sc6), - ]: - for line in lines: - bt = line.branch_target - if bt is not None: - btset.add(bt + ":") - sc.color_symbol(bt + ":") - +def diff_lines( + lines1: List[Line], + lines2: List[Line], + algorithm: str, +) -> List[Tuple[Optional[Line], Optional[Line]]]: + ret = [] for (tag, i1, i2, j1, j2) in diff_sequences( - [line.mnemonic for line in lines1], [line.mnemonic for line in lines2] + [line.mnemonic for line in lines1], + [line.mnemonic for line in lines2], + algorithm, ): for line1, line2 in itertools.zip_longest(lines1[i1:i2], lines2[j1:j2]): if tag == "replace": @@ -861,133 +1541,215 @@ def do_diff(basedump: str, mydump: str) -> List[OutputLine]: assert line1 is None elif tag == "delete": assert line2 is None + ret.append((line1, line2)) - line_color1 = line_color2 = sym_color = Fore.RESET - line_prefix = " " - if line1 and line2 and line1.diff_row == line2.diff_row: - if maybe_normalize_large_imms( - line1.original - ) == maybe_normalize_large_imms(line2.original): - out1 = line1.original - out2 = line2.original - elif line1.diff_row == "": - out1 = f"{Style.BRIGHT}{Fore.LIGHTBLACK_EX}{line1.original}" - out2 = f"{Style.BRIGHT}{Fore.LIGHTBLACK_EX}{line2.original}" + return ret + + +@dataclass(frozen=True) +class OutputLine: + base: Optional[Text] = field(compare=False) + fmt2: Text = field(compare=False) + key2: Optional[str] + boring: bool = field(compare=False) + + +def do_diff(basedump: str, mydump: str, config: Config) -> List[OutputLine]: + if config.source: + import cxxfilt # type: ignore + arch = config.arch + fmt = config.formatter + output: List[OutputLine] = [] + + lines1 = process(basedump.split("\n"), config) + lines2 = process(mydump.split("\n"), config) + + sc1 = symbol_formatter("base-reg", 0) + sc2 = symbol_formatter("my-reg", 0) + sc3 = symbol_formatter("base-stack", 4) + sc4 = symbol_formatter("my-stack", 4) + sc5 = symbol_formatter("base-branch", 0) + sc6 = symbol_formatter("my-branch", 0) + bts1: Set[str] = set() + bts2: Set[str] = set() + + if config.show_branches: + for (lines, btset, sc) in [ + (lines1, bts1, sc5), + (lines2, bts2, sc6), + ]: + for line in lines: + bt = line.branch_target + if bt is not None: + text = f"{bt}:" + btset.add(text) + sc(text) + + for (line1, line2) in diff_lines(lines1, lines2, config.algorithm): + line_color1 = line_color2 = sym_color = BasicFormat.NONE + line_prefix = " " + out1 = Text() if not line1 else Text(pad_mnemonic(line1.original)) + out2 = Text() if not line2 else Text(pad_mnemonic(line2.original)) + if line1 and line2 and line1.diff_row == line2.diff_row: + if line1.normalized_original == line2.normalized_original: + pass + elif line1.diff_row == "": + out1 = out1.reformat(BasicFormat.DELAY_SLOT) + out2 = out2.reformat(BasicFormat.DELAY_SLOT) + else: + mnemonic = line1.original.split()[0] + branchless1, branch1 = out1.plain(), "" + branchless2, branch2 = out2.plain(), "" + if mnemonic in arch.instructions_with_address_immediates: + branchless1, branch1 = split_off_branch(branchless1) + branchless2, branch2 = split_off_branch(branchless2) + + out1 = Text(branchless1) + out2 = Text(branchless2) + out1, out2 = format_fields( + arch.re_imm, out1, out2, lambda _: BasicFormat.IMMEDIATE + ) + + same_relative_target = False + if line1.branch_target is not None and line2.branch_target is not None: + relative_target1 = eval_line_num( + line1.branch_target + ) - eval_line_num(line1.line_num) + relative_target2 = eval_line_num( + line2.branch_target + ) - eval_line_num(line2.line_num) + same_relative_target = relative_target1 == relative_target2 + + if normalize_imms(branchless1, arch) == normalize_imms( + branchless2, arch + ): + if imm_matches_everything(branchless2, arch): + out1 = out1.reformat(BasicFormat.NONE) + out2 = out2.reformat(BasicFormat.NONE) + elif not same_relative_target: + # only imms differences + sym_color = BasicFormat.IMMEDIATE + line_prefix = "i" else: - mnemonic = line1.original.split()[0] - out1, out2 = line1.original, line2.original - branch1 = branch2 = "" - if mnemonic in instructions_with_address_immediates: - out1, branch1 = split_off_branch(line1.original) - out2, branch2 = split_off_branch(line2.original) - branchless1 = out1 - branchless2 = out2 - out1, out2 = color_imms(out1, out2) - - same_relative_target = False - if line1.branch_target is not None and line2.branch_target is not None: - relative_target1 = eval_line_num(line1.branch_target) - eval_line_num(line1.line_num) - relative_target2 = eval_line_num(line2.branch_target) - eval_line_num(line2.line_num) - same_relative_target = relative_target1 == relative_target2 - - if not same_relative_target: - branch1, branch2 = color_branch_imms(branch1, branch2) - - out1 += branch1 - out2 += branch2 - if normalize_imms(branchless1) == normalize_imms(branchless2): - if not same_relative_target: - # only imms differences - sym_color = Fore.LIGHTBLUE_EX - line_prefix = "i" + out1, out2 = format_fields(arch.re_sprel, out1, out2, sc3, sc4) + if normalize_stack(branchless1, arch) == normalize_stack( + branchless2, arch + ): + # only stack differences (luckily stack and imm + # differences can't be combined in MIPS, so we + # don't have to think about that case) + sym_color = BasicFormat.STACK + line_prefix = "s" else: - out1 = re.sub( - re_sprel, lambda s: sc3.color_symbol(s.group()), out1, - ) - out2 = re.sub( - re_sprel, lambda s: sc4.color_symbol(s.group()), out2, - ) - if normalize_stack(branchless1) == normalize_stack(branchless2): - # only stack differences (luckily stack and imm - # differences can't be combined in MIPS, so we - # don't have to think about that case) - sym_color = Fore.YELLOW - line_prefix = "s" - else: - # regs differences and maybe imms as well - out1 = re.sub( - re_reg, lambda s: sc1.color_symbol(s.group()), out1 + # regs differences and maybe imms as well + out1, out2 = format_fields(arch.re_reg, out1, out2, sc1, sc2) + line_color1 = line_color2 = sym_color = BasicFormat.REGISTER + line_prefix = "r" + + if same_relative_target or branch1 == branch2: + branch_imm_fmt = BasicFormat.NONE + else: + branch_imm_fmt = BasicFormat.IMMEDIATE + out1 += Text(branch1, branch_imm_fmt) + out2 += Text(branch2, branch_imm_fmt) + elif line1 and line2: + line_prefix = "|" + line_color1 = line_color2 = sym_color = BasicFormat.DIFF_CHANGE + out1 = out1.reformat(line_color1) + out2 = out2.reformat(line_color2) + elif line1: + line_prefix = "<" + line_color1 = sym_color = BasicFormat.DIFF_REMOVE + out1 = out1.reformat(line_color1) + out2 = Text() + elif line2: + line_prefix = ">" + line_color2 = sym_color = BasicFormat.DIFF_ADD + out1 = Text() + out2 = out2.reformat(line_color2) + + if config.source and line2 and line2.comment: + out2 += f" {line2.comment}" + + def format_part( + out: Text, + line: Optional[Line], + line_color: Format, + btset: Set[str], + sc: FormatFunction, + ) -> Optional[Text]: + if line is None: + return None + in_arrow = Text(" ") + out_arrow = Text() + if config.show_branches: + if line.line_num in btset: + in_arrow = Text("~>", sc(line.line_num)) + if line.branch_target is not None: + out_arrow = " " + Text("~>", sc(line.branch_target + ":")) + return ( + Text(line.line_num, line_color) + " " + in_arrow + " " + out + out_arrow + ) + + part1 = format_part(out1, line1, line_color1, bts1, sc5) + part2 = format_part(out2, line2, line_color2, bts2, sc6) + + if line2: + for source_line in line2.source_lines: + line_format = BasicFormat.SOURCE_OTHER + if config.source_old_binutils: + if source_line and re.fullmatch(".*\.c(?:pp)?:\d+", source_line): + line_format = BasicFormat.SOURCE_FILENAME + elif source_line and source_line.endswith("():"): + line_format = BasicFormat.SOURCE_FUNCTION + try: + source_line = cxxfilt.demangle( + source_line[:-3], external_only=False ) - out2 = re.sub( - re_reg, lambda s: sc2.color_symbol(s.group()), out2 - ) - line_color1 = line_color2 = sym_color = Fore.YELLOW - line_prefix = "r" - elif line1 and line2: - line_prefix = "|" - line_color1 = Fore.LIGHTBLUE_EX - line_color2 = Fore.LIGHTBLUE_EX - sym_color = Fore.LIGHTBLUE_EX - out1 = line1.original - out2 = line2.original - elif line1: - line_prefix = "<" - line_color1 = sym_color = Fore.RED - out1 = line1.original - out2 = "" - elif line2: - line_prefix = ">" - line_color2 = sym_color = Fore.GREEN - out1 = "" - out2 = line2.original - - if args.source and line2 and line2.comment: - out2 += f" {line2.comment}" - - def format_part(out: str, line: Optional[Line], line_color: str, btset: Set[str], sc: SymbolColorer) -> Optional[str]: - if line is None: - return None - in_arrow = " " - out_arrow = "" - if args.show_branches: - if line.line_num in btset: - in_arrow = sc.color_symbol(line.line_num, "~>") + line_color - if line.branch_target is not None: - out_arrow = " " + sc.color_symbol(line.branch_target + ":", "~>") - out = pad_mnemonic(out) - return f"{line_color}{line.line_num} {in_arrow} {out}{Style.RESET_ALL}{out_arrow}" - - part1 = format_part(out1, line1, line_color1, bts1, sc5) - part2 = format_part(out2, line2, line_color2, bts2, sc6) - key2 = line2.original if line2 else "" - - mid = f"{sym_color}{line_prefix}" - - if line2: - for source_line in line2.source_lines: - color = Style.DIM + except: + pass + else: # File names and function names - if source_line and source_line[0] != "|": - color += Style.BRIGHT + if source_line and source_line[0] != "│": + line_format = BasicFormat.SOURCE_FILENAME # Function names if source_line.endswith("():"): - # Underline. Colorama does not provide this feature, unfortunately. - color += "\u001b[4m" + line_format = BasicFormat.SOURCE_FUNCTION try: source_line = cxxfilt.demangle( source_line[:-3], external_only=False ) except: pass - output.append(OutputLine(None, f" {color}{source_line}{Style.RESET_ALL}", source_line)) + output.append( + OutputLine( + None, + " " + Text(source_line, line_format), + source_line, + True, + ) + ) - fmt2 = mid + " " + (part2 or "") - output.append(OutputLine(part1, fmt2, key2)) + key2 = line2.original if line2 else None + boring = False + if line_prefix == " ": + # Canonicalize matching lines to have an empty string as key, to + # ensure they are treated as the same when three-way diffing. This + # matters for branches that match only relatively. + key2 = "" + boring = True + elif config.compress and config.compress.same_instr and line_prefix in "irs": + boring = True + fmt2 = Text(line_prefix, sym_color) + " " + (part2 or Text()) + output.append(OutputLine(part1, fmt2, key2, boring)) return output def chunk_diff(diff: List[OutputLine]) -> List[Union[List[OutputLine], OutputLine]]: + """Chunk a diff into an alternating list like A B A B ... A, where: + * A is a List[OutputLine] of insertions, + * B is a single non-insertion OutputLine, with .base != None.""" cur_right: List[OutputLine] = [] chunks: List[Union[List[OutputLine], OutputLine]] = [] for output_line in diff: @@ -1001,12 +1763,46 @@ def chunk_diff(diff: List[OutputLine]) -> List[Union[List[OutputLine], OutputLin return chunks -def format_diff(old_diff: List[OutputLine], new_diff: List[OutputLine]) -> Tuple[str, List[str]]: +def compress_matching( + li: List[Tuple[Tuple[Text, ...], bool]], context: int +) -> List[Tuple[Text, ...]]: + ret: List[Tuple[Text, ...]] = [] + matching_streak: List[Tuple[Text, ...]] = [] + context = max(context, 0) + + def flush_matching() -> None: + if len(matching_streak) <= 2 * context + 1: + ret.extend(matching_streak) + else: + ret.extend(matching_streak[:context]) + skipped = len(matching_streak) - 2 * context + filler = Text(f"<{skipped} lines>", BasicFormat.SOURCE_OTHER) + columns = len(matching_streak[0]) + ret.append(tuple([filler] + [Text()] * (columns - 1))) + if context > 0: + ret.extend(matching_streak[-context:]) + matching_streak.clear() + + for (line, matching) in li: + if matching: + matching_streak.append(line) + else: + flush_matching() + ret.append(line) + + flush_matching() + return ret + + +def format_diff( + old_diff: List[OutputLine], new_diff: List[OutputLine], config: Config +) -> Tuple[Optional[Tuple[Text, ...]], List[Tuple[Text, ...]]]: + fmt = config.formatter old_chunks = chunk_diff(old_diff) new_chunks = chunk_diff(new_diff) - output: List[Tuple[str, OutputLine, OutputLine]] = [] + output: List[Tuple[Text, OutputLine, OutputLine]] = [] assert len(old_chunks) == len(new_chunks), "same target" - empty = OutputLine("", "", "") + empty = OutputLine(Text(), Text(), None, True) for old_chunk, new_chunk in zip(old_chunks, new_chunks): if isinstance(old_chunk, list): assert isinstance(new_chunk, list) @@ -1018,74 +1814,92 @@ def format_diff(old_diff: List[OutputLine], new_diff: List[OutputLine]) -> Tuple for (tag, i1, i2, j1, j2) in differ.get_opcodes(): if tag in ["equal", "replace"]: for i, j in zip(range(i1, i2), range(j1, j2)): - output.append(("", old_chunk[i], new_chunk[j])) - elif tag == "insert": - for j in range(j1, j2): - output.append(("", empty, new_chunk[j])) - else: - for i in range(i1, i2): - output.append(("", old_chunk[i], empty)) + output.append((Text(), old_chunk[i], new_chunk[j])) + if tag in ["insert", "replace"]: + for j in range(j1 + i2 - i1, j2): + output.append((Text(), empty, new_chunk[j])) + if tag in ["delete", "replace"]: + for i in range(i1 + j2 - j1, i2): + output.append((Text(), old_chunk[i], empty)) else: assert isinstance(new_chunk, OutputLine) + assert new_chunk.base # old_chunk.base and new_chunk.base have the same text since # both diffs are based on the same target, but they might # differ in color. Use the new version. - output.append((new_chunk.base or "", old_chunk, new_chunk)) + output.append((new_chunk.base, old_chunk, new_chunk)) # TODO: status line, with e.g. approximate permuter score? - width = args.column_width - if args.threeway: - header_line = "TARGET".ljust(width) + " CURRENT".ljust(width) + " PREVIOUS" + header_line: Optional[Tuple[Text, ...]] + diff_lines: List[Tuple[Tuple[Text, ...], bool]] + if config.threeway: + header_line = (Text("TARGET"), Text(" CURRENT"), Text(" PREVIOUS")) diff_lines = [ - ansi_ljust(base, width) - + ansi_ljust(new.fmt2, width) - + (old.fmt2 or "-" if old != new else "") + ( + ( + base, + new.fmt2, + old.fmt2 or Text("-") if old != new else Text(), + ), + new.boring, + ) for (base, old, new) in output ] else: - header_line = "" + header_line = None diff_lines = [ - ansi_ljust(base, width) + new.fmt2 + ((base, new.fmt2), new.boring) for (base, old, new) in output - if base or new.key2 + if base or new.key2 is not None ] - return header_line, diff_lines + if config.compress: + ret_lines = compress_matching(diff_lines, config.compress.context) + else: + ret_lines = [line for line, _ in diff_lines] + return header_line, ret_lines -def debounced_fs_watch(targets, outq, debounce_delay): +def debounced_fs_watch( + targets: List[str], + outq: "queue.Queue[Optional[float]]", + config: Config, + project: ProjectSettings, +) -> None: import watchdog.events # type: ignore import watchdog.observers # type: ignore - class WatchEventHandler(watchdog.events.FileSystemEventHandler): - def __init__(self, queue, file_targets): + class WatchEventHandler(watchdog.events.FileSystemEventHandler): # type: ignore + def __init__( + self, queue: "queue.Queue[float]", file_targets: List[str] + ) -> None: self.queue = queue self.file_targets = file_targets - def on_modified(self, ev): + def on_modified(self, ev: object) -> None: if isinstance(ev, watchdog.events.FileModifiedEvent): self.changed(ev.src_path) - def on_moved(self, ev): + def on_moved(self, ev: object) -> None: if isinstance(ev, watchdog.events.FileMovedEvent): self.changed(ev.dest_path) - def should_notify(self, path): + def should_notify(self, path: str) -> bool: for target in self.file_targets: - if path == target: + if os.path.normpath(path) == target: return True - if args.make and any( - path.endswith(suffix) for suffix in FS_WATCH_EXTENSIONS + if config.make and any( + path.endswith(suffix) for suffix in project.source_extensions ): return True return False - def changed(self, path): + def changed(self, path: str) -> None: if self.should_notify(path): self.queue.put(time.time()) - def debounce_thread(): - listenq = queue.Queue() - file_targets = [] + def debounce_thread() -> NoReturn: + listenq: "queue.Queue[float]" = queue.Queue() + file_targets: List[str] = [] event_handler = WatchEventHandler(listenq, file_targets) observer = watchdog.observers.Observer() observed = set() @@ -1093,7 +1907,7 @@ def debounced_fs_watch(targets, outq, debounce_delay): if os.path.isdir(target): observer.schedule(event_handler, target, recursive=True) else: - file_targets.append(target) + file_targets.append(os.path.normpath(target)) target = os.path.dirname(target) or "." if target not in observed: observed.add(target) @@ -1103,7 +1917,7 @@ def debounced_fs_watch(targets, outq, debounce_delay): t = listenq.get() more = True while more: - delay = t + debounce_delay - time.time() + delay = t + DEBOUNCE_DELAY - time.time() if delay > 0: time.sleep(delay) # consume entire queue @@ -1121,22 +1935,36 @@ def debounced_fs_watch(targets, outq, debounce_delay): class Display: - def __init__(self, basedump, mydump): + basedump: str + mydump: str + config: Config + emsg: Optional[str] + last_diff_output: Optional[List[OutputLine]] + pending_update: Optional[Tuple[str, bool]] + ready_queue: "queue.Queue[None]" + watch_queue: "queue.Queue[Optional[float]]" + less_proc: "Optional[subprocess.Popen[bytes]]" + + def __init__(self, basedump: str, mydump: str, config: Config) -> None: + self.config = config self.basedump = basedump self.mydump = mydump self.emsg = None self.last_diff_output = None - def run_less(self): + def run_diff(self) -> str: if self.emsg is not None: - output = self.emsg - else: - diff_output = do_diff(self.basedump, self.mydump) - last_diff_output = self.last_diff_output or diff_output + return self.emsg + + diff_output = do_diff(self.basedump, self.mydump, self.config) + last_diff_output = self.last_diff_output or diff_output + if self.config.threeway != "base" or not self.last_diff_output: self.last_diff_output = diff_output - header, diff_lines = format_diff(last_diff_output, diff_output) - header_lines = [header] if header else [] - output = "\n".join(header_lines + diff_lines[args.skip_lines :]) + header, diff_lines = format_diff(last_diff_output, diff_output, self.config) + return self.config.formatter.table(header, diff_lines[self.config.skip_lines :]) + + def run_less(self) -> "Tuple[subprocess.Popen[bytes], subprocess.Popen[bytes]]": + output = self.run_diff() # Pipe the output through 'tail' and only then to less, to ensure the # write call doesn't block. ('tail' has to buffer all its input before @@ -1146,17 +1974,19 @@ class Display: BUFFER_CMD, stdin=subprocess.PIPE, stdout=subprocess.PIPE ) less_proc = subprocess.Popen(LESS_CMD, stdin=buffer_proc.stdout) + assert buffer_proc.stdin + assert buffer_proc.stdout buffer_proc.stdin.write(output.encode()) buffer_proc.stdin.close() buffer_proc.stdout.close() return (buffer_proc, less_proc) - def run_sync(self): + def run_sync(self) -> None: proca, procb = self.run_less() procb.wait() proca.wait() - def run_async(self, watch_queue): + def run_async(self, watch_queue: "queue.Queue[Optional[float]]") -> None: self.watch_queue = watch_queue self.ready_queue = queue.Queue() self.pending_update = None @@ -1164,10 +1994,10 @@ class Display: dthread.start() self.ready_queue.get() - def display_thread(self): + def display_thread(self) -> None: proca, procb = self.run_less() self.less_proc = procb - self.ready_queue.put(0) + self.ready_queue.put(None) while True: ret = procb.wait() proca.wait() @@ -1186,19 +2016,19 @@ class Display: self.emsg = msg proca, procb = self.run_less() self.less_proc = procb - self.ready_queue.put(0) + self.ready_queue.put(None) else: # terminated by user, or killed self.watch_queue.put(None) - self.ready_queue.put(0) + self.ready_queue.put(None) break - def progress(self, msg): + def progress(self, msg: str) -> None: # Write message to top-left corner sys.stdout.write("\x1b7\x1b[1;1f{}\x1b8".format(msg + " ")) sys.stdout.flush() - def update(self, text, error): + def update(self, text: str, error: bool) -> None: if not error and not self.emsg and text == self.mydump: self.progress("Unchanged. ") return @@ -1208,23 +2038,55 @@ class Display: self.less_proc.kill() self.ready_queue.get() - def terminate(self): + def terminate(self) -> None: if not self.less_proc: return self.less_proc.kill() self.ready_queue.get() -def main(): +def main() -> None: + args = parser.parse_args() + + # Apply project-specific configuration. + settings: Dict[str, Any] = {} + diff_settings.apply(settings, args) # type: ignore + project = create_project_settings(settings) + + config = create_config(args, project) + + if config.algorithm == "levenshtein": + try: + import Levenshtein + except ModuleNotFoundError as e: + fail(MISSING_PREREQUISITES.format(e.name)) + + if config.source: + try: + import cxxfilt + except ModuleNotFoundError as e: + fail(MISSING_PREREQUISITES.format(e.name)) + + if config.threeway and not args.watch: + fail("Threeway diffing requires -w.") + if args.diff_elf_symbol: - make_target, basecmd, mycmd = dump_elf() - elif args.diff_obj: - make_target, basecmd, mycmd = dump_objfile() + make_target, basecmd, mycmd = dump_elf( + args.start, args.end, args.diff_elf_symbol, config, project + ) + elif config.diff_obj: + make_target, basecmd, mycmd = dump_objfile( + args.start, args.end, config, project + ) else: - make_target, basecmd, mycmd = dump_binary() + make_target, basecmd, mycmd = dump_binary(args.start, args.end, config, project) + + map_build_target_fn = getattr(diff_settings, "map_build_target", None) + if map_build_target_fn: + make_target = map_build_target_fn(make_target=make_target) if args.write_asm is not None: - mydump = run_objdump(mycmd) + mydump = run_objdump(mycmd, config, project) with open(args.write_asm, "w") as f: f.write(mydump) print(f"Wrote assembly to {args.write_asm}.") @@ -1234,34 +2096,40 @@ def main(): with open(args.base_asm) as f: basedump = f.read() else: - basedump = run_objdump(basecmd) + basedump = run_objdump(basecmd, config, project) - mydump = run_objdump(mycmd) + mydump = run_objdump(mycmd, config, project) - display = Display(basedump, mydump) + display = Display(basedump, mydump, config) - if not args.watch: + if args.no_pager or args.format == "html": + print(display.run_diff()) + elif not args.watch: display.run_sync() else: if not args.make: yn = input( - "Warning: watch-mode (-w) enabled without auto-make (-m). You will have to run make manually. Ok? (Y/n) " + "Warning: watch-mode (-w) enabled without auto-make (-m). " + "You will have to run make manually. Ok? (Y/n) " ) if yn.lower() == "n": return if args.make: watch_sources = None - if hasattr(diff_settings, "watch_sources_for_target"): - watch_sources = diff_settings.watch_sources_for_target(make_target) - watch_sources = watch_sources or source_directories + watch_sources_for_target_fn = getattr( + diff_settings, "watch_sources_for_target", None + ) + if watch_sources_for_target_fn: + watch_sources = watch_sources_for_target_fn(make_target) + watch_sources = watch_sources or project.source_directories if not watch_sources: fail("Missing source_directories config, don't know what to watch.") else: watch_sources = [make_target] - q = queue.Queue() - debounced_fs_watch(watch_sources, q, DEBOUNCE_DELAY) + q: "queue.Queue[Optional[float]]" = queue.Queue() + debounced_fs_watch(watch_sources, q, config, project) display.run_async(q) - last_build = 0 + last_build = 0.0 try: while True: t = q.get() @@ -1272,7 +2140,7 @@ def main(): last_build = time.time() if args.make: display.progress("Building...") - ret = run_make(make_target, capture_output=True) + ret = run_make_capture_output(make_target, project) if ret.returncode != 0: display.update( ret.stderr.decode("utf-8-sig", "replace") @@ -1280,10 +2148,11 @@ def main(): error=True, ) continue - mydump = run_objdump(mycmd) + mydump = run_objdump(mycmd, config, project) display.update(mydump, error=False) except KeyboardInterrupt: display.terminate() -main() +if __name__ == "__main__": + main() diff --git a/tools/asm-differ/diff_settings.py b/tools/asm-differ/diff_settings.py index b9d88fe6c..456ffdfdc 100644 --- a/tools/asm-differ/diff_settings.py +++ b/tools/asm-differ/diff_settings.py @@ -1,7 +1,10 @@ -#!/usr/bin/env python3 - def apply(config, args): - config['baseimg'] = 'target.bin' - config['myimg'] = 'source.bin' - config['mapfile'] = 'build.map' - config['source_directories'] = ['.'] + config["baseimg"] = "target.bin" + config["myimg"] = "source.bin" + config["mapfile"] = "build.map" + config["source_directories"] = ["."] + # config["arch"] = "mips" + # config["map_format"] = "gnu" # gnu or mw + # config["mw_build_dir"] = "build/" # only needed for mw map format + # config["makeflags"] = [] + # config["objdump_executable"] = "" diff --git a/tools/asm-differ/mypy.ini b/tools/asm-differ/mypy.ini index f7a01b342..c6b3bba20 100644 --- a/tools/asm-differ/mypy.ini +++ b/tools/asm-differ/mypy.ini @@ -11,6 +11,7 @@ warn_redundant_casts = True warn_return_any = True warn_unused_ignores = True python_version = 3.6 +files = diff.py [mypy-diff_settings] ignore_errors = True diff --git a/tools/asm-differ/screenshot.png b/tools/asm-differ/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..3230555328cd9acdf388ce1626415ceb78760535 GIT binary patch literal 99842 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 literal 0 HcmV?d00001 diff --git a/tools/asm-processor/asm_processor.py b/tools/asm-processor/asm_processor.py index 302668d7d..6923752be 100644 --- a/tools/asm-processor/asm_processor.py +++ b/tools/asm-processor/asm_processor.py @@ -269,6 +269,43 @@ class Section: assert self.sh_type == SHT_SYMTAB return self.symbol_entries[self.sh_info:] + def relocate_mdebug(self, original_offset): + assert self.sh_type == SHT_MIPS_DEBUG + new_data = bytearray(self.data) + shift_by = self.sh_offset - original_offset + + # Update the file-relative offsets in the Symbolic HDRR + hdrr_magic, hdrr_vstamp, hdrr_ilineMax, hdrr_cbLine, \ + hdrr_cbLineOffset, hdrr_idnMax, hdrr_cbDnOffset, hdrr_ipdMax, \ + hdrr_cbPdOffset, hdrr_isymMax, hdrr_cbSymOffset, hdrr_ioptMax, \ + hdrr_cbOptOffset, hdrr_iauxMax, hdrr_cbAuxOffset, hdrr_issMax, \ + hdrr_cbSsOffset, hdrr_issExtMax, hdrr_cbSsExtOffset, hdrr_ifdMax, \ + hdrr_cbFdOffset, hdrr_crfd, hdrr_cbRfdOffset, hdrr_iextMax, \ + hdrr_cbExtOffset = struct.unpack(">HHIIIIIIIIIIIIIIIIIIIIIII", self.data[0:0x60]) + + assert hdrr_magic == 0x7009 , "Invalid magic value for .mdebug symbolic header" + + hdrr_cbLineOffset += shift_by + hdrr_cbDnOffset += shift_by + hdrr_cbPdOffset += shift_by + hdrr_cbSymOffset += shift_by + hdrr_cbOptOffset += shift_by + hdrr_cbAuxOffset += shift_by + hdrr_cbSsOffset += shift_by + hdrr_cbSsExtOffset += shift_by + hdrr_cbFdOffset += shift_by + hdrr_cbRfdOffset += shift_by + hdrr_cbExtOffset += shift_by + + new_data[0:0x60] = struct.pack(">HHIIIIIIIIIIIIIIIIIIIIIII", hdrr_magic, hdrr_vstamp, hdrr_ilineMax, hdrr_cbLine, \ + hdrr_cbLineOffset, hdrr_idnMax, hdrr_cbDnOffset, hdrr_ipdMax, \ + hdrr_cbPdOffset, hdrr_isymMax, hdrr_cbSymOffset, hdrr_ioptMax, \ + hdrr_cbOptOffset, hdrr_iauxMax, hdrr_cbAuxOffset, hdrr_issMax, \ + hdrr_cbSsOffset, hdrr_issExtMax, hdrr_cbSsExtOffset, hdrr_ifdMax, \ + hdrr_cbFdOffset, hdrr_crfd, hdrr_cbRfdOffset, hdrr_iextMax, \ + hdrr_cbExtOffset) + + self.data = bytes(new_data) class ElfFile: def __init__(self, data): @@ -317,7 +354,7 @@ class ElfFile: s.late_init(self.sections) return s - def drop_irrelevant_sections(self): + def drop_mdebug_gptab(self): # We can only drop sections at the end, since otherwise section # references might be wrong. Luckily, these sections typically are. while self.sections[-1].sh_type in [SHT_MIPS_DEBUG, SHT_MIPS_GPTAB]: @@ -340,7 +377,11 @@ class ElfFile: for s in self.sections: if s.sh_type != SHT_NOBITS and s.sh_type != SHT_NULL: pad_out(s.sh_addralign) + old_offset = s.sh_offset s.sh_offset = outidx + if s.sh_type == SHT_MIPS_DEBUG and s.sh_offset != old_offset: + # The .mdebug section has moved, relocate offsets + s.relocate_mdebug(old_offset) write_out(s.data) pad_out(4) @@ -380,7 +421,7 @@ class Failure(Exception): class GlobalState: - def __init__(self, min_instr_count, skip_instr_count, use_jtbl_for_rodata): + def __init__(self, min_instr_count, skip_instr_count, use_jtbl_for_rodata, mips1): # A value that hopefully never appears as a 32-bit rodata constant (or we # miscompile late rodata). Increases by 1 in each step. self.late_rodata_hex = 0xE0123456 @@ -388,6 +429,7 @@ class GlobalState: self.min_instr_count = min_instr_count self.skip_instr_count = skip_instr_count self.use_jtbl_for_rodata = use_jtbl_for_rodata + self.mips1 = mips1 def next_late_rodata_hex(self): dummy_bytes = struct.pack('>I', self.late_rodata_hex) @@ -608,12 +650,14 @@ class GlobalAsmBlock: size = self.fn_section_sizes['.late_rodata'] // 4 skip_next = False needs_double = (self.late_rodata_alignment != 0) + extra_mips1_nop = False + jtbl_size = 11 if state.mips1 else 9 for i in range(size): if skip_next: skip_next = False continue - # Jump tables give 9 instructions for >= 5 words of rodata, and should be - # emitted when: + # Jump tables give 9 instructions (11 with -mips1) for >= 5 words of rodata, + # and should be emitted when: # - -O2 or -O2 -g3 are used, which give the right codegen # - we have emitted our first .float/.double (to ensure that we find the # created rodata in the binary) @@ -624,11 +668,12 @@ class GlobalAsmBlock: # - we have at least 10 more instructions to go in this function (otherwise our # function size computation will be wrong since the delay slot goes unused) if (not needs_double and state.use_jtbl_for_rodata and i >= 1 and - size - i >= 5 and num_instr - len(late_rodata_fn_output) >= 10): + size - i >= 5 and num_instr - len(late_rodata_fn_output) >= jtbl_size + 1): cases = " ".join("case {}:".format(case) for case in range(size - i)) late_rodata_fn_output.append("switch (*(volatile int*)0) { " + cases + " ; }") - late_rodata_fn_output.extend([""] * 8) + late_rodata_fn_output.extend([""] * (jtbl_size - 1)) jtbl_rodata_size = (size - i) * 4 + extra_mips1_nop = i != 2 break dummy_bytes = state.next_late_rodata_hex() late_rodata_dummy_bytes.append(dummy_bytes) @@ -638,12 +683,20 @@ class GlobalAsmBlock: fval, = struct.unpack('>d', dummy_bytes + dummy_bytes2) late_rodata_fn_output.append('*(volatile double*)0 = {};'.format(fval)) skip_next = True - needs_double = True + needs_double = False + if state.mips1: + # mips1 does not have ldc1/sdc1 + late_rodata_fn_output.append('') + late_rodata_fn_output.append('') + extra_mips1_nop = False else: fval, = struct.unpack('>f', dummy_bytes) late_rodata_fn_output.append('*(volatile float*)0 = {}f;'.format(fval)) + extra_mips1_nop = True late_rodata_fn_output.append('') late_rodata_fn_output.append('') + if state.mips1 and extra_mips1_nop: + late_rodata_fn_output.append('') text_name = None if self.fn_section_sizes['.text'] > 0 or late_rodata_fn_output: @@ -722,7 +775,7 @@ float_regexpr = re.compile(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?f") def repl_float_hex(m): return str(struct.unpack(">I", struct.pack(">f", float(m.group(0).strip().rstrip("f"))))[0]) -def parse_source(f, opt, framepointer, input_enc, output_enc, out_dependencies, print_source=None): +def parse_source(f, opt, framepointer, mips1, input_enc, output_enc, out_dependencies, print_source=None): if opt in ['O2', 'O1']: if framepointer: min_instr_count = 6 @@ -751,7 +804,7 @@ def parse_source(f, opt, framepointer, input_enc, output_enc, out_dependencies, if opt in ['O2', 'g3'] and not framepointer: use_jtbl_for_rodata = True - state = GlobalState(min_instr_count, skip_instr_count, use_jtbl_for_rodata) + state = GlobalState(min_instr_count, skip_instr_count, use_jtbl_for_rodata, mips1) global_asm = None asm_functions = [] @@ -803,7 +856,7 @@ def parse_source(f, opt, framepointer, input_enc, output_enc, out_dependencies, out_dependencies.append(fname) include_src = StringIO() with open(fname, encoding=input_enc) as include_file: - parse_source(include_file, opt, framepointer, input_enc, output_enc, out_dependencies, include_src) + parse_source(include_file, opt, framepointer, mips1, input_enc, output_enc, out_dependencies, include_src) include_src.write('#line ' + str(line_no + 1) + ' "' + f.name + '"') output_lines[-1] = include_src.getvalue() include_src.close() @@ -831,7 +884,7 @@ def parse_source(f, opt, framepointer, input_enc, output_enc, out_dependencies, return asm_functions -def fixup_objfile(objfile_name, functions, asm_prelude, assembler, output_enc): +def fixup_objfile(objfile_name, functions, asm_prelude, assembler, output_enc, drop_mdebug_gptab): SECTIONS = ['.data', '.text', '.rodata', '.bss'] with open(objfile_name, 'rb') as f: @@ -927,9 +980,12 @@ def fixup_objfile(objfile_name, functions, asm_prelude, assembler, output_enc): with open(o_name, 'rb') as f: asm_objfile = ElfFile(f.read()) - # Remove some clutter from objdump output + # Remove clutter from objdump output for tests, and make the tests + # portable by avoiding absolute paths. Outside of tests .mdebug is + # useful for showing source together with asm, though. mdebug_section = objfile.find_section('.mdebug') - objfile.drop_irrelevant_sections() + if drop_mdebug_gptab: + objfile.drop_mdebug_gptab() # Unify reginfo sections target_reginfo = objfile.find_section('.reginfo') @@ -1176,9 +1232,11 @@ def run_wrapped(argv, outfile, functions): parser.add_argument('--post-process', dest='objfile', help="path to .o file to post-process") parser.add_argument('--assembler', dest='assembler', help="assembler command (e.g. \"mips-linux-gnu-as -march=vr4300 -mabi=32\")") parser.add_argument('--asm-prelude', dest='asm_prelude', help="path to a file containing a prelude to the assembly file (with .set and .macro directives, e.g.)") - parser.add_argument('--input-enc', default='latin1', help="Input encoding (default: latin1)") - parser.add_argument('--output-enc', default='latin1', help="Output encoding (default: latin1)") + parser.add_argument('--input-enc', default='latin1', help="input encoding (default: %(default)s)") + parser.add_argument('--output-enc', default='latin1', help="output encoding (default: %(default)s)") + parser.add_argument('--drop-mdebug-gptab', dest='drop_mdebug_gptab', action='store_true', help="drop mdebug and gptab sections") parser.add_argument('-framepointer', dest='framepointer', action='store_true') + parser.add_argument('-mips1', dest='mips1', action='store_true') parser.add_argument('-g3', dest='g3', action='store_true') group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-O1', dest='opt', action='store_const', const='O1') @@ -1190,25 +1248,27 @@ def run_wrapped(argv, outfile, functions): if opt != 'O2': raise Failure("-g3 is only supported together with -O2") opt = 'g3' + if args.mips1 and (opt != 'O2' or args.framepointer): + raise Failure("-mips1 is only supported together with -O2") if args.objfile is None: with open(args.filename, encoding=args.input_enc) as f: deps = [] - functions = parse_source(f, opt=opt, framepointer=args.framepointer, input_enc=args.input_enc, output_enc=args.output_enc, out_dependencies=deps, print_source=outfile) + functions = parse_source(f, opt=opt, framepointer=args.framepointer, mips1=args.mips1, input_enc=args.input_enc, output_enc=args.output_enc, out_dependencies=deps, print_source=outfile) return functions, deps else: if args.assembler is None: raise Failure("must pass assembler command") if functions is None: with open(args.filename, encoding=args.input_enc) as f: - functions = parse_source(f, opt=opt, framepointer=args.framepointer, input_enc=args.input_enc, out_dependencies=[], output_enc=args.output_enc) + functions = parse_source(f, opt=opt, framepointer=args.framepointer, mips1=args.mips1, input_enc=args.input_enc, out_dependencies=[], output_enc=args.output_enc) if not functions: return asm_prelude = b'' if args.asm_prelude: with open(args.asm_prelude, 'rb') as f: asm_prelude = f.read() - fixup_objfile(args.objfile, functions, asm_prelude, args.assembler, args.output_enc) + fixup_objfile(args.objfile, functions, asm_prelude, args.assembler, args.output_enc, args.drop_mdebug_gptab) def run(argv, outfile=sys.stdout.buffer, functions=None): try: