gdb: fix stop-replies for step, signalled vCont resume, and ctrl-c interrupt - #1654
Open
retrocpugeek wants to merge 3 commits into
Open
gdb: fix stop-replies for step, signalled vCont resume, and ctrl-c interrupt#1654retrocpugeek wants to merge 3 commits into
retrocpugeek wants to merge 3 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three related defects in the gdb stub's stop/resume handling, which together make interactive debugging over
ql.debugger = Trueunusable: 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_sreported SIGTERM on every single step (#1538)Qiling.emu_startunconditionally sets the state toQL_STATE.STOPPEDonce it has run the requested step count (qiling/core.py:770), so the guard was true on every step and theSIGTRAPbranch 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_snow uses the same exit-vs-trap discriminationhandle_calready does:SIGTRAPunless the step carriedpcto the emulation exit point, in which case the guest really has terminated and we replyW{exit_code}. The step is also wrapped inhandle_c'sUcError/KeyboardInterrupthandling 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-levelUC_ERROR_SIGMAP.On #1310 — that check was not arbitrary: it was added in #1322 (commit
5ca0764, "Fix #1310") so that a user hook callingql.emu_stop()mid-step would be signalled to the client instead of leaving gdb stepping a stopped emulator forever.emu_statecannot 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-initiatedemu_stopis still surfaced oncontinue(reported as termination), and onstepit now reportsSIGTRAP. Distinguishing "the host stopped emulation" from "the step completed" needs a stop-reason from the core rather thanemu_state, which is deliberately out of scope here — happy to follow up with that if you'd like it in this PR.2.
vContrejectedC/Sactions 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_vmatched onlyc/C05ands/S05, so anything else fell through toREPLY_EMPTYand the client aborted withInvalid remote reply:. This is the failure mode @elicn identified in the issue thread, and it is independent of defect 1 — any real fault stop (SIGSEGV→vCont;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 withC02.Also drive-by:
os.exit_codeis now read withgetattr(..., 0). Bare-metal os layers (QlOsMcu) do not define it, so the termination path raisesAttributeError— 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_startsynchronously and only reads the socket again once the target stops on its own, so the bare\x03break 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-blockingselect+recv, true on\x03.QlGdbUtils.dbg_hook— checks an installedcheck_interruptcallback everyINTR_POLL_INTERVAL(200) instructions, so the socket check stays off the hot path; on a break it stops emulation and records why.handle_c— repliesS02(SIGINT) when the stop was an interrupt rather than a breakpoint or a normal exit.Test
tests/test_debugger.pygains 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_gdbdebug_stepi_reports_sigtrapsin a row all replyS05S0f(SIGTERM)test_gdbdebug_vcont_signal_actionsvCont?advertisesc;C;s;S; the #1377 packet repliesS05;vCont;C0fruns to termination (W..)test_gdbdebug_async_interrupt\x03and repliesS02Full suite on this branch:
7 tests OK(python -m unittest test_debuggerfromtests/). Each new test was also confirmed to fail against the unpatched stub.🤖 Generated with Claude Code