#!/usr/bin/env python3 """Compile an ARM assembly file and run it with qemu-arm-static.""" from __future__ import annotations import argparse import shutil import subprocess import sys import tempfile from pathlib import Path def parse_args() -> argparse.Namespace: if "--" in sys.argv: separator_index = sys.argv.index("--") tool_args = sys.argv[1:separator_index] program_args = sys.argv[separator_index + 1 :] else: tool_args = sys.argv[1:] program_args = [] parser = argparse.ArgumentParser( description=( "Compile an ARM assembly file with arm-linux-gnueabihf-gcc, " "run it with qemu-arm-static, then print stdout and $?." ) ) parser.add_argument("asm_file", type=Path, help="path to the assembly file") parser.add_argument( "--cc", default="arm-linux-gnueabihf-gcc", help="cross compiler to use (default: arm-linux-gnueabihf-gcc)", ) parser.add_argument( "--qemu", default="qemu-arm-static", help="ARM qemu runner to use (default: qemu-arm-static)", ) parser.add_argument( "-o", "--output", type=Path, help="output executable path; defaults to an auto-cleaned temporary file", ) parser.add_argument( "-g", "--gdb", action="store_true", help="compile with -g and run qemu in gdb stub mode", ) parser.add_argument( "--gdb-port", default="1234", metavar="PORT", help="qemu gdb stub port used with -g/--gdb (default: 1234)", ) parser.add_argument( "--gdb-command", help=( "gdb command shown in debug instructions " "(default: first available of arm-linux-gnueabihf-gdb, gdb-multiarch, gdb)" ), ) parser.add_argument( "--no-static", action="store_true", help="do not pass -static to the compiler", ) parser.add_argument( "--gcc-arg", action="append", default=[], help="extra argument passed to gcc; repeat for multiple args", ) parser.add_argument( "--std-c", type=Path, default=Path("tests/std.c"), help="C runtime source compiled with the assembly file (default: tests/std.c)", ) parser.add_argument( "--no-std-c", action="store_true", help="do not compile tests/std.c with the assembly file", ) args = parser.parse_args(tool_args) args.program_args = program_args return args def require_command(command: str) -> None: if shutil.which(command) is None: print(f"error: command not found: {command}", file=sys.stderr) sys.exit(127) def run_command(command: list[str]) -> subprocess.CompletedProcess[bytes]: try: return subprocess.run(command, capture_output=True, check=False) except OSError as err: print(f"error: failed to run {command[0]}: {err}", file=sys.stderr) sys.exit(127) def choose_gdb_command(requested_command: str | None) -> str: if requested_command: return requested_command for command in ("arm-linux-gnueabihf-gdb", "gdb-multiarch", "gdb"): if shutil.which(command) is not None: return command return "gdb-multiarch" def main() -> int: args = parse_args() asm_file = args.asm_file if not asm_file.is_file(): print(f"error: assembly file not found: {asm_file}", file=sys.stderr) return 2 std_c = args.std_c if not args.no_std_c and not std_c.is_file(): print(f"error: std C file not found: {std_c}", file=sys.stderr) return 2 require_command(args.cc) require_command(args.qemu) with tempfile.TemporaryDirectory(prefix="run-arm-asm-") as temp_dir: output = args.output or Path(temp_dir) / asm_file.with_suffix("").name compile_cmd = [args.cc] if not args.no_static: compile_cmd.append("-static") if args.gdb: compile_cmd.append("-g") compile_cmd.extend(args.gcc_arg) compile_cmd.append(str(asm_file)) if not args.no_std_c: compile_cmd.append(str(std_c)) compile_cmd.extend(["-o", str(output)]) compile_result = run_command(compile_cmd) sys.stdout.buffer.write(compile_result.stdout) sys.stderr.buffer.write(compile_result.stderr) if compile_result.returncode != 0: return compile_result.returncode qemu_cmd = [args.qemu] if args.gdb: qemu_cmd.extend(["-g", args.gdb_port]) resolved_output = output.resolve() gdb_command = choose_gdb_command(args.gdb_command) print(f"executable: {resolved_output}", file=sys.stderr, flush=True) print( f"gdb: {gdb_command} {resolved_output} " f"-ex 'target remote :{args.gdb_port}'", file=sys.stderr, flush=True, ) print("gdb: use 'si' or 'c'; do not use 'run'", file=sys.stderr, flush=True) qemu_cmd.extend([str(output), *args.program_args]) run_result = run_command(qemu_cmd) sys.stdout.buffer.write(run_result.stdout) sys.stderr.buffer.write(run_result.stderr) if run_result.stdout and not run_result.stdout.endswith(b"\n"): print() print(f"$? = {run_result.returncode}") return 0 if __name__ == "__main__": raise SystemExit(main())