mirror of
https://github.com/qilingframework/qiling.git
synced 2026-10-12 02:21:40 +00:00
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:
co-authored by
Claude Opus 4.8
parent
499218bd1a
commit
de34db212d
+33
-19
@@ -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}'
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user