Common pitfalls¤
Imports¤
Lets reiterate some of the most important submodules in Pwndbg:
pwndbg/dbg_mod(also providingpwndbg.dbg) - Implements a lightweight debugger abstraction layer. Provides functionality that the underlying debugger is responsible for, like setting a breakpoint or writing to memory.pwndbg/aglib- A library that usespwndbg/dbg_modto provide more complex operations, like operations on memory mappings (pwndbg/aglib/vmmap.py), registers (pwndbg/aglib/regs_mod.py), disassembly (pwndbg/aglib/disasm/) etc.pwndbg/lib- Generic functionality that does not depend on anything "debugger related", likepwndbg/lib/cache.py,pwndbg/lib/zig.py,pwndbg/lib/tempfile.pyetc.pwndbg/commands/- Pwndbg commands implementations.
To keep this architecture coherent, maintainable, and prevent import cycles, there are a few things we need to obide by that we see being violated from time to time.
pwndbg/lib/ files should only access pwndbg.lib¤
The pwndbg/lib/ files must be importable and usable from anywhere at anytime, they must not depend on any debugger state. Thus, the only Pwndbg code you should be importing in a pwndbg/lib file, is another pwndbg/lib file. God forbid you do import pwndbg.aglib or use pwndbg.dbg anywhere in such a file (even in a non-top-level, function import).
Don't access aglib in pwndbg/dbg_mod/¤
The aglib depends on dbg_mod, not the other way around. No dbg_mod/ file should have a top-level aglib import. Further, no dbg_mod/ file should have an aglib import anywhere (even function-level). Currently the second rule is not followed, and stuff works, but lets not make it any worse.
Don't access pwndbg/dbg_mod/<debugger>/ in pwndbg/dbg_mod/__init__.py¤
The top-level debugger abstraction interface, currently only consisting of the pwndbg/dbg_mod/__init__.py file, should never reach into or import debugger-specific code like the pwndbg/dbg_mod/gdb/ files.
On the other hand, it is okay to do it the other way around. In other words, you may access pwndbg/dbg_mod/__init__.py from debugger-specific code like e.g. pwndbg/dbg_mod/lldb/hooks.py.
Don't import commands¤
When a command is written, it is written with the user in mind and all that entails. This means appropriate error handling, message printing etc. A pwndbg/command/ file has access to every submodule in Pwndbg. As such, it is not made to be used as an API for some other command/functionality. If there exists a command which you want to use as API, refactor it into an aglib/ file, make sure there are no prints, make sure that it returns an error instead of silently eating it when appropriate etc.
Doing it this way prevents "fun" surprises, makes the code more maintanable, makes the dependancy graph cleaner and as such prevents import cycles.
from pwndbg.aglib import arch doesn't work¤
If you look at pwndbg/aglib/__init__.py you will see that arch is a None-initialized object and gets swapped out at runtime depending on which architecture we are debugging. As such, to get the correct information about the current architecture you must always do aglib.arch.whatever().
from pwndbg.aglib import regs doesn't work¤
If you look at pwndbg/aglib/__init__.py you will see that regs is a None-initialized object and gets swapped out during initialization. Doing from pwndbg.aglib import regs binds regs to None. Instead always access it via aglib.regs like: aglib.regs.whatever().
No module magic¤
If you think you need a class module, you don't. Any amount of convenience you gain by that is dwarfed by the pain you bring to readability, maintainability, type system processing, LSP analysis etc.
The same goes for
. This is bad. Don't do this.Don't name the object the same as the file¤
The pwndbg/dbg_mod/ folder used to be named pwndbg/dbg/, and had a singleton object also called dbg defined in pwndbg/dbg/__init__.py. pwndbg/__init__.py used to have this:
objname_mod.py. It is a recognizable idiom in the codebase. See !3492 for more info.
Don't import x as y¤
In order to keep the code more readable, we should reach for consistency throughout the codebase. Renaming imports in a non-conventional way hurts readability. To provide an example, some files in the codebase do import pwndbg.color.memory as M while some do import pwndbg.color.message as M. Fun! Prefer to use:
import pwndbg.aglib as aglib
import pwndbg.aglib.memory as memory
import pwndbg.color as color
import pwndbg.color.message as message
import pwndbg.color.memory as mem_color
import pwndbg.color.context as ctx_color
Try not to touch pwndbg/gdblib/¤
We want to refactor everything from pwndbg/gdblib/ into pwndbg/dbg_mod/gdb/. So if you're writing code into gdblib or writing code that uses gdblib you must have a really good reason to do so.
Imports have side-effects¤
A large amount of imports in the codebase have side-effects. Some/most are not immediately visible, but it is good to keep in mind. For instance the @pwndbg.commands.Command and @pwndbg.lib.cache.cache_until decorators and the pwndbg.config.add_param function all modify non-local state.
mypy is complaining about an unecessary import¤
If your import is truly unecessary then remove it. If you are performing an import because you wish to trigger the side-effects of that import, you can use this syntax to appease mypy:
Import what you use!¤
We have some places in the codebase that do stuff like import pwndbg and then use pwndbg.aglib.nearpc.whatever(). This will work at runtime because the pwndbg.aglib.nearpc module does exist, but you should import it explicitly with import pwndbg.aglib.nearpc! This makes it easier for readers to figure out inter-file dependancies and makes it possible for static checkers to resolve correct types. If after importing at the top you get a circular import error, this means it's time to refactor!
Import at the top!¤
If you need to peform a function-level import this is likely an omen that the code deserves a refactor. So import at the top of the file! There are even places in the code where function-level imports are used but they could easily be moved to top-level imports with no other changes; do not contribute to this confusion!
There are some exceptions to this rule of course, such as dbg.setup(), aglib.load_aglib(), commands.load_commands() and gdblib.load_gdblib(). But these are rare and have a clear intention.