This resurrects the hand run test script from d84842be6c5 (tests: remove test-gdbstub.py) and modernises it to use the shared test infrastructure for run-test.
We then tweak the common tuxrun code to over an alternative "launch via gdbstub" helper which reuses the existing code but: - needs a vmlinux with symbols - drops kaslr because it doesn't play nice with symbols - extend qargs with stuff normally added by vm.launch - passes kargs directly - runs with the gdb run-test script - log the run-test output to the console log - makes --binary not mandatory Message-ID: <[email protected]> Signed-off-by: Alex Bennée <[email protected]> --- v3 - mention the nokaslr in the commit message - add a comment about it to the shared kernel args - drop unused failcount - remove stale comment about hbreak - always delete the watch point pass or fail - fix unbalanced tuple in sh4's direct call of fetch_tuxrun_assets - simplify the symbol to watchpoint logic - drop the mandatory --binary - make the command copy and paste friendly v2 - add self.vm._machine to qargs - drop leading spaces in print () calls - add register read tests - use gdb.Breakpoint for hwbreak as for swbreak - drop flaky symbol evaluation, verify hit_count instead - explicit gdb.BP_BREAKPOINT for CatchBreakpoint --- .../functional/gdb-scripts/kernel_gdb_test.py | 162 ++++++++++++++++++ tests/functional/qemu_test/tuxruntest.py | 97 ++++++++++- tests/functional/sh4/test_tuxrun.py | 4 +- tests/guest-debug/run-test.py | 14 +- 4 files changed, 261 insertions(+), 16 deletions(-) create mode 100644 tests/functional/gdb-scripts/kernel_gdb_test.py diff --git a/tests/functional/gdb-scripts/kernel_gdb_test.py b/tests/functional/gdb-scripts/kernel_gdb_test.py new file mode 100644 index 00000000000..f457ccdcbf4 --- /dev/null +++ b/tests/functional/gdb-scripts/kernel_gdb_test.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Generic Kernel GDB script to verify functionality: +# - Single-stepping +# - Hardware breakpoints +# - Software breakpoints +# - Watchpoints +# +# It relies on fairly common kernel points to break at and symbols for +# the kernel. Address space randomisation (kaslr) needs to be disabled. +# +# Copyright (c) 2026 Linaro Limited +# +# Author: +# Alex Bennée <[email protected]> +# +# This is launched by the tuxrun functional tests via tests/guest-debug/run-test.py + +import gdb +from test_gdbstub import main, report + +def parse_registers(): + """Parse 'info registers' into a name -> value dictionary.""" + regs = {} + for line in gdb.execute("info registers", to_string=True).splitlines(): + parts = line.split() + if len(parts) >= 2: + regs[parts[0]] = parts[1] + return regs + + +def check_step(): + "Step an instruction, check it moved." + start_pc = gdb.parse_and_eval('$pc') + gdb.execute("si") + end_pc = gdb.parse_and_eval('$pc') + + return not (start_pc == end_pc) + + +def check_reg_updates(regs_start): + regs_end = parse_registers() + + changed = [k for k in regs_start + if k in regs_end and regs_start[k] != regs_end[k]] + + report(len(changed) > 0, f"{len(changed)} registers changed.") + + +def check_break(sym_name): + "Setup breakpoint, continue and check we stopped." + bp = gdb.Breakpoint(sym_name) + gdb.execute("c") + res = bp.hit_count == 1 + bp.delete() + + return res + + +def check_hbreak(sym_name): + "Setup hardware breakpoint, continue and check we stopped." + bp = gdb.Breakpoint(sym_name, type = gdb.BP_HARDWARE_BREAKPOINT) + gdb.execute("c") + res = bp.hit_count == 1 + bp.delete() + + return res + + +class WatchPoint(gdb.Breakpoint): + + def __init__(self, sym_name, type): + super(WatchPoint, self).__init__(sym_name, gdb.BP_WATCHPOINT, type) + + def stop(self): + end_pc = gdb.parse_and_eval('$pc') + print("HIT WP @ %s" % (end_pc)) + return True + + +def do_one_watch(sym, wtype, text): + + wp = WatchPoint(sym, wtype) + gdb.execute("c") + report_str = "%s for %s" % (text, sym) + + if wp.hit_count > 0: + report(True, report_str) + else: + report(False, report_str) + + wp.delete() + + +def check_watches(sym_name): + "Watch a symbol for any access." + + # Should hit for any read + do_one_watch(sym_name, gdb.WP_ACCESS, "awatch") + + # Again should hit for reads + do_one_watch(sym_name, gdb.WP_READ, "rwatch") + + # Finally when it is written + do_one_watch(sym_name, gdb.WP_WRITE, "watch") + + +class CatchBreakpoint(gdb.Breakpoint): + def __init__(self, sym_name): + super(CatchBreakpoint, self).__init__(sym_name, type = gdb.BP_BREAKPOINT) + self.sym, ok = gdb.lookup_symbol(sym_name) + + def stop(self): + end_pc = gdb.parse_and_eval('$pc') + print("CB: %s == %s" % (end_pc, self.sym.value())) + if end_pc == self.sym.value(): + report(False, "Hit final catchpoint") + + +def run_test(): + "Run through the tests one by one" + + # get the initial register state + regs_start = parse_registers() + + print("Checking we can step the first few instructions") + step_ok = 0 + for i in range(3): + if check_step(): + step_ok += 1 + + report(step_ok == 3, "single step in boot code") + + # check some registers changed + print("Checking register access works") + check_reg_updates(regs_start) + + print("Checking HW breakpoint works") + break_ok = check_hbreak("kernel_init") + report(break_ok, "hbreak @ kernel_init") + + # Can't set this up until we are in the kernel proper + # if we make it to run_init_process we've over-run and + # one of the tests failed + + print("Setup catch-all for run_init_process") + cbp = CatchBreakpoint("run_init_process") + + # We should see wait_for_completion called right at the start of + # kernel_init + print("Checking Normal breakpoint works") + break_ok = check_break("wait_for_completion") + report(break_ok, "break @ wait_for_completion") + + print("Checking watchpoint works") + check_watches("system_state") + + # check we didn't hit the end + report(cbp.hit_count == 0, "CatchBreakpoint not hit") + + +main(run_test) diff --git a/tests/functional/qemu_test/tuxruntest.py b/tests/functional/qemu_test/tuxruntest.py index ca1589fea37..6ad421e897d 100644 --- a/tests/functional/qemu_test/tuxruntest.py +++ b/tests/functional/qemu_test/tuxruntest.py @@ -9,7 +9,9 @@ # # SPDX-License-Identifier: GPL-2.0-or-later +import logging import os +import subprocess from qemu_test import QemuSystemTest from qemu_test import exec_command_and_wait_for_pattern @@ -18,7 +20,8 @@ class TuxRunBaselineTest(QemuSystemTest): - KERNEL_COMMON_COMMAND_LINE = 'printk.time=0' + # nokaslr is needed for gdb tests but it makes things simpler anyway + KERNEL_COMMON_COMMAND_LINE = 'printk.time=0 nokaslr' # Tests are ~10-40s, allow for --debug/--enable-gcov overhead timeout = 100 @@ -55,17 +58,20 @@ def wait_for_console_pattern(self, success_message, vm=None): failure_message='Kernel panic - not syncing', vm=vm) - def fetch_tuxrun_assets(self, kernel_asset, rootfs_asset, dtb_asset=None): + def fetch_tuxrun_assets(self, kernel_asset, rootfs_asset, + dtb_asset=None, vmlinux_asset=None): """ Fetch the TuxBoot assets. """ kernel_image = kernel_asset.fetch() disk_image = self.uncompress(rootfs_asset) dtb = dtb_asset.fetch() if dtb_asset is not None else None + vmlinux = self.uncompress(vmlinux_asset) if vmlinux_asset is not None else None - return (kernel_image, disk_image, dtb) + return (kernel_image, disk_image, dtb, vmlinux) - def prepare_run(self, accel, kernel, disk, drive, dtb=None, console_index=0): + def prepare_run(self, accel, kernel, disk, drive, + dtb=None, console_index=0, append = True): """ Setup to run and add the common parameters to the system """ @@ -83,9 +89,15 @@ def prepare_run(self, accel, kernel, disk, drive, dtb=None, console_index=0): if accel: self.vm.add_args('-accel', accel) - self.vm.add_args('-kernel', kernel, - '-append', self.kcmd_line, - '-blockdev', blockdev) + if kernel: + self.vm.add_args('-kernel', kernel) + + # When using the gdb run script we might not want to include + # the kcmd line directly in the QEMU args. + if append: + self.vm.add_args('-append', self.kcmd_line) + + self.vm.add_args('-blockdev', blockdev) # Sometimes we need extra devices attached if self.extradev: @@ -132,10 +144,77 @@ def common_tuxrun(self, special with the command line we can process most things using the tag metadata. """ - (kernel, disk, dtb) = self.fetch_tuxrun_assets(kernel_asset, rootfs_asset, - dtb_asset) + (kernel, disk, dtb, _) = self.fetch_tuxrun_assets(kernel_asset, + rootfs_asset, + dtb_asset) self.prepare_run(accel, kernel, disk, drive, dtb, console_index) self.vm.launch() self.run_tuxtest_tests(haltmsg) os.remove(disk) + + def common_tuxrun_gdb(self, + kernel_asset, + vmlinux_asset, + rootfs_asset, + accel=None, + dtb_asset=None, + drive="virtio-blk-device", + haltmsg="reboot: System halted", + console_index=0): + """ + Common path for LKFT tests under gdb. Unlike the common_tuxrun + function we need to launch with the gdb test script and deal + with munging the arguments for that especially. + """ + (kernel, disk, dtb, vmlinux) = self.fetch_tuxrun_assets(kernel_asset, + rootfs_asset, + dtb_asset, + vmlinux_asset) + + gdb_path = os.getenv("QEMU_TEST_GDB") + run_script = self.data_file('..', 'guest-debug','run-test.py') + test_script = self.data_file('gdb-scripts', 'kernel_gdb_test.py') + + # Instead of launching QEMU via the functional test machinery + # we need to launch it with the gdb run script. We utilise the + # existing prepare_run machinery to build the command line but + # we pass the kernel args separately to avoid horrible string + # escaping. + + self.prepare_run(accel, kernel, disk, drive, dtb, + console_index, append = False) + + # the functional test machinery adds more stuff for handling + # console redirection which we need to do manually. + + qargs = self.vm.args + qargs.extend(["-display", "none", "-serial", "stdio"]) + + # and the machine type + if self.vm._machine is not None: + qargs.extend(["-machine", self.vm._machine]) + + cmd = [ + str(run_script), + "--qemu", self.qemu_bin, + "--qargs", " ".join(qargs), + "--kargs", self.kcmd_line, + "--symbols", str(vmlinux), + "--test", str(test_script), + "--gdb", gdb_path, + ] + + log_cmd = " ".join(f'"{arg}"' if ' ' in arg else arg for arg in cmd) + self.log.info(f"run-test: {log_cmd}") + + res = subprocess.run(cmd, text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + if res.stdout: + console_logger = logging.getLogger('console') + console_logger.info(res.stdout) + + self.assertEqual(res.returncode, 0, + f"run-test.py failed ({res.returncode}):\n") diff --git a/tests/functional/sh4/test_tuxrun.py b/tests/functional/sh4/test_tuxrun.py index 42b503585b2..07c7ee6e03b 100755 --- a/tests/functional/sh4/test_tuxrun.py +++ b/tests/functional/sh4/test_tuxrun.py @@ -31,8 +31,8 @@ def test_sh4(self): # The test is currently too unstable to do much in userspace # so we skip common_tuxrun and do a minimal boot and shutdown. - (kernel, disk, dtb) = self.fetch_tuxrun_assets(self.ASSET_SH4_KERNEL, - self.ASSET_SH4_ROOTFS) + (kernel, disk, dtb, _) = self.fetch_tuxrun_assets(self.ASSET_SH4_KERNEL, + self.ASSET_SH4_ROOTFS) # the console comes on the second serial port self.prepare_run(None, kernel, disk, diff --git a/tests/guest-debug/run-test.py b/tests/guest-debug/run-test.py index e74bb489ee1..48a5261618d 100755 --- a/tests/guest-debug/run-test.py +++ b/tests/guest-debug/run-test.py @@ -26,8 +26,7 @@ def get_args(): parser.add_argument("--qargs", help="Qemu arguments for test") parser.add_argument("--pargs", help="Program arguments for test") parser.add_argument("--kargs", help="Kernel args to --append") - parser.add_argument("--binary", help="Binary to debug", - required=True) + parser.add_argument("--binary", help="Binary to debug") parser.add_argument("--symbols", help="Symbol file for binary to debug") parser.add_argument("--test", help="GDB test script") parser.add_argument('test_args', nargs='*', @@ -86,8 +85,10 @@ def format_cmd(cmd): qemu_cmd.extend(shlex.split(args.qargs)) if "system" in args.qemu: - # for system mode we assume qargs has set-up for binary - qemu_cmd.append(args.binary) + # for system mode we assume qargs has set-up for binary if + # indeed we have set one. + if args.binary: + qemu_cmd.append(args.binary) if not args.no_suspend: qemu_cmd.append("-S") qemu_cmd.extend(["-gdb", f"unix:path={socket_name},server=on"]) @@ -116,7 +117,10 @@ def format_cmd(cmd): inferior = subprocess.Popen(qemu_cmd) # Now launch gdb with our test and collect the result - gdb_cmd = [args.gdb, args.binary] + gdb_cmd = [args.gdb] + + if args.binary: + gdb_cmd.append(args.binary) if args.gdb_args: gdb_cmd.extend(shlex.split(args.gdb_args)) -- 2.47.3
