Skip to content

Commit 83c5b1b

Browse files
authored
Merge pull request #1654 from retrocpugeek/fix/gdb-async-interrupt
gdb: fix stop-replies for step, signalled vCont resume, and ctrl-c interrupt
2 parents 499218b + a13a8c8 commit 83c5b1b

3 files changed

Lines changed: 300 additions & 24 deletions

File tree

qiling/debugger/gdb/gdb.py

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import os
1717
import socket
18+
import select
1819
import re
1920
import tempfile
2021
from functools import partial
@@ -30,7 +31,7 @@
3031
)
3132

3233
from qiling import Qiling
33-
from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS, QL_STATE
34+
from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS
3435
from qiling.debugger import QlDebugger
3536
from qiling.debugger.gdb.xmlregs import QlGdbFeatures
3637
from qiling.debugger.gdb.utils import QlGdbUtils
@@ -56,6 +57,21 @@
5657
SIGCONT = 17
5758
SIGSTOP = 18
5859

60+
# translate a unicorn cpu fault into the closest posix signal, shared by the
61+
# continue ('c') and single-step ('s') handlers when emulation faults
62+
UC_ERROR_SIGMAP = {
63+
UC_ERR_READ_UNMAPPED : SIGSEGV,
64+
UC_ERR_WRITE_UNMAPPED : SIGSEGV,
65+
UC_ERR_FETCH_UNMAPPED : SIGSEGV,
66+
UC_ERR_WRITE_PROT : SIGSEGV,
67+
UC_ERR_READ_PROT : SIGSEGV,
68+
UC_ERR_FETCH_PROT : SIGSEGV,
69+
UC_ERR_READ_UNALIGNED : SIGBUS,
70+
UC_ERR_WRITE_UNALIGNED : SIGBUS,
71+
UC_ERR_FETCH_UNALIGNED : SIGBUS,
72+
UC_ERR_INSN_INVALID : SIGILL
73+
}
74+
5975
# common replies
6076
REPLY_ACK = b'+'
6177
REPLY_EMPTY = b''
@@ -124,6 +140,10 @@ def run(self):
124140
server = GdbSerialConn(self.ip, self.port, self.ql.log)
125141
killed = False
126142

143+
# let the run hook break into a free-running guest when the client sends
144+
# an async interrupt (ctrl-c / \x03); see QlGdbUtils.dbg_hook.
145+
self.gdb.check_interrupt = server.poll_interrupt
146+
127147
def __hexstr(value: int, nibbles: int = 0) -> str:
128148
"""Encode a value into a hex string.
129149
"""
@@ -226,33 +246,24 @@ def handle_c(subcmd: str) -> Reply:
226246
try:
227247
self.gdb.resume_emu()
228248
except UcError as err:
229-
sigmap = {
230-
UC_ERR_READ_UNMAPPED : SIGSEGV,
231-
UC_ERR_WRITE_UNMAPPED : SIGSEGV,
232-
UC_ERR_FETCH_UNMAPPED : SIGSEGV,
233-
UC_ERR_WRITE_PROT : SIGSEGV,
234-
UC_ERR_READ_PROT : SIGSEGV,
235-
UC_ERR_FETCH_PROT : SIGSEGV,
236-
UC_ERR_READ_UNALIGNED : SIGBUS,
237-
UC_ERR_WRITE_UNALIGNED : SIGBUS,
238-
UC_ERR_FETCH_UNALIGNED : SIGBUS,
239-
UC_ERR_INSN_INVALID : SIGILL
240-
}
241-
242249
# determine signal from uc error; default to SIGTERM
243-
reply = f'S{sigmap.get(err.errno, SIGTERM):02x}'
250+
reply = f'S{UC_ERROR_SIGMAP.get(err.errno, SIGTERM):02x}'
244251

245252
except KeyboardInterrupt:
246253
# emulation was interrupted with ctrl+c
247254
reply = f'S{SIGINT:02x}'
248255

249256
else:
250-
if getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.last_bp:
257+
if self.gdb.interrupted:
258+
# emulation was stopped by an async interrupt from the client
259+
reply = f'S{SIGINT:02x}'
260+
elif getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.last_bp:
251261
# emulation stopped because it hit a breakpoint
252262
reply = f'S{SIGTRAP:02x}'
253263
else:
254-
# emulation has completed successfully
255-
reply = f'W{self.ql.os.exit_code:02x}'
264+
# emulation has completed successfully. note bare-metal os layers
265+
# do not have an exit code (issue #1276)
266+
reply = f'W{getattr(self.ql.os, "exit_code", 0):02x}'
256267

257268
return reply
258269

@@ -662,10 +673,17 @@ def handle_v(subcmd: str) -> Reply:
662673
for grp in groups:
663674
cmd, *tid = grp.split(':', maxsplit=1)
664675

665-
if cmd in ('c', f'C{SIGTRAP:02x}'):
676+
# 'C sig' and 'S sig' resume or step while delivering a signal
677+
# to the guest. we do not deliver signals, so the signal value
678+
# is ignored and the action is carried out as a plain resume or
679+
# step. matching only 'C05' and 'S05' here made clients that
680+
# resume with any other pending signal (e.g. 'S0f' after a stop
681+
# reply we sent) receive an empty reply and bail out with
682+
# 'Invalid remote reply' (issue #1377)
683+
if cmd[:1] in ('c', 'C'):
666684
return handle_c('')
667685

