While working on Raspberry Pi Pico/RP2040 support, I noticed that Pico SDK programs deliberately execute BKPT from _exit(). On real hardware this is useful when a debug probe is attached: returning from main() stops the debugger at the program exit point.
Under QEMU this currently does not behave like the debug-probe case. For M-profile guests, the BKPT instruction is routed through the architectural guest debug exception path. Without halting debug, that path should remain a guest-visible DebugMonitor exception when DebugMonitor is enabled, or escalate towards HardFault otherwise. This patch deliberately leaves that no-debugger architectural path unchanged. The problem addressed here is the case where GDB is connected to QEMU's gdbstub. In that situation, firmware that uses BKPT as a debugger stop point should stop in the attached debugger. Instead, the M-profile guest currently continues down the guest exception path and may end in HardFault/lockup rather than reporting a clean trap to GDB. This is not specific to the RP2040 machine model. It is a generic Cortex-M/gdbstub interaction: firmware that uses BKPT as a debugger stop point should be reported to the attached debugger, while keeping the guest architectural exception path when no debugger is attached. Expose a small gdbstub helper to test whether a CPU is visible to an attached debugger. When an M-profile BKPT instruction is executed with such a debugger attached, leave the translated block with EXCP_DEBUG so the existing gdbstub stop path reports a trap to GDB. Signed-off-by: Gilles Grimaud <[email protected]> --- gdbstub/gdbstub.c | 13 ++++++++++++- include/exec/gdbstub.h | 6 ++++++ target/arm/tcg/debug.c | 9 +++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/gdbstub/gdbstub.c b/gdbstub/gdbstub.c index c3c944e965..9f259fc005 100644 --- a/gdbstub/gdbstub.c +++ b/gdbstub/gdbstub.c @@ -237,6 +237,18 @@ static GDBProcess *gdb_get_cpu_process(CPUState *cpu) return gdb_get_process(gdb_get_cpu_pid(cpu)); } +bool gdb_cpu_is_attached(CPUState *cpu) +{ + GDBProcess *process; + + if (!gdbserver_state.init || !cpu) { + return false; + } + + process = gdb_get_cpu_process(cpu); + return process && process->attached; +} + static CPUState *find_cpu(uint32_t thread_id) { CPUState *cpu; @@ -2520,4 +2532,3 @@ void gdb_create_default_process(GDBState *s) process->attached = false; process->target_xml = NULL; } - diff --git a/include/exec/gdbstub.h b/include/exec/gdbstub.h index 75eb4d9c36..6e5f75a1f3 100644 --- a/include/exec/gdbstub.h +++ b/include/exec/gdbstub.h @@ -59,6 +59,12 @@ void gdb_unregister_coprocessor_all(CPUState *cpu); */ bool gdbserver_start(const char *port_or_device, Error **errp); +/** + * gdb_cpu_is_attached() - return whether @cpu is visible to an attached GDB + * @cpu: The CPU to test. + */ +bool gdb_cpu_is_attached(CPUState *cpu); + /** * gdb_feature_builder_init() - Initialize GDBFeatureBuilder. * @builder: The builder to be initialized. diff --git a/target/arm/tcg/debug.c b/target/arm/tcg/debug.c index 07a52643e7..2c60f1c49f 100644 --- a/target/arm/tcg/debug.c +++ b/target/arm/tcg/debug.c @@ -12,7 +12,9 @@ #include "internals.h" #include "cpu-features.h" #include "cpregs.h" +#include "accel/tcg/cpu-loop.h" #include "exec/watchpoint.h" +#include "exec/gdbstub.h" #include "system/tcg.h" /* Return the Exception Level targeted by debug exceptions. */ @@ -513,6 +515,7 @@ void arm_debug_excp_handler(CPUState *cs) */ void HELPER(exception_bkpt_insn)(CPUARMState *env, uint32_t syndrome) { + CPUState *cs = env_cpu(env); int debug_el = arm_debug_target_el(env); int cur_el = arm_current_el(env); @@ -535,6 +538,12 @@ void HELPER(exception_bkpt_insn)(CPUARMState *env, uint32_t syndrome) if (debug_el < cur_el) { debug_el = cur_el; } + + if (arm_feature(env, ARM_FEATURE_M) && gdb_cpu_is_attached(cs)) { + cs->exception_index = EXCP_DEBUG; + cpu_loop_exit(cs); + } + raise_exception(env, EXCP_BKPT, syndrome, debug_el); } -- 2.55.0
