Move addition of custom commands to the Debugger-agnostic API (#2254)

pull/2255/head
Matt 1 year ago committed by GitHub
parent f2bdc4c755
commit 0355804908
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -90,7 +90,7 @@ GDB_BUILTIN_COMMANDS = list_current_commands()
pwndbg_is_reloading = getattr(gdb, "pwndbg_is_reloading", False)
class Command(gdb.Command):
class Command:
"""Generic command wrapper"""
builtin_override_whitelist: Set[str] = {"up", "down", "search", "pwd", "start", "ignore"}
@ -114,7 +114,10 @@ class Command(gdb.Command):
if command_name is None:
command_name = function.__name__
super().__init__(command_name, gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION, prefix=prefix)
def _handler(_debugger, arguments, is_interactive):
self.invoke(arguments, is_interactive)
self.handle = pwndbg.dbg.add_command(command_name, _handler)
self.function = function
if command_name in command_names:
@ -141,7 +144,7 @@ class Command(gdb.Command):
A ``(tuple, dict)``, in the form of ``*args, **kwargs``.
The contents of the tuple/dict are undefined.
"""
return gdb.string_to_argv(argument), {}
return pwndbg.dbg.lex_args(argument), {}
def invoke(self, argument: str, from_tty: bool) -> None:
"""Invoke the command with an argument string"""
@ -170,23 +173,13 @@ class Command(gdb.Command):
if not from_tty:
return False
lines = gdb.execute("show commands", from_tty=False, to_string=True)
lines = lines.splitlines()
last_line = pwndbg.dbg.history(1)
# No history
if not lines:
return False
last_line = lines[-1]
number_str, command = last_line.split(maxsplit=1)
try:
number = int(number_str)
except ValueError:
# In rare cases GDB will output a warning after executing `show commands`
# (i.e. "warning: (Internal error: pc 0x0 in read in CU, but not in
# symtab.)").
if not last_line:
return False
number, command = last_line[-1]
# A new command was entered by the user
if number not in Command.history:
Command.history[number] = command
@ -547,7 +540,7 @@ class _ArgparsedCommand(Command):
)
def split_args(self, argument: str):
argv = gdb.string_to_argv(argument)
argv = pwndbg.dbg.lex_args(argument)
return (), vars(self.parser.parse_args(argv))

@ -5,31 +5,74 @@ The abstracted debugger interface.
from __future__ import annotations
from typing import Any
from typing import Callable
from typing import List
from typing import Tuple
dbg: Debugger = None
class CommandHandle:
"""
An opaque handle to an installed command.
"""
def remove(self) -> None:
"""
Removes this command from the command palette of the debugger.
"""
raise NotImplementedError()
class Debugger:
"""
The base class
The base class representing a debugger.
"""
def setup(self, *args: Any) -> None:
"""
Perform debugger-specific initialization.
This method should be run immediately after `pwndbg.dbg` is set to an
instance of this class, and, as such, is allowed to run code that
depends on it being set.
Because we can't really know what a given debugger object will need as
part of its setup process, we allow for as many arguments as desired to
be passed in, and leave it up to the implementations to decide what they
need.
This shouldn't be a problem, seeing as, unlike other methods in this
class, this should only be called as part of the debugger-specific
need. This shouldn't be a problem, seeing as, unlike other methods in
this class, this should only be called as part of the debugger-specific
bringup code.
"""
raise NotImplementedError()
def history(self, last: int = 10) -> List[Tuple[int, str]]:
"""
The command history of the interactive session in this debugger.
This function returns the last `last` items in the command history, as
an oldest-to-youngest-sorted list of tuples, where the first element in
each tuple is the index of the command in the history, and the second
element is a string giving the command itself.
"""
raise NotImplementedError()
def lex_args(self, command_line: str) -> List[str]:
"""
Lexes the given command line into a list of arguments, according to the
conventions of the debugger being used and of the interactive session.
"""
raise NotImplementedError()
def add_command(
self, name: str, handler: Callable[[Debugger, str, bool], None]
) -> CommandHandle:
"""
Adds a command with the given name to the debugger, that invokes the
given function every time it is called.
"""
raise NotImplementedError()
# WARNING
#
# These are hacky parts of the API that were strictly necessary to bring up

@ -2,9 +2,11 @@ from __future__ import annotations
import signal
from typing import Any
from typing import List
from typing import Tuple
import gdb
from typing_extensions import Callable
from typing_extensions import override
import pwndbg
@ -13,7 +15,30 @@ import pwndbg.gdblib
from pwndbg.commands import load_commands
from pwndbg.gdblib import gdb_version
from pwndbg.gdblib import load_gdblib
from pwndbg.gdblib import prompt
class GDBCommand(gdb.Command):
def __init__(
self,
debugger: GDB,
name: str,
handler: Callable[[pwndbg.dbg_mod.Debugger, str, bool], None],
):
self.debugger = debugger
self.handler = handler
super().__init__(name, gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION)
def invoke(self, args: str, from_tty: bool) -> None:
self.handler(self.debugger, args, from_tty)
class GDBCommandHandle(pwndbg.dbg_mod.CommandHandle):
def __init__(self, command: gdb.Command):
self.command = command
def remove(self) -> None:
# GDB doesn't support command removal.
pass
class GDB(pwndbg.dbg_mod.Debugger):
@ -22,6 +47,13 @@ class GDB(pwndbg.dbg_mod.Debugger):
load_gdblib()
load_commands()
# Importing `pwndbg.gdblib.prompt` ends up importing code that has the
# side effect of setting a command up. Because command setup requires
# `pwndbg.dbg` to already be set, and this module is used as part of the
# process of setting it, we have to wait, and do the import as part of
# this method.
from pwndbg.gdblib import prompt
prompt.set_prompt()
pre_commands = f"""
@ -69,7 +101,119 @@ class GDB(pwndbg.dbg_mod.Debugger):
config_mod.init_params()
pwndbg.gdblib.prompt.show_hint()
prompt.show_hint()
@override
def add_command(
self, name: str, handler: Callable[[pwndbg.dbg_mod.Debugger, str, bool], None]
) -> pwndbg.dbg_mod.CommandHandle:
command = GDBCommand(self, name, handler)
return GDBCommandHandle(command)
@override
def history(self, last: int = 10) -> List[Tuple[int, str]]:
# GDB displays commands in groups of 10. We might want more than that,
# so we fetch multiple blocks of 10 and assemble them into the final
# history in a second step.
parsed_blocks = []
parsed_lines_count = 0
parsed_lines_min = None
parsed_lines_max = None
parsed_lines_base = None
while parsed_lines_count < last:
# Fetch and parse the block we're currently interested in.
base = f" {parsed_lines_base}" if parsed_lines_base else ""
lines = gdb.execute(f"show commands{base}", from_tty=False, to_string=True)
lines = lines.splitlines()
parsed_lines = []
for line in lines:
number_str, command = line.split(maxsplit=1)
try:
number = int(number_str)
except ValueError:
# In rare cases GDB will output a warning after executing `show commands`
# (i.e. "warning: (Internal error: pc 0x0 in read in CU, but not in
# symtab.)").
return []
parsed_lines.append((number, command))
# We have nothing more to parse if GDB gives us nothing here.
if len(parsed_lines) == 0:
break
# Set the maximum command index we know about. This is simply the
# last element of the first block.
if not parsed_lines_max:
parsed_lines_max = parsed_lines[-1][0]
# Keep track of the minimum command index we've seen.
#
# This is usually the first element in the most recent block, but
# GDB isn't very clear about whether running commands with
# `gdb.execute` affects the command history, and what the exact size
# of the command history is. This means that, at the very end, the
# first index in the last block might be one greater than the last
# index in the second-to-last block.
#
# Additionally, the value of the first element being greater than
# the minimum also means that we reached the end of the command
# history on the last block, can break out of the loop early, and
# don't even need to bother with this block.
if parsed_lines_min:
if parsed_lines[0][0] < parsed_lines_min:
parsed_lines_min = parsed_lines[0][0]
else:
break
else:
parsed_lines_min = parsed_lines[0][0]
parsed_blocks.append(parsed_lines)
parsed_lines_count += len(parsed_lines)
# If we've just pulled the block with command index 0, we know we
# can't possibly go back any farther.
if parsed_lines_base == 0:
break
# The way GDB displays the command history is _weird_. The argument
# we pass to `show commands <arg>` is the index of the 6th element
# in the block, meaning we'll get a block whose values range from
# at most <arg> - 5 to at most <arg> + 4, inclusive.
#
# Given that we want the first element in this block to the just one
# past the maximum range of the block returned by the next arguemnt,
# and that we know the last element in a block is at most <arg> + 4,
# we can subtract five from its index to land in the right spot.
parsed_lines_base = max(0, parsed_lines[0][0] - 5)
# We've got nothing.
if len(parsed_blocks) == 0:
return []
# Sort the elements in the block into the final history array.
remaining = parsed_lines_max - parsed_lines_min + 1
plines: List[Tuple[int, str]] = [None] * remaining
while remaining > 0 and len(parsed_blocks) > 0:
block = parsed_blocks.pop()
for pline in block:
index = pline[0] - parsed_lines_min
if not plines[index]:
plines[pline[0] - parsed_lines_min] = pline
remaining -= 1
# If this fails, either some of our assumptions were wrong, or GDB is
# doing something funky with the output, either way, not good.
assert remaining == 0, "There are gaps in the command history"
return plines[-last:]
@override
def lex_args(self, command_line: str) -> List[str]:
return gdb.string_to_argv(command_line)
@override
def addrsz(self, address: Any) -> str:

Loading…
Cancel
Save