668-
elif cmd in ('s', f'S{SIGTRAP:02x}'):
686+
elif cmd[:1] in ('s', 'S'):
669687
return handle_s('')
670688

671689
# FIXME: not sure how to handle multiple command
@@ -678,11 +696,23 @@ def handle_s(subcmd: str) -> Reply:
678696
"""Perform a single step.
679697
"""
680698

681-
self.gdb.resume_emu(steps=1)
699+
try:
700+
self.gdb.resume_emu(steps=1)
701+
except UcError as err:
702+
# stepping faulted; report the closest posix signal
703+
return f'S{UC_ERROR_SIGMAP.get(err.errno, SIGTERM):02x}'
704+
705+
except KeyboardInterrupt:
706+
# emulation was interrupted with ctrl+c
707+
return f'S{SIGINT:02x}'
682708

683-
# if emulation has been stopped, signal program termination
684-
if self.ql.emu_state is QL_STATE.STOPPED:
685-
return f'S{SIGTERM:02x}'
709+
# emu_start always leaves emu_state as STOPPED after a step, so that
710+
# cannot tell an ordinary step apart from the guest exiting (see
711+
# issues #1377 and #1538). instead, the guest has terminated only
712+
# when the step carried pc all the way to the emulation exit point.
713+
if getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.exit_point:
714+
# program terminated; report its exit code
715+
return f'W{getattr(self.ql.os, "exit_code", 0):02x}'
686716

687717
# otherwise, this is just single stepping
688718
return f'S{SIGTRAP:02x}'
@@ -823,6 +853,27 @@ def close(self):
823853
self.client.close()
824854
self.sock.close()
825855

856+
def poll_interrupt(self) -> bool:
857+
"""Non-blocking check for an async interrupt from the client.
858+
859+
While the target is running the only thing gdb sends is a bare ``\\x03``
860+
break byte (it waits for a stop reply before sending anything else), so
861+
any readable bytes here are that interrupt or a stray protocol ack.
862+
Returns True if a break was seen. Called from the run hook, so it must
863+
never block.
864+
"""
865+
866+
readable, _, _ = select.select([self.client], [], [], 0)
867+
if not readable:
868+
return False
869+
870+
try:
871+
incoming = self.client.recv(self.BUFSIZE)
872+
except (ConnectionError, OSError):
873+
return False
874+
875+
return b'\x03' in incoming
876+
826877
def readpackets(self) -> Iterator[bytes]:
827878
"""Iterate through incoming packets in an active connection until
828879
it is terminated.

qiling/debugger/gdb/utils.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,26 @@
1414

1515

1616
class QlGdbUtils:
17+
# how often (in guest instructions) the run hook polls the client socket for
18+
# an async interrupt. small enough to feel instant, large enough that the
19+
# extra non-blocking socket check does not dominate the per-instruction hook.
20+
INTR_POLL_INTERVAL = 200
21+
1722
def __init__(self, ql: Qiling, entry_point: int, exit_point: int):
1823
self.ql = ql
1924

2025
self.exit_point = exit_point
2126
self.swbp = set()
2227
self.last_bp = None
2328

29+
# async-interrupt support: `check_interrupt` is a callable installed by the
30+
# gdb stub that returns True when the client sent a break (ctrl-c / \x03)
31+
# while the target was running. `interrupted` records that the last resume
32+
# stopped for that reason (rather than a breakpoint or normal exit).
33+
self.check_interrupt = None
34+
self.interrupted = False
35+
self._poll_counter = 0
36+
2437
def __entry_point_hook(ql: Qiling):
2538
ql.hook_del(ep_hret)
2639
ql.hook_code(self.dbg_hook)
@@ -36,6 +49,24 @@ def dbg_hook(self, ql: Qiling, address: int, size: int):
3649
if getattr(ql.arch, 'is_thumb', False):
3750
address |= 1
3851

52+
# poll for an async interrupt from the client (gdb sends a bare \x03 while
53+
# the target is running). throttled so the socket check stays off the hot
54+
# path. this is the only way to break into a free-running guest, since the
55+
# stub's packet loop is blocked inside emu_start until the target stops.
56+
if self.check_interrupt is not None:
57+
self._poll_counter += 1
58+
59+
if self._poll_counter >= self.INTR_POLL_INTERVAL:
60+
self._poll_counter = 0
61+
62+
if self.check_interrupt():
63+
self.interrupted = True
64+
self.last_bp = None
65+
66+
ql.log.info(f'{PROMPT} interrupted by client, stopped at {address:#x}')
67+
ql.stop()
68+
return
69+
3970
# resuming emulation after hitting a breakpoint will re-enter this hook.
4071
# avoid an endless hooking loop by detecting and skipping this case
4172
if address == self.last_bp:
@@ -83,4 +114,8 @@ def resume_emu(self, address: Optional[int] = None, steps: int = 0):
83114
op = f'stepping {steps} instructions' if steps else 'resuming'
84115
self.ql.log.info(f'{PROMPT} {op} from {address:#x}')
85116

117+
# clear any pending interrupt state from a previous resume
118+
self.interrupted = False
119+
self._poll_counter = 0
120+
86121
self.ql.emu_start(address, self.exit_point, count=steps)

0 commit comments

Comments
 (0)