diff --git a/qiling/debugger/gdb/gdb.py b/qiling/debugger/gdb/gdb.py index c6f591a5..77c0623f 100644 --- a/qiling/debugger/gdb/gdb.py +++ b/qiling/debugger/gdb/gdb.py @@ -261,8 +261,9 @@ class QlGdb(QlDebugger): # emulation stopped because it hit a breakpoint reply = f'S{SIGTRAP:02x}' else: - # emulation has completed successfully - reply = f'W{self.ql.os.exit_code:02x}' + # emulation has completed successfully. note bare-metal os layers + # do not have an exit code (issue #1276) + reply = f'W{getattr(self.ql.os, "exit_code", 0):02x}' return reply @@ -672,10 +673,17 @@ class QlGdb(QlDebugger): for grp in groups: cmd, *tid = grp.split(':', maxsplit=1) - if cmd in ('c', f'C{SIGTRAP:02x}'): + # 'C sig' and 'S sig' resume or step while delivering a signal + # to the guest. we do not deliver signals, so the signal value + # is ignored and the action is carried out as a plain resume or + # step. matching only 'C05' and 'S05' here made clients that + # resume with any other pending signal (e.g. 'S0f' after a stop + # reply we sent) receive an empty reply and bail out with + # 'Invalid remote reply' (issue #1377) + if cmd[:1] in ('c', 'C'): return handle_c('') - elif cmd in ('s', f'S{SIGTRAP:02x}'): + elif cmd[:1] in ('s', 'S'): return handle_s('') # FIXME: not sure how to handle multiple command @@ -704,7 +712,7 @@ class QlGdb(QlDebugger): # 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}' + return f'W{getattr(self.ql.os, "exit_code", 0):02x}' # otherwise, this is just single stepping return f'S{SIGTRAP:02x}' diff --git a/tests/test_debugger.py b/tests/test_debugger.py index 6219cca4..17ae7b8d 100644 --- a/tests/test_debugger.py +++ b/tests/test_debugger.py @@ -260,6 +260,55 @@ class DebuggerTest(unittest.TestCase): self.assertEqual(replies, ['S05', 'S05', 'S05']) + def test_gdbdebug_vcont_signal_actions(self): + # regression for issue #1377: a client resuming with a pending signal sends + # a 'vCont;S' or 'vCont;C' action (e.g. 'S0f' after a stop-reply we + # sent). the stub only recognized 'S05' and 'C05', replied empty to anything + # else, and the client bailed out with 'Invalid remote reply'. any signal must + # be accepted and carried out as a plain step or resume. + ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_hello"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.OFF) + ql.debugger = 'gdb:127.0.0.1:9995' + + replies = [] + + def gdb_test_client(): + # yield to allow ql to launch its gdbserver + time.sleep(1.337 * 2) + + with ReadingGdbClient('127.0.0.1', 9995) as client: + client.send('qSupported:multiprocess+;swbreak+;hwbreak+;vContSupported+;xmlRegisters=i386') + client.read_packet() + client.send('QStartNoAckMode') + client.read_packet() + + client.send('vCont?') + replies.append(client.read_packet()) + + # step while delivering SIGTERM; this is the packet reported in #1377 + client.send('vCont;S0f:p1.1;c:p1.-1') + replies.append(client.read_packet()) + + # resume while delivering SIGTERM; runs the guest to completion + client.send('vCont;C0f:p1.1') + 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[0], 'vCont;c;C;s;S') + + # the signalled step is an ordinary step: SIGTRAP, not an empty reply + self.assertEqual(replies[1], 'S05') + + # the signalled resume ran to termination and reported an exit code + self.assertTrue(replies[2].startswith('W'), f'unexpected reply to signalled resume: {replies[2]!r}') + def test_gdbdebug_async_interrupt(self): # a free-running guest could not be interrupted at all: the stub is blocked # inside emu_start while the target runs, so the bare '\x03' break byte a