From aa566e595ac2a95925cd97e443d57f3b4356cbb9 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 9 Jul 2024 14:25:52 -0300 Subject: [PATCH] Expand Debugger-agnostic API and split `pwndbg.dbg.evaluate_expression` (#2280) --- poetry.lock | 8 ++-- pwndbg/commands/__init__.py | 24 +++++++++-- pwndbg/dbg/__init__.py | 62 ++++++++++++++++++++++------ pwndbg/dbg/gdb.py | 81 ++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- 5 files changed, 154 insertions(+), 23 deletions(-) diff --git a/poetry.lock b/poetry.lock index 672703d81..2cfc48b7b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2059,13 +2059,13 @@ files = [ [[package]] name = "types-gdb" -version = "12.1.4.20240408" +version = "12.1.4.20240704" description = "Typing stubs for gdb" optional = false python-versions = ">=3.8" files = [ - {file = "types-gdb-12.1.4.20240408.tar.gz", hash = "sha256:f2136ccf15ab74b5b2702b88b3c38a41029a89c12d9c12851c6a0b9789047d83"}, - {file = "types_gdb-12.1.4.20240408-py3-none-any.whl", hash = "sha256:b5fc44c1929cc5eb071792d5f66b5f74e880589683d065651bf9464d8f41ad75"}, + {file = "types-gdb-12.1.4.20240704.tar.gz", hash = "sha256:0b537f6acf8339e82dc92388aad057a30c83d89105cd820cf5dc948d6debce10"}, + {file = "types_gdb-12.1.4.20240704-py3-none-any.whl", hash = "sha256:da273ebf395ded84ea12745ef70d0c1560694c26b6d9c04575d500a74929fed1"}, ] [[package]] @@ -2354,4 +2354,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "06adbdf62440aaedba8a00255a1ba7db2206e6124a4f4beef8fadce656df8f10" +content-hash = "c46fbb9c8a498c53bc1acb13d027819c5c3c88124a6c58b8408d30699f1a81f1" diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py index 11dbc6738..2bec8c702 100644 --- a/pwndbg/commands/__init__.py +++ b/pwndbg/commands/__init__.py @@ -157,6 +157,10 @@ class Command: pwndbg.exception.handle(self.function.__name__) return + if not pwndbg.dbg.selected_inferior(): + log.error("Pwndbg commands require a target binary to be selected") + return + try: self.repeat = self.check_repeated(argument, from_tty) self(*args, **kwargs) @@ -220,15 +224,20 @@ def fix( if isinstance(arg, pwndbg.dbg_mod.Value): return arg + frame = pwndbg.dbg.selected_frame() + target: pwndbg.dbg_mod.Frame | pwndbg.dbg_mod.Process = ( + frame if frame else pwndbg.dbg.selected_inferior() + ) + assert target, "Reached command expression evaluation with no frame or inferior" + try: - parsed = pwndbg.dbg.evaluate_expression(arg) - return parsed + return target.evaluate_expression(arg) except Exception: pass try: arg = pwndbg.gdblib.regs.fix(arg) - return pwndbg.dbg.evaluate_expression(arg) + return target.evaluate_expression(arg) except Exception as e: if not quiet: print(e) @@ -609,8 +618,15 @@ def sloppy_gdb_parse(s: str) -> int | str: :param s: String. :return: Whatever gdb.parse_and_eval returns or string. """ + + frame = pwndbg.dbg.selected_frame() + target: pwndbg.dbg_mod.Frame | pwndbg.dbg_mod.Process = ( + frame if frame else pwndbg.dbg.selected_inferior() + ) + assert target, "Reached command expression evaluation with no frame or inferior" + try: - val = pwndbg.dbg.evaluate_expression(s) + val = target.evaluate_expression(s) # We can't just return int(val) because GDB may return: # "Python Exception Cannot convert value to long." # e.g. for: diff --git a/pwndbg/dbg/__init__.py b/pwndbg/dbg/__init__.py index ef515bb25..07f9b96b7 100644 --- a/pwndbg/dbg/__init__.py +++ b/pwndbg/dbg/__init__.py @@ -13,6 +13,38 @@ from typing import Tuple dbg: Debugger = None +class Frame: + def evaluate_expression(self, expression: str) -> Value: + """ + Evaluate the given expression in the context of this frame, and + return a `Value`. + """ + raise NotImplementedError() + + +class Thread: + def bottom_frame(self) -> Frame: + """ + Frame at the bottom of the call stack for this thread. + """ + raise NotImplementedError() + + +class Process: + def threads(self) -> List[Thread]: + """ + Returns a list containing the threads in this process. + """ + raise NotImplementedError() + + def evaluate_expression(self, expression: str) -> Value: + """ + Evaluate the given expression in the context of the current process, and + return a `Value`. + """ + raise NotImplementedError() + + class TypeCode(Enum): """ Broad categories of types. @@ -228,6 +260,24 @@ class Debugger: """ raise NotImplementedError() + def selected_inferior(self) -> Process | None: + """ + The inferior process currently being focused on in this interactive session. + """ + raise NotImplementedError() + + def selected_thread(self) -> Thread | None: + """ + The thread currently being focused on in this interactive session. + """ + raise NotImplementedError() + + def selected_frame(self) -> Frame | None: + """ + The stack frame currently being focused on in this interactive session. + """ + raise NotImplementedError() + def add_command( self, name: str, handler: Callable[[Debugger, str, bool], None] ) -> CommandHandle: @@ -244,18 +294,6 @@ class Debugger: # removed or replaced as the porting work continues. # - # This function will be split up into a frame-context and a global-context - # versions very soon, in a way that properly represents the way evaluation - # works under both GDB and LLDB. - # - # TODO: Split up `evaluate_expressions` into its global and local versions. - def evaluate_expression(self, expression: str) -> Value: - """ - Evaluate the given expression in the context of the current frame, and - return a `Value`. - """ - raise NotImplementedError() - def addrsz(self, address: Any) -> str: """ Format the given address value. diff --git a/pwndbg/dbg/gdb.py b/pwndbg/dbg/gdb.py index 64b120d7f..9a43da127 100644 --- a/pwndbg/dbg/gdb.py +++ b/pwndbg/dbg/gdb.py @@ -17,6 +17,66 @@ from pwndbg.gdblib import gdb_version from pwndbg.gdblib import load_gdblib +def parse_and_eval(expression: str, global_context: bool) -> gdb.Value: + """ + Same as `gdb.parse_and_eval`, but only uses `global_context` if it is + supported by the current version of GDB. + + `global_context` was introduced in GDB 14. + """ + try: + return gdb.parse_and_eval(expression, global_context) + except TypeError: + return gdb.parse_and_eval(expression) + + +class GDBFrame(pwndbg.dbg_mod.Frame): + def __init__(self, inner: gdb.Frame): + self.inner = inner + + @override + def evaluate_expression(self, expression: str) -> pwndbg.dbg_mod.Value: + selected = gdb.selected_frame() + restore = False + if selected != self.inner: + self.inner.select() + restore = True + + value = parse_and_eval(expression, global_context=False) + + if restore: + selected.select() + + return GDBValue(value) + + +class GDBThread(pwndbg.dbg_mod.Thread): + def __init__(self, inner: gdb.InferiorThread): + self.inner = inner + + @override + def bottom_frame(self) -> pwndbg.dbg_mod.Frame: + selected = gdb.selected_thread() + restore = False + if selected != self.inner: + self.inner.switch() + restore = True + + value = gdb.newest_frame() + if restore: + selected.switch() + return GDBFrame(value) + + +class GDBProcess(pwndbg.dbg_mod.Process): + def __init__(self, inner: gdb.Inferior): + self.inner = inner + + @override + def evaluate_expression(self, expression: str) -> pwndbg.dbg_mod.Value: + return GDBValue(parse_and_eval(expression, global_context=True)) + + class GDBCommand(gdb.Command): def __init__( self, @@ -326,8 +386,25 @@ class GDB(pwndbg.dbg_mod.Debugger): return gdb.string_to_argv(command_line) @override - def evaluate_expression(self, expression: str) -> pwndbg.dbg_mod.Value: - return GDBValue(gdb.parse_and_eval(expression)) + def selected_thread(self) -> pwndbg.dbg_mod.Thread | None: + thread = gdb.selected_thread() + if thread: + return GDBThread(thread) + return None + + @override + def selected_frame(self) -> pwndbg.dbg_mod.Frame | None: + try: + frame = gdb.selected_frame() + if frame: + return GDBFrame(frame) + except gdb.error: + pass + return None + + @override + def selected_inferior(self) -> pwndbg.dbg_mod.Process | None: + return GDBProcess(gdb.selected_inferior()) @override def addrsz(self, address: Any) -> str: diff --git a/pyproject.toml b/pyproject.toml index 23cfcd1b4..ec90c8023 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -177,7 +177,7 @@ ruff = "^0.4.1" sortedcontainers-stubs = "^2.4.2" testresources = "^2.0.1" tomli = "^2.0.1" -types-gdb = "^12.1.4.20240408" +types-gdb = "^12.1.4.20240704" types-psutil = "^5.9.5.20240423" types-pygments = "^2.17.0.20240310" types-requests = "^2.31.0.20240406"