blob: 7d745eaef9c88b51fc097f18749a75383fabf0d1 [file] [log] [blame] [edit]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2025 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import json
import os
import re
import shlex
import subprocess
import sys
from pathlib import Path
from typing import NoReturn
def eprint(*args, **kwargs) -> None:
print(*args, file=sys.stderr, **kwargs)
def fail(*args, **kwargs) -> NoReturn:
eprint("\033[31mError:\033[0m ", end="")
eprint(*args, **kwargs)
sys.exit(1)
def warn(*args, **kwargs) -> None:
eprint("\033[33mWarning:\033[0m ", end="")
eprint(*args, **kwargs)
def getenv(key: str) -> str:
value = os.getenv(key)
if value:
return value
fail(
f"{key} environment variable not set. "
"Are you running this command from an `fx` environment?",
)
def main() -> None:
parser = argparse.ArgumentParser(
description="Dump disassembly for a binary to a file in the build directory.",
epilog="""Each BINARY can be the name of the binary file without a directory,
e.g. "libzircon.so" or "zircon.elf"; or the name of a GN target, e.g. "zircon";
or a string of (lowercase) hex characters that's an ELF build ID.
The disassembly will be written to a file next to the binary file in
the build directory, using its name with a ".lst" suffix.""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--line-numbers",
"-l",
help="Display source line numbers (the default)",
action="store_true",
default=True,
)
parser.add_argument(
"--no-line-numbers",
help="Do not display source line numbers",
dest="line_numbers",
action="store_false",
)
parser.add_argument(
"--source",
"-S",
help="Display source interleaved with the disassembly",
action="store_true",
)
parser.add_argument(
"--no-source",
help="Do not display source interleaved with the disassembly (the default)",
dest="source",
action="store_false",
)
parser.add_argument(
"--reloc",
"-r",
help="Display the relocation entries",
action="store_true",
)
parser.add_argument(
"--no-reloc",
help="Do not display the relocation entries",
dest="reloc",
action="store_false",
)
parser.add_argument(
"--demangle",
help="Demangle symbol names (the default)",
action="store_true",
default=True,
)
parser.add_argument(
"--no-demangle",
"-n",
help="Do not demangle symbol names",
dest="demangle",
action="store_false",
)
parser.add_argument(
"-G",
"--gnu",
action="store_true",
help="Use GNU objdump rather than llvm-objdump.",
)
parser.add_argument(
"-P",
"--objdump-path",
help="Use objdump binary at the specified path.",
)
parser.add_argument(
"-e",
"--edit",
help="Automatically open the output disassembly file, provided $EDITOR is set",
action="store_true",
)
parser.add_argument(
"binary",
help="Name of the binary, GN target, or ELF build ID.",
)
args = parser.parse_args()
if args.objdump_path:
objdump_path = Path(args.objdump_path)
elif args.gnu:
prebuilt_binutils_dir = getenv("PREBUILT_BINUTILS_DIR")
objdump_path = Path(prebuilt_binutils_dir) / "bin" / "objdump"
else:
prebuilt_clang_dir = getenv("PREBUILT_CLANG_DIR")
objdump_path = Path(prebuilt_clang_dir) / "bin" / "llvm-objdump"
if not objdump_path.exists():
fail(f"no objdump not found at {objdump_path}")
objdump_args = [objdump_path, "--disassemble"]
if args.line_numbers:
objdump_args.append("--line-numbers")
if args.source:
objdump_args.append("--source")
if args.reloc:
objdump_args.append("--reloc")
if args.demangle:
objdump_args.append("--demangle")
build_dir = Path(getenv("FUCHSIA_BUILD_DIR"))
(bin_path, bin_label) = find_binary(build_dir, args.binary)
full_bin_path = build_dir / bin_path
output_path = full_bin_path.with_suffix(".lst")
print(f"Disassembling {bin_label}...")
try:
with output_path.open("w") as outfile:
subprocess.run(
[*objdump_args, bin_path],
cwd=build_dir,
stdout=outfile,
stderr=subprocess.PIPE,
text=True,
check=True,
)
except FileNotFoundError:
fail(f"Binary file not found at '{full_bin_path}'")
except subprocess.CalledProcessError as e:
fail(f"objdump failed for '{bin_path}': {e.stderr}")
rel_output_path = output_path.relative_to(Path.cwd())
print(f"Wrote '{rel_output_path}' for '{bin_label}'")
if args.edit:
# $EDITOR may contain other flags like `--wait` supplied in service of
# `git commit` in one's terminal - but the base editor executable
# followed by a file should always simply open the file for common
# editors.
editor = getenv("EDITOR").split()[0]
subprocess.run(
f"{editor} {shlex.quote(str(rel_output_path))}",
stderr=subprocess.PIPE,
check=True,
shell=True,
text=True,
)
def find_binary(
build_dir: Path,
bin_spec: str,
) -> tuple[str, str]:
"""
Finds the binary path and label in the build directory based on a binary spec.
"""
binaries_json = build_dir / "binaries.json"
if not binaries_json.exists():
fail(
f"'{binaries_json}' not found. Please run `fx build` first.",
)
with binaries_json.open("r") as f:
binaries = json.load(f)
hex_re = re.compile(r"[0-9a-f]+")
if re.fullmatch(hex_re, bin_spec):
for binary in binaries:
if "elf_build_id" not in binary:
continue
build_id_path = build_dir / binary["elf_build_id"]
try:
with build_id_path.open("r") as f:
build_id = f.read().strip()
except FileNotFoundError:
# It's possible the build ID file doesn't exist; skip it.
continue
if build_id == bin_spec:
return (binary["dist"], binary["label"])
fail(f"No binary with build ID '{bin_spec}' found")
debug_re = re.compile(f"(^|/){bin_spec}(.debug)?$")
label_re = re.compile(rf":{bin_spec}\(")
matches = []
for binary in binaries:
if "debug" not in binary:
continue
if re.search(debug_re, binary["debug"]) or (
"label" in binary and re.search(label_re, binary["label"])
):
matches.append((binary["dist"], binary["label"]))
if len(matches) == 0:
fail(f"'{bin_spec}' did not match any binaries")
if len(matches) > 1:
warn(
f"'{bin_spec}' matched more than one binary (going with the first): "
f"{json.dumps(matches, indent=2)}",
)
return matches[0]
if __name__ == "__main__":
main()