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 - 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 Message-ID: <[email protected]> Signed-off-by: Alex Bennée <[email protected]> --- 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 | 174 ++++++++++++++++++ tests/functional/qemu_test/tuxruntest.py | 96 +++++++++- 2 files changed, 261 insertions(+), 9 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..2d62fe01f10 --- /dev/null +++ b/tests/functional/gdb-scripts/kernel_gdb_test.py @@ -0,0 +1,174 @@ +# 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 + +failcount = 0 + +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 + + +# We need to do hbreak manually as the python interface doesn't export it +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 get_wpstr(self, sym_name): + "Setup sym and wp_str for given symbol." + self.sym, ok = gdb.lookup_symbol(sym_name) + wp_addr = gdb.parse_and_eval(sym_name).address + self.wp_str = '*(%(type)s)(&%(address)s)' % dict( + type = wp_addr.type, address = sym_name) + + return(self.wp_str) + + def __init__(self, sym_name, type): + wp_str = self.get_wpstr(sym_name) + super(WatchPoint, self).__init__(wp_str, 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 (%s)" % (text, sym, wp.sym.value()) + + if wp.hit_count > 0: + report(True, report_str) + wp.delete() + else: + report(False, report_str) + + +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..8e940fd7738 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,7 @@ class TuxRunBaselineTest(QemuSystemTest): - KERNEL_COMMON_COMMAND_LINE = 'printk.time=0' + KERNEL_COMMON_COMMAND_LINE = 'printk.time=0 nokaslr' # Tests are ~10-40s, allow for --debug/--enable-gcov overhead timeout = 100 @@ -55,17 +57,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 +88,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 +143,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, + "--binary", "", + "--symbols", str(vmlinux), + "--test", str(test_script), + "--gdb", gdb_path, + ] + + self.log.info(f"run-test: {' '.join(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") -- 2.47.3
