> 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]>
>
> Message-ID: <[email protected]>
>
> 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 000000000000..2d62fe01f10c
> --- /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
unused variable
> +
> +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
comment is now stale
> +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)
Add wp.delete() in the else branch too?
> +
> +
> +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 ca1589fea37d..8e940fd77383 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'
This was flagged in the v1 review: nokaslr is only needed for the gdb tests (the
kernel_gdb_test.py header says "Address space randomisation (kaslr) needs to be
disabled") but it is added globally here, affecting all tuxrun tests. Mention it
in the commit message or scope it to gdb tests only?
--
Marc-André Lureau <[email protected]>