Skip to content

gdb: fix stop-replies for step, signalled vCont resume, and ctrl-c interrupt - #1654

Open
retrocpugeek wants to merge 3 commits into
qilingframework:devfrom
retrocpugeek:fix/gdb-async-interrupt
Open

gdb: fix stop-replies for step, signalled vCont resume, and ctrl-c interrupt#1654
retrocpugeek wants to merge 3 commits into
qilingframework:devfrom
retrocpugeek:fix/gdb-async-interrupt

Conversation

@retrocpugeek

Copy link
Copy Markdown

Summary

Three related defects in the gdb stub's stop/resume handling, which together make interactive debugging over ql.debugger = True unusable: stepping disconnects the client, resuming with a pending signal is rejected, and a free-running guest cannot be interrupted at all.

Fixes #1538, fixes #1377. Supersedes #1653 (its commit is the first of the three here).

1. handle_s reported SIGTERM on every single step (#1538)

self.gdb.resume_emu(steps=1)
if self.ql.emu_state is QL_STATE.STOPPED:   # always true after a step
    return f'S{SIGTERM:02x}'
return f'S{SIGTRAP:02x}'

Qiling.emu_start unconditionally sets the state to QL_STATE.STOPPED once it has run the requested step count (qiling/core.py:770), so the guard was true on every step and the SIGTRAP branch was dead code. Clients saw a spurious termination signal and dropped the session. Diagnosed in detail by @cbdm in #1377; the widely shared workaround is to comment the two lines out, which then leaves the client unable to detect real termination ("bad exit if you step too much").

handle_s now uses the same exit-vs-trap discrimination handle_c already does: SIGTRAP unless the step carried pc to the emulation exit point, in which case the guest really has terminated and we reply W{exit_code}. The step is also wrapped in handle_c's UcError/KeyboardInterrupt handling so a fault while stepping maps to a signal instead of an unhandled exception, and the duplicated uc-error→signal table is hoisted to a module-level UC_ERROR_SIGMAP.

On #1310 — that check was not arbitrary: it was added in #1322 (commit 5ca0764, "Fix #1310") so that a user hook calling ql.emu_stop() mid-step would be signalled to the client instead of leaving gdb stepping a stopped emulator forever. emu_state cannot express that difference, though, so the check fired on ordinary steps too: it turned a real but narrow bug into a false positive on every step. The exit-point test restores correct behaviour for ordinary steps; a hook-initiated emu_stop is still surfaced on continue (reported as termination), and on step it now reports SIGTRAP. Distinguishing "the host stopped emulation" from "the step completed" needs a stop-reason from the core rather than emu_state, which is deliberately out of scope here — happy to follow up with that if you'd like it in this PR.

2. vCont rejected C/S actions carrying any signal but SIGTRAP (#1377)

The packet #1377 actually fails on is vCont;S0f:pa410.1996;c:pa410.-1 — step-and-deliver-SIGTERM, which gdb sends precisely because of defect 1. handle_v matched only c/C05 and s/S05, so anything else fell through to REPLY_EMPTY and the client aborted with Invalid remote reply:. This is the failure mode @elicn identified in the issue thread, and it is independent of defect 1 — any real fault stop (SIGSEGVvCont;C0b:…) hits it too.

Since we do not deliver host signals to the guest, the signal value carries no meaning for us: any C<sig>/S<sig> is now accepted and carried out as a plain resume or step, which is what the client asked for. This matters more with defect 3 fixed, as a client may resume from a SIGINT stop with C02.

Also drive-by: os.exit_code is now read with getattr(..., 0). Bare-metal os layers (QlOsMcu) do not define it, so the termination path raises AttributeError — the crash shown in #1276. The underlying MCU interrupt handling of #1276 is out of scope.

3. A free-running guest could not be interrupted (ctrl-c)

The stub drives continue by calling emu_start synchronously and only reads the socket again once the target stops on its own, so the bare \x03 break byte a client sends to pause a running target was never seen. A guest that free-runs (an idle loop, an event loop, a hang you want to inspect) could not be interrupted at all — gdb and Ghidra report "Cannot execute this command while the target is running".

The break byte is now polled from the per-instruction run hook, which does run on the emulation thread during emu_start:

  • GdbSerialConn.poll_interrupt() — non-blocking select+recv, true on \x03.
  • QlGdbUtils.dbg_hook — checks an installed check_interrupt callback every INTR_POLL_INTERVAL (200) instructions, so the socket check stays off the hot path; on a break it stops emulation and records why.
  • handle_c — replies S02 (SIGINT) when the stop was an interrupt rather than a breakpoint or a normal exit.

Test

tests/test_debugger.py gains three regression tests (and a small reply-reading client — the existing tests only sent packets and never read replies, which is why all of this went uncaught):

test asserts without the fix
test_gdbdebug_stepi_reports_sigtrap three s in a row all reply S05 S0f (SIGTERM)
test_gdbdebug_vcont_signal_actions vCont? advertises c;C;s;S; the #1377 packet replies S05; vCont;C0f runs to termination (W..) empty replies
test_gdbdebug_async_interrupt an infinite-loop guest is broken out of with \x03 and replies S02 no reply at all; the test stops the guest itself so it fails instead of hanging

Full suite on this branch: 7 tests OK (python -m unittest test_debugger from tests/). Each new test was also confirmed to fail against the unpatched stub.

🤖 Generated with Claude Code

retrocpugeek and others added 3 commits August 7, 2026 22:28
handle_s answered every single-step ('s') with a SIGTERM stop-reply
whenever ql.emu_state was QL_STATE.STOPPED. But emu_start always leaves
the state STOPPED after running the requested step count, so the guard
was true on every step and gdb clients saw a spurious termination signal,
disconnecting mid-debug (issues qilingframework#1377, qilingframework#1538).

Give handle_s the same exit-vs-trap discrimination handle_c already uses:
a step reports SIGTRAP unless it carried pc to the emulation exit point,
in which case the guest has actually terminated and we reply W{exit_code}.
Also wrap the step in the same UcError/KeyboardInterrupt handling as
handle_c so a fault while stepping maps to a signal instead of crashing
the stub, and hoist the shared uc-error->signal map to a module constant.

Add a regression test that single-steps over the gdb stub and asserts the
stop-reply is 'S05' (SIGTRAP), which fails as 'S0f' (SIGTERM) without the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stub drove continue by calling emu_start synchronously and only read
the socket again once the target stopped on its own, so the bare \x03 break
byte a client sends to pause a running target was never seen. A guest that
free-runs (e.g. an idle/event loop) could not be interrupted at all --
gdb/Ghidra reported 'Cannot execute this command while the target is running'.

Poll the client socket for the break byte from the per-instruction run hook
(dbg_hook), which does run on the emulation thread during emu_start:

- GdbSerialConn.poll_interrupt(): non-blocking select+recv, True on \x03.
- QlGdbUtils.dbg_hook: throttled (every INTR_POLL_INTERVAL insns) check of an
  installed check_interrupt callback; on a break, stop emulation and record it.
- handle_c: reply SIGINT when the stop was an interrupt rather than a
  breakpoint or normal exit.

Add a regression test that lets an infinite-loop guest free-run, sends the
bare break byte and asserts the stop-reply is 'S02' (SIGINT). Without the
fix no reply ever arrives, so the test stops the guest itself and fails
rather than hanging the run.

Also verified against a free-running MIPS64 BE guest: \x03 -> S02 in <1ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A client that resumes while a signal is pending sends a vCont action of the
form 'C<sig>' (continue and deliver) or 'S<sig>' (step and deliver), e.g.
'vCont;S0f:pa410.1996;c:pa410.-1' -- the exact packet reported in issue
qilingframework#1377. handle_v matched only 'c'/'C05' and 's'/'S05', so any other signal
fell through to an empty reply and clients aborted the session with
'Invalid remote reply:'.

We do not deliver host signals to the guest, so the signal value carries no
meaning for us: accept any of them and carry the action out as a plain
resume or step, which is what the client asked for. This matters more now
that the stub can stop with SIGINT on an async interrupt, since a client
may well resume from such a stop with 'C02'.

Also stop assuming os.exit_code exists when reporting termination: bare-metal
os layers (QlOsMcu) do not define it, which turns the exit path into an
AttributeError (seen in issue qilingframework#1276). The underlying MCU interrupt handling
of qilingframework#1276 is out of scope here.

Add a regression test asserting the 'qilingframework#1377' packet is answered with a
SIGTRAP stop-reply and that a signalled continue runs the guest to
termination; both replies are empty without the fix.

Fixes qilingframework#1377

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant