gdb: report SIGTRAP (not SIGTERM) on single-step

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 #1377, #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 <[email protected]>
This commit is contained in:
retrocpugeek
2026-08-07 22:28:08 +10:00
co-authored by Claude Opus 4.8
parent 499218bd1a
commit de34db212d
2 changed files with 119 additions and 19 deletions
+33 -19
View File
@@ -30,7 +30,7 @@ from unicorn.unicorn_const import (
)
from qiling import Qiling
from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS, QL_STATE
from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS
from qiling.debugger import QlDebugger
from qiling.debugger.gdb.xmlregs import QlGdbFeatures
from qiling.debugger.gdb.utils import QlGdbUtils
@@ -56,6 +56,21 @@ SIGCHLD = 16
SIGCONT = 17
SIGSTOP = 18
# translate a unicorn cpu fault into the closest posix signal, shared by the
# continue ('c') and single-step ('s') handlers when emulation faults
UC_ERROR_SIGMAP = {
UC_ERR_READ_UNMAPPED : SIGSEGV,
UC_ERR_WRITE_UNMAPPED : SIGSEGV,
UC_ERR_FETCH_UNMAPPED : SIGSEGV,
UC_ERR_WRITE_PROT : SIGSEGV,
UC_ERR_READ_PROT : SIGSEGV,
UC_ERR_FETCH_PROT : SIGSEGV,
UC_ERR_READ_UNALIGNED : SIGBUS,
UC_ERR_WRITE_UNALIGNED : SIGBUS,
UC_ERR_FETCH_UNALIGNED : SIGBUS,
UC_ERR_INSN_INVALID : SIGILL
}
# common replies
REPLY_ACK = b'+'
REPLY_EMPTY = b''
@@ -226,21 +241,8 @@ class QlGdb(QlDebugger):
try:
self.gdb.resume_emu()
except UcError as err:
sigmap = {
UC_ERR_READ_UNMAPPED : SIGSEGV,
UC_ERR_WRITE_UNMAPPED : SIGSEGV,
UC_ERR_FETCH_UNMAPPED : SIGSEGV,
UC_ERR_WRITE_PROT : SIGSEGV,
UC_ERR_READ_PROT : SIGSEGV,
UC_ERR_FETCH_PROT : SIGSEGV,
UC_ERR_READ_UNALIGNED : SIGBUS,
UC_ERR_WRITE_UNALIGNED : SIGBUS,
UC_ERR_FETCH_UNALIGNED : SIGBUS,
UC_ERR_INSN_INVALID : SIGILL
}
# determine signal from uc error; default to SIGTERM
reply = f'S{sigmap.get(err.errno, SIGTERM):02x}'
reply = f'S{UC_ERROR_SIGMAP.get(err.errno, SIGTERM):02x}'
except KeyboardInterrupt:
# emulation was interrupted with ctrl+c
@@ -678,11 +680,23 @@ class QlGdb(QlDebugger):
"""Perform a single step.
"""
self.gdb.resume_emu(steps=1)
try:
self.gdb.resume_emu(steps=1)
except UcError as err:
# stepping faulted; report the closest posix signal
return f'S{UC_ERROR_SIGMAP.get(err.errno, SIGTERM):02x}'
# if emulation has been stopped, signal program termination
if self.ql.emu_state is QL_STATE.STOPPED:
return f'S{SIGTERM:02x}'
except KeyboardInterrupt:
# emulation was interrupted with ctrl+c
return f'S{SIGINT:02x}'
# emu_start always leaves emu_state as STOPPED after a step, so that
# cannot tell an ordinary step apart from the guest exiting (see
# issues #1377 and #1538). instead, the guest has terminated only
# when the step carried pc all the way to the emulation exit point.
if getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.exit_point:
# program terminated; report its exit code
return f'W{self.ql.os.exit_code:02x}'
# otherwise, this is just single stepping
return f'S{SIGTRAP:02x}'
+86
View File
@@ -44,6 +44,55 @@ class SimpleGdbClient:
self.__file.flush()
class ReadingGdbClient:
"""A minimal gdb remote client that can also read replies, so tests can
assert on the stop-reply packets the server sends back.
"""
def __init__(self, host: str, port: int):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.settimeout(10)
self.__sock = sock
self.__buf = b''
def __enter__(self):
return self
def __exit__(self, ex_type, ex_value, ex_traceback):
self.__sock.close()
@staticmethod
def checksum(data: str) -> int:
return sum(ord(c) for c in data) & 0xff
def send(self, msg: str):
self.__sock.sendall(f'${msg}#{ReadingGdbClient.checksum(msg):02x}'.encode('latin'))
def read_packet(self) -> str:
"""Read a single '$<data>#<checksum>' reply, skipping any '+'/'-' acks.
"""
# wait for a packet start marker
while b'$' not in self.__buf:
self.__buf += self.__sock.recv(4096)
start = self.__buf.index(b'$')
# wait until the terminating '#' and its two checksum digits arrived
while True:
end = self.__buf.find(b'#', start)
if end != -1 and len(self.__buf) >= end + 3:
break
self.__buf += self.__sock.recv(4096)
data = self.__buf[start + 1:end]
self.__buf = self.__buf[end + 3:]
return data.decode('latin')
class DebuggerTest(unittest.TestCase):
def test_gdbdebug_file_server(self):
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.DEBUG)
@@ -168,6 +217,43 @@ class DebuggerTest(unittest.TestCase):
ql.run()
del ql
def test_gdbdebug_stepi_reports_sigtrap(self):
# regression for issues #1377 and #1538: a single-step ('s') used to be
# answered with a SIGTERM stop-reply because emu_state is always STOPPED
# after a step, which made gdb clients believe the program had died. the
# stop-reply for an ordinary step must be a SIGTRAP ('S05'), never a
# SIGTERM ('S0f').
ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF)
ql.debugger = 'gdb:127.0.0.1:9996'
replies = []
def gdb_test_client():
# yield to allow ql to launch its gdbserver
time.sleep(1.337 * 2)
with ReadingGdbClient('127.0.0.1', 9996) as client:
client.send('qSupported:multiprocess+;swbreak+;hwbreak+;vContSupported+;xmlRegisters=i386')
client.read_packet()
client.send('QStartNoAckMode')
client.read_packet()
# step a few instructions; every reply must be a SIGTRAP stop
for _ in range(3):
client.send('s')
replies.append(client.read_packet())
client.send('k')
thread = threading.Thread(target=gdb_test_client, daemon=True)
thread.start()
ql.run()
thread.join(timeout=30)
del ql
self.assertEqual(replies, ['S05', 'S05', 'S05'])
def test_gdbdebug_shellcode_server(self):
X8664_LIN = bytes.fromhex('31c048bbd19d9691d08c97ff48f7db53545f995257545eb03b0f05')