Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 75 additions & 24 deletions qiling/debugger/gdb/gdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import os
import socket
import select
import re
import tempfile
from functools import partial
Expand All @@ -30,7 +31,7 @@
)

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
Expand All @@ -56,6 +57,21 @@
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''
Expand Down Expand Up @@ -124,6 +140,10 @@ def run(self):
server = GdbSerialConn(self.ip, self.port, self.ql.log)
killed = False

# let the run hook break into a free-running guest when the client sends
# an async interrupt (ctrl-c / \x03); see QlGdbUtils.dbg_hook.
self.gdb.check_interrupt = server.poll_interrupt

def __hexstr(value: int, nibbles: int = 0) -> str:
"""Encode a value into a hex string.
"""
Expand Down Expand Up @@ -226,33 +246,24 @@ def handle_c(subcmd: str) -> Reply:
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
reply = f'S{SIGINT:02x}'

else:
if getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.last_bp:
if self.gdb.interrupted:
# emulation was stopped by an async interrupt from the client
reply = f'S{SIGINT:02x}'
elif getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.last_bp:
# 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

Expand Down Expand Up @@ -662,10 +673,17 @@ def handle_v(subcmd: str) -> Reply:
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
Expand All @@ -678,11 +696,23 @@ def handle_s(subcmd: str) -> Reply:
"""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}'

except KeyboardInterrupt:
# emulation was interrupted with ctrl+c
return f'S{SIGINT:02x}'

# if emulation has been stopped, signal program termination
if self.ql.emu_state is QL_STATE.STOPPED:
return f'S{SIGTERM: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{getattr(self.ql.os, "exit_code", 0):02x}'

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

def poll_interrupt(self) -> bool:
"""Non-blocking check for an async interrupt from the client.

While the target is running the only thing gdb sends is a bare ``\\x03``
break byte (it waits for a stop reply before sending anything else), so
any readable bytes here are that interrupt or a stray protocol ack.
Returns True if a break was seen. Called from the run hook, so it must
never block.
"""

readable, _, _ = select.select([self.client], [], [], 0)
if not readable:
return False

try:
incoming = self.client.recv(self.BUFSIZE)
except (ConnectionError, OSError):
return False

return b'\x03' in incoming

def readpackets(self) -> Iterator[bytes]:
"""Iterate through incoming packets in an active connection until
it is terminated.
Expand Down
35 changes: 35 additions & 0 deletions qiling/debugger/gdb/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,26 @@


class QlGdbUtils:
# how often (in guest instructions) the run hook polls the client socket for
# an async interrupt. small enough to feel instant, large enough that the
# extra non-blocking socket check does not dominate the per-instruction hook.
INTR_POLL_INTERVAL = 200

def __init__(self, ql: Qiling, entry_point: int, exit_point: int):
self.ql = ql

self.exit_point = exit_point
self.swbp = set()
self.last_bp = None

# async-interrupt support: `check_interrupt` is a callable installed by the
# gdb stub that returns True when the client sent a break (ctrl-c / \x03)
# while the target was running. `interrupted` records that the last resume
# stopped for that reason (rather than a breakpoint or normal exit).
self.check_interrupt = None
self.interrupted = False
self._poll_counter = 0

def __entry_point_hook(ql: Qiling):
ql.hook_del(ep_hret)
ql.hook_code(self.dbg_hook)
Expand All @@ -36,6 +49,24 @@ def dbg_hook(self, ql: Qiling, address: int, size: int):
if getattr(ql.arch, 'is_thumb', False):
address |= 1

# poll for an async interrupt from the client (gdb sends a bare \x03 while
# the target is running). throttled so the socket check stays off the hot
# path. this is the only way to break into a free-running guest, since the
# stub's packet loop is blocked inside emu_start until the target stops.
if self.check_interrupt is not None:
self._poll_counter += 1

if self._poll_counter >= self.INTR_POLL_INTERVAL:
self._poll_counter = 0

if self.check_interrupt():
self.interrupted = True
self.last_bp = None

ql.log.info(f'{PROMPT} interrupted by client, stopped at {address:#x}')
ql.stop()
return

# resuming emulation after hitting a breakpoint will re-enter this hook.
# avoid an endless hooking loop by detecting and skipping this case
if address == self.last_bp:
Expand Down Expand Up @@ -83,4 +114,8 @@ def resume_emu(self, address: Optional[int] = None, steps: int = 0):
op = f'stepping {steps} instructions' if steps else 'resuming'
self.ql.log.info(f'{PROMPT} {op} from {address:#x}')

# clear any pending interrupt state from a previous resume
self.interrupted = False
self._poll_counter = 0

self.ql.emu_start(address, self.exit_point, count=steps)
Loading