From 6a14843e9748a1159cd991d1d65f948028b7a92c Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Sat, 30 Jul 2022 11:40:19 -0700 Subject: [PATCH 01/10] boot-utils: Rewrite core scripts in Python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Over the years, this script has gotten quite complex with the logic of the QEMU options and user input. To make it easier to extend this in the future, rewrite it in Python, which allows us to use much higher built-in functions to parse input and make decisions based on that. The performance difference between the shell and Python implementation is not much. It is most noticeable when parsing the Linux kernel version from an aarch64 kernel but the overall performance is still reasonable. On an aarch64 host with a SoC with 16 Cortex-A72 cores: arm32_v7: Benchmark 1: boot-qemu.sh Time (mean ± σ): 14.265 s ± 0.021 s [User: 15.572 s, System: 1.062 s] Range (min … max): 14.246 s … 14.305 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 14.319 s ± 0.025 s [User: 15.811 s, System: 0.925 s] Range (min … max): 14.282 s … 14.361 s 10 runs Summary 'boot-qemu.sh' ran 1.00 ± 0.00 times faster than 'boot-qemu.py' arm64: Benchmark 1: boot-qemu.sh Time (mean ± σ): 3.949 s ± 0.024 s [User: 1.145 s, System: 1.165 s] Range (min … max): 3.916 s … 3.992 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 4.009 s ± 0.032 s [User: 1.426 s, System: 1.149 s] Range (min … max): 3.967 s … 4.049 s 10 runs Summary 'boot-qemu.sh' ran 1.02 ± 0.01 times faster than 'boot-qemu.py' arm64be: Benchmark 1: boot-qemu.sh Time (mean ± σ): 3.942 s ± 0.035 s [User: 1.332 s, System: 1.114 s] Range (min … max): 3.910 s … 4.016 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 3.985 s ± 0.028 s [User: 1.196 s, System: 1.143 s] Range (min … max): 3.954 s … 4.034 s 10 runs Summary 'boot-qemu.sh' ran 1.01 ± 0.01 times faster than 'boot-qemu.py' x86: Benchmark 1: boot-qemu.sh Time (mean ± σ): 14.993 s ± 0.059 s [User: 12.278 s, System: 1.094 s] Range (min … max): 14.926 s … 15.095 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 14.985 s ± 0.045 s [User: 12.455 s, System: 0.898 s] Range (min … max): 14.907 s … 15.039 s 10 runs Summary 'boot-qemu.py' ran 1.00 ± 0.00 times faster than 'boot-qemu.sh' x86_64: Benchmark 1: boot-qemu.sh Time (mean ± σ): 16.663 s ± 0.049 s [User: 14.539 s, System: 0.544 s] Range (min … max): 16.601 s … 16.732 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 16.692 s ± 0.059 s [User: 14.044 s, System: 1.057 s] Range (min … max): 16.607 s … 16.819 s 10 runs Summary 'boot-qemu.sh' ran 1.00 ± 0.00 times faster than 'boot-qemu.py' On an x86_64 host with an AMD Threadripper 3990X: arm32_v7: Benchmark 1: boot-qemu.sh Time (mean ± σ): 4.773 s ± 0.014 s [User: 2.716 s, System: 0.065 s] Range (min … max): 4.750 s … 4.796 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 4.807 s ± 0.021 s [User: 2.749 s, System: 0.065 s] Range (min … max): 4.777 s … 4.841 s 10 runs Summary 'boot-qemu.sh' ran 1.01 ± 0.01 times faster than 'boot-qemu.py' arm64: Benchmark 1: boot-qemu.sh Time (mean ± σ): 5.089 s ± 0.022 s [User: 3.196 s, System: 0.105 s] Range (min … max): 5.057 s … 5.122 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 5.311 s ± 0.016 s [User: 3.263 s, System: 0.141 s] Range (min … max): 5.297 s … 5.340 s 10 runs Summary 'boot-qemu.sh' ran 1.04 ± 0.01 times faster than 'boot-qemu.py' arm64be: Benchmark 1: boot-qemu.sh Time (mean ± σ): 5.011 s ± 0.021 s [User: 3.124 s, System: 0.093 s] Range (min … max): 4.986 s … 5.049 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 5.249 s ± 0.017 s [User: 3.181 s, System: 0.155 s] Range (min … max): 5.223 s … 5.273 s 10 runs Summary 'boot-qemu.sh' ran 1.05 ± 0.01 times faster than 'boot-qemu.py' x86: Benchmark 1: boot-qemu.sh Time (mean ± σ): 2.869 s ± 0.022 s [User: 0.532 s, System: 0.236 s] Range (min … max): 2.850 s … 2.906 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 2.910 s ± 0.029 s [User: 0.551 s, System: 0.236 s] Range (min … max): 2.856 s … 2.950 s 10 runs Summary 'boot-qemu.sh' ran 1.01 ± 0.01 times faster than 'boot-qemu.py' x86_64: Benchmark 1: boot-qemu.sh Time (mean ± σ): 3.242 s ± 0.043 s [User: 1.390 s, System: 0.636 s] Range (min … max): 3.134 s … 3.277 s 10 runs Benchmark 2: boot-qemu.py Time (mean ± σ): 3.251 s ± 0.052 s [User: 1.434 s, System: 0.602 s] Range (min … max): 3.130 s … 3.317 s 10 runs Summary 'boot-qemu.sh' ran 1.00 ± 0.02 times faster than 'boot-qemu.py' This is designed to be a one-to-one rewrite, with the exception of dropping support for '--debian' for simplicity's sake. It was always a little janky at times and using other options such as libvirt is more reliable. I may eventually contribute my other QEMU Python script to this repository, which aims to drive full distributions. Signed-off-by: Nathan Chancellor --- .gitignore | 1 + boot-qemu.py | 730 +++++++++++++++++++++++++++++++++++++++++++++++++++ boot-qemu.sh | 468 --------------------------------- boot-uml.py | 82 ++++++ boot-uml.sh | 39 --- utils.py | 86 ++++++ utils.sh | 42 --- 7 files changed, 899 insertions(+), 549 deletions(-) create mode 100755 boot-qemu.py delete mode 100755 boot-qemu.sh create mode 100755 boot-uml.py delete mode 100755 boot-uml.sh create mode 100755 utils.py delete mode 100755 utils.sh diff --git a/.gitignore b/.gitignore index 3d319b2..b8bf959 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ qemu-binaries/ +*.pyc diff --git a/boot-qemu.py b/boot-qemu.py new file mode 100755 index 0000000..36c7886 --- /dev/null +++ b/boot-qemu.py @@ -0,0 +1,730 @@ +#!/usr/bin/env python3 + +import argparse +import os +from pathlib import Path +import platform +import shutil +import subprocess + +import utils + +base_folder = Path(__file__).resolve().parent +supported_architectures = [ + "arm", "arm32_v5", "arm32_v6", "arm32_v7", "arm64", "arm64be", "m68k", + "mips", "mipsel", "ppc32", "ppc32_mac", "ppc64", "ppc64le", "riscv", + "s390", "x86", "x86_64" +] + + +def parse_arguments(): + """ + Parses arguments to script. + + Returns: + A Namespace object containing key values from parser.parse_args() + """ + parser = argparse.ArgumentParser() + + parser.add_argument( + "-a", + "--architecture", + metavar="ARCH", + required=True, + type=str, + choices=supported_architectures, + help="The architecture to boot. Possible values are: %(choices)s") + parser.add_argument( + "--append", + default="", + type=str, + help="A string of values to pass to the kernel command line.") + parser.add_argument( + "-g", + "--gdb", + action="store_true", + help="Start QEMU with '-s -S' then launch GDB on 'vmlinux'.") + parser.add_argument("--gdb-bin", + type=str, + default="gdb-multiarch", + help="GDB binary to use for debugging.") + parser.add_argument( + "-i", + "--interactive", + "--shell", + action="store_true", + help= + "Instead of immediately shutting down the machine upon successful boot, pass 'rdinit=/bin/sh' on the kernel command line to allow interacting with the machine via a shell." + ) + parser.add_argument( + "-k", + "--kernel-location", + required=True, + type=str, + help= + "Path to kernel image or kernel build folder to search for image in. Can be an absolute or relative path." + ) + parser.add_argument( + "--no-kvm", + action="store_true", + help= + "Do not use KVM for acceleration even when supported (only recommended for debugging)." + ) + parser.add_argument( + "-s", + "--smp", + type=int, + help= + "Number of processors for virtual machine. By default, only machines spawned with KVM will use multiple vCPUS." + ) + parser.add_argument( + "-t", + "--timeout", + type=str, + default="3m", + help="Value to pass along to 'timeout' (default: '3m')") + + return parser.parse_args() + + +def can_use_kvm(args): + """ + Checks that KVM can be used for faster VMs based on: + * User's request + * Whether or not '--no-kvm' was used + * '/dev/kvm' is available + * The guest architecture + * Only 'arm'/'arm32_v7', 'arm64', 'arm64be', 'x86', and 'x86_64' + are supported with KVM + * Availability of hardware virtualization support + * aarch64 may not support accelerated 32-bit guests + * i386 and x86_64 need the virtualization extensions in + '/proc/cpuinfo' + + Parameters: + args (Namespace): The Namespace object returned from parse_arguments() + + Returns: + True if KVM can be used based on the above parameters, False if not. + """ + # We can only test for KVM if the user did not opt out of it + can_test_for_kvm = not args.no_kvm + if can_test_for_kvm: + # /dev/kvm must exist to use KVM with QEMU + if Path("/dev/kvm").exists(): + guest_arch = args.architecture + host_arch = platform.machine() + + if host_arch == "aarch64": + # If /dev/kvm exists on aarch64, KVM is supported for aarch64 guests + if "arm64" in guest_arch: + return True + # 32-bit EL1 is not always supported, test for it first + if guest_arch == "arm" or guest_arch == "arm32_v7": + check_32_bit_el1_exec = base_folder.joinpath( + "utils", "aarch64_32_bit_el1_supported") + check_32_bit_el1 = subprocess.run( + [check_32_bit_el1_exec.as_posix()]) + return check_32_bit_el1.returncode == 0 + + if host_arch == "x86_64" and "x86" in guest_arch: + # Check /proc/cpuinfo for whether or not the machine supports hardware virtualization + with open("/proc/cpuinfo") as f: + cpuinfo = f.read() + # SVM is AMD, VMX is Intel + return cpuinfo.count("svm") > 0 or cpuinfo.count("vmx") > 0 + + # We could not prove that we could use KVM safely so don't try + return False + + +def get_smp_value(args): + """ + Get the value of '-smp' based on user input and kernel configuration. + 1. If '--smp' is supplied by the user, it is used unconditionally. + 2. If '--smp' is not supplied by the user, attempt to locate the + .config file (see comment below for logic). + 3. If the .config can be found, the upper bound of '-smp' is + CONFIG_NR_CPUS. + 4. If the .config cannot be found, the upper bound of '-smp' is 8. + 5. Get the number of usable cores in the system. + 6. Return the smaller number between the limit from steps 3/4 and the + number of cores in the system from step 5. + + Parameters: + args (Namespace): The Namespace object returned from parse_arguments() + + Returns: + The smaller number between the number of usable cores in the system and + CONFIG_NR_CPUS. + """ + # If the user specified a value, use it + if args.smp: + return args.smp + + # kernel_location is either a path to the kernel source or a full kernel + # location. If it is a file, we need to strip off the basename so that we + # can easily navigate around with '..'. + kernel_dir = Path(args.kernel_location) + if kernel_dir.is_file(): + kernel_dir = kernel_dir.parent + + # If kernel_location is the kernel source, the configuration will be at + # /.config + # + # If kernel_location is a full kernel location, it could either be: + # * /.config (if the image is vmlinux) + # * /../../../.config (if the image is in arch/*/boot/) + # * /config (if the image is in a TuxMake folder) + config_file = None + for config_name in [".config", "../../../.config", "config"]: + config_path = kernel_dir.joinpath(config_name) + if config_path.is_file(): + config_file = config_path.as_posix() + break + + # Choose a sensible default value based on treewide defaults for + # CONFIG_NR_CPUS then get the actual value if possible. + config_nr_cpus = 8 + if config_file: + with open(config_file) as f: + for line in f: + if "CONFIG_NR_CPUS=" in line: + config_nr_cpus = int(line.split("=", 1)[1]) + break + + # Use the minimum of the number of usable processors for the script or + # CONFIG_NR_CPUS. + usable_cpus = len(os.sched_getaffinity(0)) + return min(usable_cpus, config_nr_cpus) + + +def setup_cfg(args): + """ + Sets up the global configuration based on user input. + + Meaning of each key: + + * append: The additional values to pass to the kernel command line. + * architecture: The guest architecture from the list of supported + architectures. + * gdb: Whether or not the user wants to debug the kernel using GDB. + * gdb_bin: The name of or path to the GDB executable that the user + wants to debug with. + * interactive: Whether or not the user is going to be running the + machine interactively. + * kernel_location: The full path to the kernel image or build folder. + * smp_requested: Whether or not the user specified a value with + '--smp'. + * smp_value: The value to use with '-smp' (will be used when + smp_requested is True or using KVM). + * timeout: The value to pass along to 'timeout' if not running + interactively. + * use_kvm: Whether or not KVM will be used. + + Parameters: + args (Namespace): The Namespace object returned from parse_arguments() + + Returns: + A dictionary of configuration values + """ + return { + # Required + "architecture": args.architecture, + "kernel_location": Path(args.kernel_location).resolve().as_posix(), + + # Optional + "append": args.append, + "gdb": args.gdb, + "gdb_bin": args.gdb_bin, + "interactive": args.interactive or args.gdb, + "smp_requested": args.smp is not None, + "smp_value": get_smp_value(args), + "timeout": args.timeout, + "use_kvm": can_use_kvm(args), + } + + +def print_version_code(version): + """ + Prints a version list with three values (major, minor, and patch level) as + an integer with at least six digits: + * major: as is + * minor: with a minimum length of two ("1" becomes "01") + * patch level: with a minimum length of three ("1" becomes "001") + + Parameters: + version (list): A list with three integer values (major, minor, and + patch level). + + Returns: + An integer with at least six digits. + """ + major, minor, patch = [int(version[i]) for i in (0, 1, 2)] + return int("{:d}{:02d}{:03d}".format(major, minor, patch)) + + +def get_qemu_ver_string(qemu): + """ + Prints the first line of QEMU's version output. + + Parameters: + qemu (str): The QEMU executable name or path to get the version of. + + Returns: + The first line of the QEMU version output. + """ + utils.check_cmd(qemu) + qemu_version_call = subprocess.run([qemu, "--version"], + capture_output=True, + check=True) + # Equivalent of 'head -1' + return qemu_version_call.stdout.decode("UTF-8").split("\n")[0] + + +def get_qemu_ver_code(qemu): + """ + Prints QEMU's version as an integer with at least six digits. + + Errors if the requested QEMU could not be found. + + Parameters: + qemu (str): The QEMU executable name or path to get the version of. + + Returns: + The QEMU version as an integer with at least six digits. + """ + qemu_version_string = get_qemu_ver_string(qemu) + # "QEMU emulator version x.y.z (...)" -> x.y.z -> ['x', 'y', 'z'] + qemu_version = qemu_version_string.split(" ")[3].split(".") + + return print_version_code(qemu_version) + + +def get_linux_ver_code(decomp_cmd): + """ + Searches the Linux kernel binary for the version string using 'strings' + then prints it as an integer with at least six digits. + + Errors if the decompression executable could not be found. + + Parameters: + decomp_cmd (list): A list with the decompression command plus arguments + to decompress the kernel to stdout. + + Returns: + The Linux kernel version as an integer with at least six digits. + """ + decomp_exec = decomp_cmd[0] + utils.check_cmd(decomp_exec) + decomp = subprocess.run(decomp_cmd, capture_output=True, check=True) + + utils.check_cmd("strings") + strings = subprocess.run(["strings"], + capture_output=True, + check=True, + input=decomp.stdout) + + linux_version = None + for line in strings.stdout.decode("UTF-8").split("\n"): + if "Linux version" in line: + # "Linux version x.y.z- ..." -> "x.y.z-" -> "x.y.z" -> ['x', 'y', 'z'] + linux_version = line.split(" ")[2].split("-")[0].split(".") + break + if not linux_version: + kernel_path = decomp_cmd[-1] + utils.die("Linux version string could not be found in '{}'".format( + kernel_path)) + + return print_version_code(linux_version) + + +def get_and_decomp_rootfs(cfg): + """ + Decompress and get the full path of the initial ramdisk for use with QEMU's + '-initrd' parameter. Handles the special cases of the arm32_* and ppc32* + values sharing the same initial ramdisk. + + Parameters: + cfg (dict): The configuration dictionary generated with setup_cfg(). + + Returns: + rootfs (str): The path to the decompressed rootfs file. + """ + + arch = cfg["architecture"] + if "arm32" in arch: + arch_rootfs_dir = "arm" + elif "ppc32" in arch: + arch_rootfs_dir = "ppc32" + else: + arch_rootfs_dir = arch + rootfs = base_folder.joinpath("images", arch_rootfs_dir, "rootfs.cpio") + + # This could be 'rootfs.unlink(missing_ok=True)' but that was only added in + # Python 3.8. + if rootfs.exists(): + rootfs.unlink() + + # Print as string for remainder due to use in subprocess command lists + rootfs = rootfs.as_posix() + + utils.check_cmd("zstd") + subprocess.run(["zstd", "-q", "-d", "{}.zst".format(rootfs), "-o", rootfs], + check=True) + + return rootfs + + +def get_qemu_args(cfg): + """ + Generate the QEMU command from the QEMU executable and parameters, based on + a variety of factors: + * User's input + * Whether or not KVM is being used + * A different executable and options might be needed + * QEMU and Linux kernel version + * Locations of firmwares and device tree blobs + + Parameters: + cfg (dict): The configuration dictionary generated with setup_cfg(). + + Returns: + cfg (dict): The configuration dictionary updated with the QEMU command. + """ + # Static values from cfg + arch = cfg["architecture"] + kernel_location = cfg["kernel_location"] + gdb = cfg["gdb"] + interactive = cfg["interactive"] + smp_requested = cfg["smp_requested"] + smp_value = cfg["smp_value"] + use_kvm = cfg["use_kvm"] + + # Default values, may be overwritten or modified below + append = cfg["append"] + dtb = None + kernel = None + kernel_arch = arch + kernel_image = "zImage" + kvm_cpu = "host" + ram = "512m" + qemu_args = [] + + if arch == "arm32_v5": + append += " earlycon" + dtb = "aspeed-bmc-opp-palmetto.dtb" + kernel_arch = "arm" + qemu_args += ["-machine", "palmetto-bmc"] + qemu = "qemu-system-arm" + + elif arch == "arm32_v6": + dtb = "aspeed-bmc-opp-romulus.dtb" + kernel_arch = "arm" + qemu = "qemu-system-arm" + qemu_args += ["-machine", "romulus-bmc"] + + elif arch == "arm" or arch == "arm32_v7": + append += " console=ttyAMA0 earlycon" + kernel_arch = "arm" + qemu_args += ["-machine", "virt"] + if use_kvm: + kvm_cpu += ",aarch64=off" + qemu = "qemu-system-aarch64" + else: + qemu = "qemu-system-arm" + + elif arch == "arm64" or arch == "arm64be": + append += " console=ttyAMA0 earlycon" + kernel_arch = "arm64" + kernel_image = "Image.gz" + qemu = "qemu-system-aarch64" + qemu_args += ["-machine", "virt,gic-version=max"] + + if not use_kvm: + cpu = "max" + kernel = utils.get_full_kernel_path(kernel_location, kernel_image, + kernel_arch) + qemu_ver_code = get_qemu_ver_code(qemu) + + if qemu_ver_code >= 602050: + gzip_kernel_cmd = ["gzip", "-c", "-d", kernel.as_posix()] + linux_ver_code = get_linux_ver_code(gzip_kernel_cmd) + + # https://gitlab.com/qemu-project/qemu/-/issues/964 + if linux_ver_code < 416000: + cpu = "cortex-a72" + # https://gitlab.com/qemu-project/qemu/-/commit/69b2265d5fe8e0f401d75e175e0a243a7d505e53 + elif linux_ver_code < 512000: + cpu += ",lpa2=off" + + # https://lore.kernel.org/YlgVa+AP0g4IYvzN@lakrids/ + if "max" in cpu and qemu_ver_code >= 600000: + cpu += ",pauth-impdef=true" + + qemu_args += ["-cpu", cpu] + qemu_args += ["-machine", "virtualization=true"] + + elif arch == "m68k": + append += " console=ttyS0,115200" + kernel_image = "vmlinux" + qemu = "qemu-system-m68k" + qemu_args += ["-cpu", "m68040"] + qemu_args += ["-M", "q800"] + + elif arch == "mips" or arch == "mipsel": + kernel_arch = "mips" + kernel_image = "vmlinux" + qemu = "qemu-system-{}".format(arch) + qemu_args += ["-cpu", "24Kf"] + qemu_args += ["-machine", "malta"] + + elif "ppc32" in arch: + if arch == "ppc32": + kernel_image = "uImage" + qemu_args += ["-machine", "bamboo"] + elif arch == "ppc32_mac": + kernel_image = "vmlinux" + qemu_args += ["-machine", "mac99"] + + append += " console=ttyS0" + kernel_arch = "powerpc" + qemu = "qemu-system-ppc" + ram = "128m" + + elif arch == "ppc64": + kernel_arch = "powerpc" + kernel_image = "vmlinux" + qemu = "qemu-system-ppc64" + qemu_args += ["-cpu", "power8"] + qemu_args += ["-machine", "pseries"] + qemu_args += ["-vga", "none"] + ram = "1G" + + elif arch == "ppc64le": + kernel_arch = "powerpc" + kernel_image = "zImage.epapr" + qemu = "qemu-system-ppc64" + qemu_args += ["-device", "ipmi-bmc-sim,id=bmc0"] + qemu_args += ["-device", "isa-ipmi-bt,bmc=bmc0,irq=10"] + qemu_args += ["-L", base_folder.joinpath("images", arch).as_posix()] + qemu_args += ["-bios", "skiboot.lid"] + qemu_args += ["-machine", "powernv8"] + ram = "2G" + + elif arch == "riscv": + append += " earlycon" + kernel_image = "Image" + + bios = "default" + deb_bios = Path( + "/usr/lib/riscv64-linux-gnu/opensbi/qemu/virt/fw_jump.elf") + if "BIOS" in os.environ: + bios = os.environ["BIOS"] + elif deb_bios.exists(): + bios = deb_bios.as_posix() + + qemu = "qemu-system-riscv64" + qemu_args += ["-bios", bios] + qemu_args += ["-M", "virt"] + + elif arch == "s390": + kernel_image = "bzImage" + qemu = "qemu-system-s390x" + qemu_args += ["-M", "s390-ccw-virtio"] + + elif "x86" in arch: + append += " console=ttyS0 earlycon=uart8250,io,0x3f8" + kernel_image = "bzImage" + + if use_kvm: + qemu_args += ["-d", "unimp,guest_errors"] + elif arch == "x86_64": + qemu_args += ["-cpu", "Nehalem"] + + if arch == "x86": + qemu = "qemu-system-i386" + else: + qemu = "qemu-system-x86_64" + + # Make sure QEMU is available in PATH, otherwise there is little point to + # continuing. + utils.check_cmd(qemu) + + # '-kernel' + if not kernel: + kernel = utils.get_full_kernel_path(kernel_location, kernel_image, + kernel_arch) + qemu_args += ["-kernel", kernel.as_posix()] + + # '-dtb' + if dtb: + # If we are in a boot folder, look for them in the dts folder in it + if "boot" in kernel.as_posix(): + dtb_dir = "dts" + # Otherwise, assume there is a dtbs folder in the same folder as the + # kernel image (tuxmake) + else: + dtb_dir = "dtbs" + + dtb = kernel.parent.joinpath(dtb_dir, dtb) + if not dtb.exists(): + utils.die( + "'{}' is required for booting but it could not be found at '{}'" + .format(dtb.stem.as_posix(), dtb.as_posix())) + + qemu_args += ["-dtb", dtb.as_posix()] + + # '-append' + if gdb: + append += " nokaslr" + if interactive: + append += " rdinit=/bin/sh" + if len(append) > 0: + qemu_args += ["-append", append.strip()] + + # KVM and '-smp' + if use_kvm: + qemu_args += ["-cpu", kvm_cpu] + qemu_args += ["-enable-kvm"] + qemu_args += ["-smp", str(smp_value)] + else: + # By default, we do not use '-smp' with TCG for performance reasons. + # Only add it if the user explicitly requested it. + if smp_requested: + qemu_args += ["-smp", str(smp_value)] + + # Other miscellaneous options + qemu_args += ["-display", "none"] + qemu_args += ["-initrd", get_and_decomp_rootfs(cfg)] + qemu_args += ["-m", ram] + qemu_args += ["-nodefaults"] + qemu_args += ["-no-reboot"] + + # Resolve the full path to QEMU for the command, as recommended for use + # with subprocess.Popen() + qemu = shutil.which(qemu) + + cfg["qemu_cmd"] = [qemu] + qemu_args + + return cfg + + +def pretty_print_qemu_info(qemu): + """ + Prints where QEMU is being used from and its version. Useful for making + sure a specific version of QEMU is being used. + + Parameters: + qemu (str): A string containing the full path to the QEMU executable. + """ + qemu_dir = Path(qemu).parent.as_posix() + qemu_version_string = get_qemu_ver_string(qemu) + + utils.green("QEMU location: \033[0m{}".format(qemu_dir)) + utils.green("QEMU version: \033[0m{}\n".format(qemu_version_string)) + + +def pretty_print_qemu_cmd(qemu_cmd): + """ + Prints the QEMU command in a "pretty" manner, similar to how 'set -x' works in bash. + * Surrounds list elements that have spaces with quotation marks so that + copying and pasting the command in a shell works. + * Prints the QEMU executable as just the executable name, rather than + the full path. This is done purely for aesthetic reasons, as the + executable would normally be called with just its name through PATH + but subprocess.Popen() recommends using a full path for maximum + compatibility so it was generated in get_qemu_args(). + + Parameters: + qemu_cmd (list): QEMU command list. + """ + qemu_cmd_pretty = "" + for element in qemu_cmd: + if " " in element: + qemu_cmd_pretty += ' "{}"'.format(element) + elif "qemu-system-" in element: + qemu_cmd_pretty += " {}".format(element.split("/")[-1]) + else: + qemu_cmd_pretty += " {}".format(element) + print("$ {}".format(qemu_cmd_pretty.strip())) + + +def launch_qemu(cfg): + """ + Runs the QEMU command generated from get_qemu_args(), depending on whether + or not the user wants to debug with GDB. + + If debugging with GDB, QEMU is called with '-s -S' in the background then + gdb_bin is called against 'vmlinux' connected to the target remote. This + can be repeated multiple times. + + Otherwise, QEMU is called with 'timeout' so that it is terminated if there + is a problem while booting, passing along any error code that is returned. + + Parameters: + cfg (dict): The configuration dictionary generated with setup_cfg(). + """ + interactive = cfg["interactive"] + gdb = cfg["gdb"] + gdb_bin = cfg["gdb_bin"] + kernel_location = cfg["kernel_location"] + qemu_cmd = cfg["qemu_cmd"] + timeout = cfg["timeout"] + + # Print information about the QEMU binary + pretty_print_qemu_info(qemu_cmd[0]) + + if gdb: + while True: + utils.check_cmd("lsof") + lsof = subprocess.run(["lsof", "-i:1234"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + if lsof.returncode == 0: + utils.die("Port 1234 is already in use, is QEMU running?") + + utils.green("Starting QEMU with GDB connection on port 1234...") + qemu_process = subprocess.Popen(qemu_cmd + ["-s", "-S"]) + + utils.green("Starting GDB...") + utils.check_cmd(gdb_bin) + gdb_cmd = [gdb_bin] + gdb_cmd += [Path(kernel_location).joinpath("vmlinux").as_posix()] + gdb_cmd += ["-ex", "target remote :1234"] + subprocess.run(gdb_cmd) + + utils.red("Killing QEMU...") + qemu_process.kill() + qemu_process.wait() + + answer = input("Re-run QEMU + gdb? [y/n] ") + if answer.lower() == "n": + exit(0) + else: + qemu_cmd += ["-serial", "mon:stdio"] + + if not interactive: + timeout_cmd = ["timeout", "--foreground", timeout] + stdbuf_cmd = ["stdbuf", "-oL", "-eL"] + qemu_cmd = timeout_cmd + stdbuf_cmd + qemu_cmd + + try: + pretty_print_qemu_cmd(qemu_cmd) + subprocess.run(qemu_cmd, check=True) + except subprocess.CalledProcessError as ex: + if ex.returncode == 124: + utils.red("ERROR: QEMU timed out!") + else: + utils.red("ERROR: QEMU did not exit cleanly!") + exit(ex.returncode) + + +if __name__ == '__main__': + args = parse_arguments() + + # Build configuration from arguments and QEMU flags + cfg = setup_cfg(args) + cfg = get_qemu_args(cfg) + + launch_qemu(cfg) diff --git a/boot-qemu.sh b/boot-qemu.sh deleted file mode 100755 index f3bd73e..0000000 --- a/boot-qemu.sh +++ /dev/null @@ -1,468 +0,0 @@ -#!/usr/bin/env bash - -# Root of the repo -BASE=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd) -APPEND_STRING="" - -source "${BASE}"/utils.sh - -# Check that a binary is found -function checkbin() { - command -v "${1}" &>/dev/null || die "${1} could not be found, please install it!" -} - -# Parse inputs to the script -function parse_parameters() { - while ((${#})); do - case ${1} in - -a | --arch | --architecture) - shift - case ${1} in - arm | arm32_v5 | arm32_v6 | arm32_v7 | arm64 | arm64be | m68k | mips | mipsel | ppc32 | ppc32_mac | ppc64 | ppc64le | riscv | s390 | x86 | x86_64) ARCH=${1} ;; - *) die "Invalid --arch value '${1}'" ;; - esac - ;; - - --append) - shift && APPEND_STRING="${1} " - ;; - - -d | --debug) - set -x - ;; - - --debian) - DEBIAN=true - INTERACTIVE=true - ;; - - -g | --gdb) - GDB=true - INTERACTIVE=true - ;; - - -h | --help) - echo - cat "${BASE}"/README.txt - echo - exit 0 - ;; - - -i | --interactive | --shell) - INTERACTIVE=true - ;; - - -k | --kernel-location) - shift && KERNEL_LOCATION=${1} - ;; - - --no-kvm) - KVM=false - ;; - - -s | --smp) - shift && SMP=${1} - ;; - - -t | --timeout) - shift && TIMEOUT=${1} - ;; - - *) - die "Invalid parameter '${1}'" - ;; - esac - shift - done -} - -# Sanity check parameters and required tools -function sanity_check() { - # Kernel build folder and architecture are required paramters - [[ -z ${ARCH} ]] && die "Architecture ('-a') is required but not specified!" - [[ -z ${KERNEL_LOCATION} ]] && die "Kernel image or kernel build folder ('-k') is required but not specified!" - - # Some default values - [[ -z ${DEBIAN} ]] && DEBIAN=false - [[ -z ${INTERACTIVE} ]] && INTERACTIVE=false - [[ -z ${KVM} ]] && KVM=true - - # KERNEL_LOCATION could be a relative path; turn it into an absolute one with readlink - KERNEL_LOCATION=$(readlink -f "${KERNEL_LOCATION}") - - # Make sure zstd is install - checkbin zstd -} - -function get_default_smp_value() { - # KERNEL_LOCATION is either a path to the kernel source or a full kernel - # location. If it is a file, we need to strip off the basename so that we - # can easily navigate around with '..'. - if [[ -f ${KERNEL_LOCATION} ]]; then - KERNEL_DIRNAME=$(dirname "${KERNEL_LOCATION}") - else - KERNEL_DIRNAME=${KERNEL_LOCATION} - fi - - # If KERNEL_LOCATION is the kernel source, the configuration will be at - # ${KERNEL_DIRNAME}/.config - # - # If KERNEL_LOCATION is a full kernel location, it could either be: - # * ${KERNEL_DIRNAME}/.config (if the image is vmlinux) - # * ${KERNEL_DIRNAME}/../../../.config (if the image is in arch/*/boot/) - # * ${KERNEL_DIRNAME}/config (if the image is in a TuxMake folder) - for CONFIG_LOCATION in .config ../../../.config config; do - CONFIG_FILE=$(readlink -f "${KERNEL_DIRNAME}/${CONFIG_LOCATION}") - if [[ -f ${CONFIG_FILE} ]]; then - HAS_CONFIG=true - break - fi - done - - if ${HAS_CONFIG:=false}; then - CONFIG_NR_CPUS=$(grep "^CONFIG_NR_CPUS=" "${CONFIG_FILE}" | cut -d= -f2) - fi - - if [[ -z ${CONFIG_NR_CPUS} ]]; then - # Sensible default value based on treewide defaults for CONFIG_NR_CPUS. - CONFIG_NR_CPUS=8 - fi - - # Use the minimum of the number of processors in the system or - # CONFIG_NR_CPUS. - CPUS=$(nproc) - if [[ ${CPUS} -gt ${CONFIG_NR_CPUS} ]]; then - echo "${CONFIG_NR_CPUS}" - else - echo "${CPUS}" - fi -} - -# Takes a version (x.y.z) and prints a six or seven digit number -# For example, QEMU 6.2.50 would become 602050 and Linux 5.10.100 -# would become 510100 -function print_ver_code() { - IFS=. read -ra VER_CODE <<<"${1}" - printf "%d%02d%03d" "${VER_CODE[@]}" -} - -# Print QEMU version as a six or seven digit number -function get_qemu_ver_code() { - print_ver_code "$("${QEMU[@]}" --version | head -1 | cut -d ' ' -f 4)" -} - -# Print Linux version of a kernel image as a six or seven digit number -# Takes the command to dump a kernel image to stdout as its argument -function get_lnx_ver_code() { - print_ver_code "$("${@}" |& strings |& grep -E "^Linux version [0-9]\.[0-9]+\.[0-9]+" | cut -d ' ' -f 3 | cut -d - -f 1)" -} - -# Boot QEMU -function setup_qemu_args() { - # All arm32_* options share the same rootfs, under images/arm - [[ ${ARCH} =~ arm32 ]] && ARCH_RTFS_DIR=arm - # All ppc32_* options share the same rootfs, under images/ppc32 - [[ ${ARCH} =~ ppc32 ]] && ARCH_RTFS_DIR=ppc32 - - IMAGES_DIR=${BASE}/images/${ARCH_RTFS_DIR:-${ARCH}} - if ${DEBIAN}; then - ROOTFS=${IMAGES_DIR}/debian.img - [[ -f ${ROOTFS} ]] || die "'--debian' requires a debian.img. Run 'sudo debian/build.sh -a ${IMAGES_DIR##*/}' to generate it." - else - ROOTFS=${IMAGES_DIR}/rootfs.cpio - fi - - if ${INTERACTIVE}; then - if ${DEBIAN}; then - APPEND_STRING+="root=/dev/vda " - else - APPEND_STRING+="rdinit=/bin/sh " - fi - fi - if ${GDB:=false}; then - APPEND_STRING+="nokaslr " - fi - - case ${ARCH} in - arm32_v5) - APPEND_STRING+="earlycon " - ARCH=arm - DTB=aspeed-bmc-opp-palmetto.dtb - QEMU_ARCH_ARGS=( - -machine palmetto-bmc - ) - QEMU=(qemu-system-arm) - ;; - - arm32_v6) - ARCH=arm - DTB=aspeed-bmc-opp-romulus.dtb - QEMU_ARCH_ARGS=( - -machine romulus-bmc - ) - QEMU=(qemu-system-arm) - ;; - - arm | arm32_v7) - ARCH=arm - APPEND_STRING+="console=ttyAMA0 earlycon " - # https://lists.nongnu.org/archive/html/qemu-discuss/2018-08/msg00030.html - # VFS: Cannot open root device "vda" or unknown-block(0,0): error -6 - ${DEBIAN} && HIGHMEM=,highmem=off - QEMU_ARCH_ARGS=( - -machine "virt${HIGHMEM}" - ) - # It is possible to boot ARMv7 kernels under KVM on AArch64 hosts, - # if it is supported. ARMv7 KVM support was ripped out of the - # kernel in 5.7 so we don't even bother checking. - if [[ "$(uname -m)" = "aarch64" && -e /dev/kvm ]] && ${KVM} && - "${BASE}"/utils/aarch64_32_bit_el1_supported; then - QEMU_ARCH_ARGS+=( - -cpu "host,aarch64=off" - -enable-kvm - -smp "${SMP:-$(get_default_smp_value)}" - ) - QEMU=(qemu-system-aarch64) - else - QEMU=(qemu-system-arm) - fi - ;; - - arm64 | arm64be) - ARCH=arm64 - KIMAGE=Image.gz - QEMU=(qemu-system-aarch64) - APPEND_STRING+="console=ttyAMA0 earlycon " - QEMU_ARCH_ARGS=(-machine "virt,gic-version=max") - if [[ "$(uname -m)" = "aarch64" && -e /dev/kvm ]] && ${KVM}; then - QEMU_ARCH_ARGS+=( - -cpu host - -enable-kvm - -smp "${SMP:-$(get_default_smp_value)}" - ) - else - get_full_kernel_path - QEMU_VER_CODE=$(get_qemu_ver_code) - if [[ ${QEMU_VER_CODE} -ge 602050 ]]; then - LNX_VER_CODE=$(get_lnx_ver_code gzip -c -d "${KERNEL}") - # https://gitlab.com/qemu-project/qemu/-/issues/964 - if [[ ${LNX_VER_CODE} -lt 416000 ]]; then - CPU=cortex-a72 - # lpa2=off: https://gitlab.com/qemu-project/qemu/-/commit/69b2265d5fe8e0f401d75e175e0a243a7d505e53 - # pauth-impdef=true: https://lore.kernel.org/YlgVa+AP0g4IYvzN@lakrids/ - elif [[ ${LNX_VER_CODE} -lt 512000 ]]; then - CPU=max,lpa2=off,pauth-impdef=true - fi - fi - if [[ -z ${CPU} ]]; then - CPU=max - # https://lore.kernel.org/YlgVa+AP0g4IYvzN@lakrids/ - [[ ${QEMU_VER_CODE} -ge 600000 ]] && CPU=${CPU},pauth-impdef=true - fi - QEMU_ARCH_ARGS+=( - -cpu "${CPU}" - -machine "virtualization=true" - ) - fi - # Give the machine more cores and memory when booting Debian to - # improve performance - if ${DEBIAN}; then - QEMU_RAM=2G - # Do not add '-smp' if it is present at this point, as that - # means that KVM is being used, which will already have a - # suitable number of cores - if ! echo "${QEMU_ARCH_ARGS[*]}" | grep -q smp; then - QEMU_ARCH_ARGS+=(-smp "${SMP:-4}") - fi - fi - ;; - - m68k) - APPEND_STRING+="console=ttyS0,115200 " - KIMAGE=vmlinux - QEMU_ARCH_ARGS=( - -cpu m68040 - -M q800 - ) - QEMU=(qemu-system-m68k) - ;; - - mips | mipsel) - KIMAGE=vmlinux - QEMU_ARCH_ARGS=( - -cpu 24Kf - -machine malta - ) - QEMU=(qemu-system-"${ARCH}") - ARCH=mips - ;; - - ppc32 | ppc32_mac) - case ${ARCH} in - ppc32) - KIMAGE=uImage - QEMU_ARCH_ARGS=(-machine bamboo) - ;; - ppc32_mac) - KIMAGE=vmlinux - QEMU_ARCH_ARGS=(-machine mac99) - ;; - esac - ARCH=powerpc - APPEND_STRING+="console=ttyS0 " - QEMU_RAM=128m - QEMU=(qemu-system-ppc) - ;; - - ppc64) - ARCH=powerpc - KIMAGE=vmlinux - QEMU_ARCH_ARGS=( - -cpu power8 - -machine pseries - -vga none - ) - QEMU_RAM=1G - QEMU=(qemu-system-ppc64) - ;; - - ppc64le) - ARCH=powerpc - KIMAGE=zImage.epapr - QEMU_ARCH_ARGS=( - -device "ipmi-bmc-sim,id=bmc0" - -device "isa-ipmi-bt,bmc=bmc0,irq=10" - -L "${IMAGES_DIR}/" -bios skiboot.lid - -machine powernv8 - ) - QEMU_RAM=2G - QEMU=(qemu-system-ppc64) - ;; - - riscv) - APPEND_STRING+="earlycon " - KIMAGE=Image - DEB_BIOS=/usr/lib/riscv64-linux-gnu/opensbi/qemu/virt/fw_jump.elf - [[ -f ${DEB_BIOS} && -z ${BIOS} ]] && BIOS=${DEB_BIOS} - QEMU_ARCH_ARGS=( - -bios "${BIOS:-default}" - -M virt - ) - QEMU=(qemu-system-riscv64) - ;; - - s390) - KIMAGE=bzImage - QEMU_ARCH_ARGS=(-M s390-ccw-virtio) - QEMU=(qemu-system-s390x) - ;; - - x86 | x86_64) - KIMAGE=bzImage - APPEND_STRING+="console=ttyS0 earlycon=uart8250,io,0x3f8 " - # Use KVM if the processor supports it and the KVM module is loaded (i.e. /dev/kvm exists) - if [[ $(grep -c -E 'vmx|svm' /proc/cpuinfo) -gt 0 && -e /dev/kvm ]] && ${KVM}; then - QEMU_ARCH_ARGS=( - -cpu host - -d "unimp,guest_errors" - -enable-kvm - -smp "${SMP:-$(get_default_smp_value)}" - ) - else - [[ ${ARCH} = "x86_64" ]] && QEMU_ARCH_ARGS=(-cpu Nehalem) - fi - case ${ARCH} in - x86) QEMU=(qemu-system-i386) ;; - x86_64) QEMU=(qemu-system-x86_64) ;; - esac - ;; - esac - checkbin "${QEMU[*]}" - - [[ -z ${KERNEL} ]] && get_full_kernel_path - - if [[ -n ${DTB} ]]; then - # If we are in a boot folder, look for them in the dts folder in it - if [[ $(basename "${KERNEL%/*}") = "boot" ]]; then - DTB_FOLDER=dts/ - # Otherwise, assume there is a dtbs folder in the same folder as the kernel image (tuxmake) - else - DTB_FOLDER=dtbs/ - fi - DTB=${KERNEL%/*}/${DTB_FOLDER}${DTB} - [[ -f ${DTB} ]] || die "${DTB##*/} is required for booting but it could not be found at ${DTB}!" - QEMU_ARCH_ARGS+=(-dtb "${DTB}") - fi -} - -# Invoke QEMU -function invoke_qemu() { - green "QEMU location: " "$(dirname "$(command -v "${QEMU[*]}")")" '\n' - green "QEMU version: " "$("${QEMU[@]}" --version | head -n1)" '\n' - - [[ -z ${QEMU_RAM} ]] && QEMU_RAM=512m - if ${DEBIAN}; then - QEMU+=(-drive "file=${ROOTFS},format=raw,if=virtio,index=0,media=disk") - else - rm -rf "${ROOTFS}" - zstd -q -d "${ROOTFS}".zst -o "${ROOTFS}" - QEMU+=(-initrd "${ROOTFS}") - fi - # Removing trailing space for aesthetic purposes - [[ -n ${APPEND_STRING} ]] && QEMU+=(-append "${APPEND_STRING%* }") - if [[ -n ${SMP} ]] && ! echo "${QEMU_ARCH_ARGS[*]}" | grep -q "smp"; then - QEMU+=(-smp "${SMP}") - fi - if ${GDB:=false}; then - while true; do - if lsof -i:1234 &>/dev/null; then - red "Port :1234 already bound to. QEMU already running?" - exit 1 - fi - green "Starting QEMU with GDB connection on port 1234..." - # Note: no -serial mon:stdio - "${QEMU[@]}" \ - "${QEMU_ARCH_ARGS[@]}" \ - -display none \ - -kernel "${KERNEL}" \ - -m "${QEMU_RAM}" \ - -nodefaults \ - -s -S & - QEMU_PID=$! - green "Starting GDB..." - "${GDB_BIN:-gdb-multiarch}" "${KERNEL_LOCATION}/vmlinux" \ - -ex "target remote :1234" - red "Killing QEMU..." - kill -9 "${QEMU_PID}" - wait "${QEMU_PID}" 2>/dev/null - while true; do - read -rp "Rerun [Y/n/?] " yn - case $yn in - [Yy]*) break ;; - [Nn]*) exit 0 ;; - *) break ;; - esac - done - done - fi - - ${INTERACTIVE} || QEMU=(timeout --foreground "${TIMEOUT:=3m}" stdbuf -oL -eL "${QEMU[@]}") - set -x - "${QEMU[@]}" \ - "${QEMU_ARCH_ARGS[@]}" \ - -no-reboot \ - -display none \ - -kernel "${KERNEL}" \ - -m "${QEMU_RAM}" \ - -nodefaults \ - -serial mon:stdio - RET=${?} - set +x - - return ${RET} -} - -parse_parameters "${@}" -sanity_check -setup_qemu_args -invoke_qemu diff --git a/boot-uml.py b/boot-uml.py new file mode 100755 index 0000000..58b52e8 --- /dev/null +++ b/boot-uml.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +import argparse +from pathlib import Path +import subprocess + +import utils + +base_folder = Path(__file__).resolve().parent + + +def parse_arguments(): + """ + Parses arguments to script. + + Returns: + A Namespace object containing key values from parser.parse_args() + """ + parser = argparse.ArgumentParser() + + parser.add_argument( + "-i", + "--interactive", + action="store_true", + help= + "Instead of immediately shutting down upon successful boot, pass 'init=/bin/sh' to the UML executable to allow interacting with UML via a shell." + ) + parser.add_argument( + "-k", + "--kernel-location", + required=True, + type=str, + help= + "Path to UML executable ('linux') or kernel build folder to search for executable in. Can be an absolute or relative path." + ) + + return parser.parse_args() + + +def decomp_rootfs(): + """ + Decompress and get the full path of the initial ramdisk for use with UML. + + Returns: + rootfs (Path): rootfs Path object containing full path to rootfs. + """ + rootfs = base_folder.joinpath("images", "x86_64", "rootfs.ext4") + + # This could be 'rootfs.unlink(missing_ok=True)' but that was only added in Python 3.8. + if rootfs.exists(): + rootfs.unlink() + + utils.check_cmd("zstd") + subprocess.run(["zstd", "-q", "-d", "{}.zst".format(rootfs), "-o", rootfs], + check=True) + + return rootfsi + + +def run_kernel(kernel, rootfs, interactive): + """ + Run UML command with path to rootfs and additional arguments based on user + input. + + Parameters: + * kernel (Path): kernel Path object containing full path to kernel. + * rootfs (Path): rootfs Path object containing full path to rootfs. + * interactive (bool): Whether or not to run UML interactively. + """ + uml_cmd = [kernel.as_posix(), "ubd0={}".format(rootfs.as_posix())] + if interactive: + uml_cmd += ["init=/bin/sh"] + print("$ {}".format(" ".join([str(element) for element in uml_cmd]))) + subprocess.run(uml_cmd, check=True) + + +if __name__ == '__main__': + args = parse_arguments() + kernel = utils.get_full_kernel_path(args.kernel_location, "linux") + rootfs = decomp_rootfs() + + run_kernel(kernel, rootfs, args.interactive) diff --git a/boot-uml.sh b/boot-uml.sh deleted file mode 100755 index e011b48..0000000 --- a/boot-uml.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -BASE=$(dirname "$(readlink -f "$0")") -source "$BASE"/utils.sh - -function parse_parameters() { - while (($#)); do - case $1 in - -i | --interactive | --shell) - kernel_args=(init=/bin/sh) - ;; - -k | --kernel-location) - shift - KERNEL_LOCATION=$1 - ;; - esac - shift - done -} - -function reality_check() { - [[ -z $KERNEL_LOCATION ]] && die "Kernel image or kernel build folder ('-k') is required but not specified!" - KIMAGE=linux get_full_kernel_path -} - -function decomp_rootfs() { - rootfs=$BASE/images/x86_64/rootfs.ext4 - rm -rf "$rootfs" - zstd -q -d "$rootfs".zst -o "$rootfs" -} - -function execute_kernel() { - "$KERNEL" ubd0="$rootfs" "${kernel_args[@]}" -} - -parse_parameters "$@" -reality_check -decomp_rootfs -execute_kernel diff --git a/utils.py b/utils.py new file mode 100755 index 0000000..f627491 --- /dev/null +++ b/utils.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import shutil + + +def check_cmd(cmd): + """ + Checks if external command is available in PATH, erroring out if it is not + available. + + Parameters: + cmd (str): External command name or path. + """ + if not shutil.which(cmd): + die("The external command '{}' is needed but it could not be found in PATH, please install it!" + .format(cmd)) + + +def die(string): + """ + Prints a string in bold red then exits with an error code of 1. + + Parameters: + string (str): String to print in red; prefixed with "ERROR: " + automatically. + """ + red("ERROR: {}".format(string)) + exit(1) + + +def get_full_kernel_path(kernel_location, image, arch=None): + """ + Get the full path to a kernel image based on the architecture and image + name if necessary. + + Parameters: + kernel_location (str): Absolute or relative path to kernel image or + kernel build folder. + image (str): Kernel image name. + arch (str, optional): Architecture name according to Kbuild; should be + the parent of the "boot" folder containing the + kernel image (default: None). + """ + kernel_location = Path(kernel_location) + + # If '-k' is a file, we can just use it directly + if kernel_location.is_file(): + kernel = kernel_location + # If not, we need to find it based on the kernel build directory + else: + # If the image is an uncompressed vmlinux or a UML image, it is in the + # root of the build folder + if image == "vmlinux" or image == "linux": + kernel = kernel_location.joinpath(image) + # Otherwise, it is in the architecture's boot directory + else: + if not arch: + die("Kernel image ('{}') is in the arch/ directory but 'arch' was not provided!" + .format(image)) + kernel = kernel_location.joinpath("arch", arch, "boot", image) + + if not kernel.exists(): + die("Kernel ('{}') does not exist!".format(kernel)) + + return kernel + + +def green(string): + """ + Prints string in bold green. + + Parameters: + string (str): String to print in bold green. + """ + print("\n\033[01;32m{}\033[0m".format(string)) + + +def red(string): + """ + Prints string in bold red. + + Parameters: + string (str): String to print in bold red. + """ + print("\n\033[01;31m{}\033[0m".format(string)) diff --git a/utils.sh b/utils.sh deleted file mode 100755 index 922b050..0000000 --- a/utils.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -function pretty_print() { - printf "%b%s\033[0m" "${1}" "${2}" - shift 2 - while ((${#})); do - printf "%b" "${1}" - shift - done - printf '\n' -} - -function green() { - pretty_print "\033[01;32m" "${@}" -} - -function red() { - pretty_print "\033[01;31m" "${@}" -} - -# Prints an error message in bold red then exits -function die() { - red "${@}" - exit 1 -} - -# Expands '-k' to an absolute path to a kernel image if necessary -function get_full_kernel_path() { - # If '-k' is a file, we can just use it directly - if [[ -f ${KERNEL_LOCATION} ]]; then - KERNEL=${KERNEL_LOCATION} - # If not, we need to find it based on the kernel build directory - else - # If the image is an uncompressed vmlinux or a UML image, it is in the - # root of the build folder - # Otherwise, it is in the architecture's boot directory - [[ -z ${KIMAGE} ]] && KIMAGE=zImage - [[ ${KIMAGE} == "vmlinux" || ${KIMAGE} == "linux" ]] || BOOT_DIR=arch/${ARCH}/boot/ - KERNEL=${KERNEL_LOCATION}/${BOOT_DIR}${KIMAGE} - fi - [[ -f ${KERNEL} ]] || die "${KERNEL} does not exist!" -} From 7f88af34915f62dc43371d7eb5ac2525ab1e8351 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Mon, 1 Aug 2022 14:28:16 -0700 Subject: [PATCH 02/10] github: Add yapf step Now that there is Python in this repository, we should make sure that it stays consistently formatted like the shell scripts are. Signed-off-by: Nathan Chancellor --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 627e741..f179c51 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -# Run shellcheck and shfmt on all shell files in this repository +# Run shellcheck and shfmt on all shell files and yapf on all Python files in this repository name: Lint checks on: [push, pull_request] jobs: @@ -6,3 +6,5 @@ jobs: uses: ClangBuiltLinux/actions-workflows/.github/workflows/shellcheck.yml@main shfmt: uses: ClangBuiltLinux/actions-workflows/.github/workflows/shfmt.yml@main + yapf: + uses: ClangBuiltLinux/actions-workflows/.github/workflows/yapf.yml@main From aedb4a4c10fbd61d7b59d6d5c57365204eb5d441 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Mon, 1 Aug 2022 10:15:16 -0700 Subject: [PATCH 03/10] README: Rewrite Now that boot-qemu.sh has been rewritten in Python, this does not serve any real purpose, as Python's argparse automatically shows the options and help text. Rewrite the README to give a brief overview of the repository. Signed-off-by: Nathan Chancellor --- README.md | 11 +++++++ README.txt | 89 ------------------------------------------------------ 2 files changed, 11 insertions(+), 89 deletions(-) create mode 100644 README.md delete mode 100644 README.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..4584dc2 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Boot utilities + +This repository houses scripts to quickly boot test Linux kernels with a simple [Buildroot](https://buildroot.org)-based rootfs. + +* `boot-qemu.py`: Script to boot Linux kernels in QEMU. Run with `-h` for information on options. +* `boot-uml.py`: Script to boot a User Mode Linux (UML) kernel. Run with `-h` for information on options. +* `utils.py`: Common functions to Python scripts, not meant to be called.i + +* `buildroot/`: Scripts and configuration files to generate rootfs images. +* `images/`: Generated rootfs images from Buildroot (compressed with `zstd`). +* `utils/`: Miscellaneous utilities/programs. diff --git a/README.txt b/README.txt deleted file mode 100644 index e27129d..0000000 --- a/README.txt +++ /dev/null @@ -1,89 +0,0 @@ -Usage: ./boot-qemu.sh - -Script description: Boots a Linux kernel in QEMU. - -Required parameters: - -a | --arch | --architecture: - The architecture to boot. Possible values are: - * arm32_v5 - * arm32_v6 - * arm32_v7 - * arm (shorthand for arm32_v7) - * arm64 - * arm64be - * m68k - * mips - * mipsel - * ppc32 - * ppc32_mac - * ppc64 - * ppc64le - * riscv - * s390 - * x86 - * x86_64 - - -k | --kernel-location: - The kernel location, which can either be the kernel image itself or - the root of the kernel build output folder. Either option can be - passed as an absolute path or relative path from wherever the script - is being run. - -Optional parameters: - --append: - Adds additional kernel boot params to the kernel command line. - - -d | --debug: - Invokes 'set -x' for debugging the script. - - --debian: - By default, the script boots a very simple Busybox based root filesystem. - This option allows the script to boot a full Debian root filesystem, - which can be built using 'build.sh' in the debian folder. Run - - $ sudo debian/build.sh -h - - for more information on that script. - - The kernel should be built with the 'kvm_guest.config' target to boot - successfully. For example on an x86_64 host, - - $ make defconfig kvm_guest.config bzImage - - will produce a bootable kernel image. - - -g | --gdb: - Add '-s -S' to the QEMU invocation to allow debugging via GDB (will invoke - `$GDB_BIN` env var else `gdb-multiarch`). - - -h | --help: - Prints this message then exits. - - -i | --interactive | --shell: - By default, the rootfs images in this repo just boots the kernel, - print the version string, then exit. If you would like to actually - interact with the machine, this option passes 'rdinit=/bin/sh' to - the kernel command line so that you are thrown into an interactive - shell. When this is set, there is no timeout so any value supplied - via the script's -t option is ignored. - - --no-kvm: - By default, the script passes '-enable-kvm' to QEMU for hardware - virtualization support if the host machine supports it. The option - prevents that, causing QEMU to fallback to software virtualization. - This can be useful for reproducing certain bugs but booting kernels - will be much slower. - - -s | --smp: - By default, the script does not specify a number of cores for the - QEMU machine, which usually means it spawns with only one core. - Certain features such as the KCSAN KUnit tests require multiple cores - to work so this value will be used for the number of cores for the - virtual machine. It can be more than the number of processors on your - host machine. - - -t | --timeout: - By default, the timeout command waits 3 minutes before killing the - QEMU machine. Depending on the power of the host machine, this might - not be long enough for a kernel to boot so this allows that timeout - to be configured. Takes the value passed to timeout (e.g. 30s or 4m). From 9883bf88383a113de31e585b82d7ff883fd19aa8 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Mon, 1 Aug 2022 10:15:40 -0700 Subject: [PATCH 04/10] debian: Remove Support for this was not forward ported during the Python rewrite, as it did not always work that well. There are other alternatives such as libvirt for running full distributions with QEMU. A future contribution might add a Python script for roughly achieving what this set out to accomplish. Signed-off-by: Nathan Chancellor --- debian/.gitignore | 1 - debian/README.txt | 39 ---------- debian/build.sh | 184 ---------------------------------------------- debian/ltp.sh | 23 ------ 4 files changed, 247 deletions(-) delete mode 100644 debian/.gitignore delete mode 100644 debian/README.txt delete mode 100755 debian/build.sh delete mode 100755 debian/ltp.sh diff --git a/debian/.gitignore b/debian/.gitignore deleted file mode 100644 index 53f2697..0000000 --- a/debian/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tmp.*/ diff --git a/debian/README.txt b/debian/README.txt deleted file mode 100644 index 66eb681..0000000 --- a/debian/README.txt +++ /dev/null @@ -1,39 +0,0 @@ -Usage: ./build.sh - -Script description: Builds a Debian filesystem image that can be booted in QEMU. - -Required parameters: - -a | --arch: - The architecture to build the image for. Possible values are: - * arm - * arm64 - * ppc64le - * s390 - * x86_64 - -Optional parameters: - -l | --ltp: - Builds some test cases from the Linux Test Project that are useful for - finding issues. - - -m | --modules-folder: - Path to the "modules" folder in a Linux kernel build tree. They will be - copied into /lib within the image. For example, - - $ make INSTALL_MOD_PATH=rootfs modules_install - - in a kernel tree will place the modules folder within rootfs/lib/modules - so the value that is passed to this script would be - /rootfs/lib/modules. This is useful for - testing that kernel modules can load as well as verifying additional - functionality within QEMU. - - -p | --password: - The created user account's password. By default, it is just "password". - - -u | --user: - The created user account's name. By default, it is just "user". - - -v | --version: - The version of Debian to build. By default, it is the latest stable which - is currently Buster. diff --git a/debian/build.sh b/debian/build.sh deleted file mode 100755 index ff725da..0000000 --- a/debian/build.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash - -trap 'umount "${MOUNT_DIR}" 2>/dev/null; rm -rf "${WORK_DIR}"' INT TERM EXIT - -# Prints a message in color -function print_color() { - # Reset escape code - RST="\033[0m" - printf "\n%b%s%b\n" "${1}" "${2}" "${RST}" -} - -# Prints an error message in bold red then exits -function die() { - print_color "\033[01;31m" "${1}" - exit "${2:-33}" -} - -# Prints a warning message in bold yellow -function warn() { - print_color "\033[01;33m" "${1}" -} - -# The script requires root in several places, re-run the script with sudo if necessary -function run_as_root() { - [[ ${EUID} -eq 0 ]] && return 0 - warn "Script needs to be run as root, invoking sudo on script..." - echo - echo "$ exec sudo bash ${*}" - exec sudo PATH="${PATH}" bash "${@}" -} - -# Get user inputs -function get_parameters() { - DEBIAN=$(dirname "$(readlink -f "${0}")") - - while ((${#})); do - case ${1} in - -a | --arch) - shift - case ${1} in - arm64) - DEB_ARCH=${1} - OUR_ARCH=${DEB_ARCH} - ;; - arm) - DEB_ARCH=${1}hf - OUR_ARCH=${1} - ;; - ppc64le) - DEB_ARCH=ppc64el - OUR_ARCH=${1} - ;; - s390) - DEB_ARCH=${1}x - OUR_ARCH=${1} - ;; - x86_64) - DEB_ARCH=amd64 - OUR_ARCH=${1} - ;; - *) die "${1} is not supported by this script!" ;; - esac - ;; - -h | --help) - echo - cat "${DEBIAN}"/README.txt - echo - exit 0 - ;; - -l | --ltp) - LTP=true - ;; - -m | --modules-folder) - shift - MODULES_FOLDER=${1} - [[ -d ${MODULES_FOLDER} ]] || die "${MODULES_FOLDER} specified but it does not exist!" - ;; - -p | --password) - shift - DEB_PASS=${1} - ;; - -u | --user) - shift - DEB_USER=${1} - ;; - -v | --version) - shift - DEB_VERSION=${1} - ;; - esac - shift - done -} - -# Checks if command is available -function is_available() { - command -v "${1}" &>/dev/null || die "${1} needs to be installed!" -} - -# Do some initial checks for environment and configuration -function reality_checks() { - # Validity checks - [[ -z ${DEB_ARCH} ]] && die "'-a' is required but not specified!" - - # Some tools are in /usr/sbin or /sbin but they might not be in PATH by default - [[ ${PATH} =~ /sbin ]] || PATH=${PATH}:/usr/sbin:/sbin - is_available blkid - is_available debootstrap - is_available findmnt - is_available mkfs.ext4 - is_available qemu-img - - # Default values - [[ -z ${DEB_VERSION} ]] && DEB_VERSION=bullseye - [[ -z ${DEB_USER} ]] && DEB_USER=user - [[ -z ${DEB_PASS} ]] && DEB_PASS=password - [[ -z ${LTP} ]] && LTP=false -} - -# Build image -function create_img() { - WORK_DIR=$(mktemp -d -p "${DEBIAN}") - ORIG_USER=$(logname) - - set -x - - # Create the image that we will use and mount it - IMG=${WORK_DIR}/debian.img - qemu-img create "${IMG}" 5g - mkfs.ext4 "${IMG}" - MOUNT_DIR=${WORK_DIR}/rootfs - mkdir -p "${MOUNT_DIR}" - mount -o loop "${IMG}" "${MOUNT_DIR}" - - # Install packages - PACKAGES=( - autoconf - automake - bash - bison - build-essential - ca-certificates - flex - git - libtool - m4 - pkg-config - stress-ng - sudo - vim - ) - debootstrap --arch "${DEB_ARCH}" --include="${PACKAGES[*]//${IFS:0:1}/,}" "${DEB_VERSION}" "${MOUNT_DIR}" || exit ${?} - - # Setup user account - chroot "${MOUNT_DIR}" bash -c "useradd -m -G sudo ${DEB_USER} -s /bin/bash && echo ${DEB_USER}:${DEB_PASS} | chpasswd" - - # Add fstab so that / mounts as rw instead of ro - printf "UUID=%s\t/\text4\terrors=remount-ro\t0\t1\n" "$(blkid -o value -s UUID "$(findmnt -n -o SOURCE "${MOUNT_DIR}")")" | tee -a "${MOUNT_DIR}"/etc/fstab - - # Add hostname entry to /etc/hosts so sudo does not complain - printf "127.0.0.1\t%s\n" "$(uname -n)" | tee -a "${MOUNT_DIR}"/etc/hosts - - # Install some problematic LTP testcases for debugging if requested - if ${LTP}; then - LTP_SCRIPT=/home/${DEB_USER}/ltp.sh - cp -v "${DEBIAN}"/ltp.sh "${MOUNT_DIR}${LTP_SCRIPT}" - chroot "${MOUNT_DIR}" bash "${LTP_SCRIPT}" - rm -rf "${LTP_SCRIPT}" - fi - - # Install modules if requested - [[ -n ${MODULES_FOLDER} ]] && cp -rv "${MODULES_FOLDER}" "${MOUNT_DIR}"/lib - - # Unmount, move image, and clean up - umount "${MOUNT_DIR}" - chown -R "${ORIG_USER}:${ORIG_USER}" "${IMG}" - mv -v "${IMG}" "${DEBIAN%/*}/images/${OUR_ARCH}" - rm -rf "${WORK_DIR}" -} - -run_as_root "${0}" "${@}" -get_parameters "${@}" -reality_checks -create_img diff --git a/debian/ltp.sh b/debian/ltp.sh deleted file mode 100755 index 517e4fe..0000000 --- a/debian/ltp.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -# This should be run in the Debian chroot - -LTP=$(dirname "$(readlink -f "${0}")")/ltp -MAKE=(make -skj"$(nproc)") - -set -x - -git clone --depth=1 https://github.com/linux-test-project/ltp "${LTP}" -cd "${LTP}" || exit ${?} - -"${MAKE[@]}" autotools -./configure - -TEST_CASES=( - kernel/fs/proc - kernel/fs/read_all - lib -) -for TEST_CASE in "${TEST_CASES[@]}"; do - cd "${LTP}"/testcases/"${TEST_CASE}" || exit ${?} - "${MAKE[@]}" -done From 961f44063165dc16669e3c5e8d2863af737c6bd8 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 2 Aug 2022 12:20:34 -0700 Subject: [PATCH 05/10] README: Remove stray 'i' Can you tell I use vim? :) Signed-off-by: Nathan Chancellor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4584dc2..b6c0612 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This repository houses scripts to quickly boot test Linux kernels with a simple * `boot-qemu.py`: Script to boot Linux kernels in QEMU. Run with `-h` for information on options. * `boot-uml.py`: Script to boot a User Mode Linux (UML) kernel. Run with `-h` for information on options. -* `utils.py`: Common functions to Python scripts, not meant to be called.i +* `utils.py`: Common functions to Python scripts, not meant to be called. * `buildroot/`: Scripts and configuration files to generate rootfs images. * `images/`: Generated rootfs images from Buildroot (compressed with `zstd`). From 314d18dc9b2d48d6ba7df5a46d4e0dcf7799c791 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 2 Aug 2022 12:22:12 -0700 Subject: [PATCH 06/10] boot-qemu.py: Eliminate unnecessary local variable in can_use_kvm() Signed-off-by: Nathan Chancellor --- boot-qemu.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/boot-qemu.py b/boot-qemu.py index 36c7886..637fec8 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -108,8 +108,7 @@ def can_use_kvm(args): True if KVM can be used based on the above parameters, False if not. """ # We can only test for KVM if the user did not opt out of it - can_test_for_kvm = not args.no_kvm - if can_test_for_kvm: + if not args.no_kvm: # /dev/kvm must exist to use KVM with QEMU if Path("/dev/kvm").exists(): guest_arch = args.architecture From 8ba5012653af7f0f74cf181e93ab3ca71e74d68e Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 2 Aug 2022 12:31:44 -0700 Subject: [PATCH 07/10] boot-qemu.py: Use more specific arguments for can_use_kvm() Signed-off-by: Nathan Chancellor --- boot-qemu.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/boot-qemu.py b/boot-qemu.py index 637fec8..42bba60 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -87,7 +87,7 @@ def parse_arguments(): return parser.parse_args() -def can_use_kvm(args): +def can_use_kvm(can_test_for_kvm, guest_arch): """ Checks that KVM can be used for faster VMs based on: * User's request @@ -102,13 +102,13 @@ def can_use_kvm(args): '/proc/cpuinfo' Parameters: - args (Namespace): The Namespace object returned from parse_arguments() + user_kvm_opt_out (bool): False if user passed in '--no-kvm', True if not + guest_arch (str): The guest architecture being run. Returns: True if KVM can be used based on the above parameters, False if not. """ - # We can only test for KVM if the user did not opt out of it - if not args.no_kvm: + if can_test_for_kvm: # /dev/kvm must exist to use KVM with QEMU if Path("/dev/kvm").exists(): guest_arch = args.architecture @@ -240,7 +240,7 @@ def setup_cfg(args): "smp_requested": args.smp is not None, "smp_value": get_smp_value(args), "timeout": args.timeout, - "use_kvm": can_use_kvm(args), + "use_kvm": can_use_kvm(not args.no_kvm, args.architecture), } From 9dc0d2048b01e39860fda2a849859c43931ba140 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 2 Aug 2022 12:33:20 -0700 Subject: [PATCH 08/10] boot-qemu.py: print_version_code() -> create_version_code() The version code is not printed anywhere so the function name is not as accurate as it should be. Signed-off-by: Nathan Chancellor --- boot-qemu.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/boot-qemu.py b/boot-qemu.py index 42bba60..4ebf74f 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -244,9 +244,9 @@ def setup_cfg(args): } -def print_version_code(version): +def create_version_code(version): """ - Prints a version list with three values (major, minor, and patch level) as + Turns a version list with three values (major, minor, and patch level) into an integer with at least six digits: * major: as is * minor: with a minimum length of two ("1" becomes "01") @@ -297,7 +297,7 @@ def get_qemu_ver_code(qemu): # "QEMU emulator version x.y.z (...)" -> x.y.z -> ['x', 'y', 'z'] qemu_version = qemu_version_string.split(" ")[3].split(".") - return print_version_code(qemu_version) + return create_version_code(qemu_version) def get_linux_ver_code(decomp_cmd): @@ -335,7 +335,7 @@ def get_linux_ver_code(decomp_cmd): utils.die("Linux version string could not be found in '{}'".format( kernel_path)) - return print_version_code(linux_version) + return create_version_code(linux_version) def get_and_decomp_rootfs(cfg): From 312aaefd30d3aa167c095c9788605d8631d2552e Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 2 Aug 2022 12:36:32 -0700 Subject: [PATCH 09/10] boot-qemu.py: Use 'break' instead of 'exit(0)' in launch_qemu() This will be better in case we add anything after the 'gdb' if block. Signed-off-by: Nathan Chancellor --- boot-qemu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boot-qemu.py b/boot-qemu.py index 4ebf74f..931d966 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -699,7 +699,7 @@ def launch_qemu(cfg): answer = input("Re-run QEMU + gdb? [y/n] ") if answer.lower() == "n": - exit(0) + break else: qemu_cmd += ["-serial", "mon:stdio"] From d23d9941f99ed04d100ef57ea8f306ec3f18c196 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 2 Aug 2022 12:37:47 -0700 Subject: [PATCH 10/10] boot-qemu.py: Move pretty_print_qemu_cmd() out of try block in launch_qemu() It is not necessary. Signed-off-by: Nathan Chancellor --- boot-qemu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boot-qemu.py b/boot-qemu.py index 931d966..094d7ca 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -708,8 +708,8 @@ def launch_qemu(cfg): stdbuf_cmd = ["stdbuf", "-oL", "-eL"] qemu_cmd = timeout_cmd + stdbuf_cmd + qemu_cmd + pretty_print_qemu_cmd(qemu_cmd) try: - pretty_print_qemu_cmd(qemu_cmd) subprocess.run(qemu_cmd, check=True) except subprocess.CalledProcessError as ex: if ex.returncode == 124: