Skip to content

Adding a Command¤

Command skeleton¤

To add a command to Pwndbg, create a new python file in pwndbg/commands/my_command.py where my_command is the name of the command you want to add. The most basic command looks like this:

import argparse
import pwndbg.commands

parser = argparse.ArgumentParser(description="Command description.")
parser.add_argument("arg", type=str, help="An example argument.")

@pwndbg.commands.Command(parser, category=pwndbg.commands.CommandCategory.MISC)
def my_command(arg: str) -> None:
    """Print the argument"""
    print(f"Argument is {arg}")
Next, import this file in the load_commands function in pwndbg/commands/__init__.py.

That's all you need to get it working!

pwndbg> my-command foo
Argument is foo

Getting started¤

Let's see what arguments the @pwndbg.commands.Command decorator takes. It is defined in pwndbg/commands/__init__.py:

    def __init__(
        self,
        parser_or_desc: argparse.ArgumentParser | str,
        *,  # All further parameters are not positional
        category: CommandCategory,
        command_name: str | None = None,
        aliases: List[str] = [],
        examples: str = "",
        notes: str = "",
        only_debuggers: Set[pwndbg.dbg_mod.DebuggerType] = None,
        exclude_debuggers: Set[pwndbg.dbg_mod.DebuggerType] = None,
    ) -> None:
        # ...
We will cover the first four arguments now, and come back to the rest later.

If your command takes no arguments you can pass the description of the command as the first argument (parser_or_desc) to the constructor. Otherwise you will be passing an argparse.ArgumentParser object there.

The only other required argument is category. The category determines how commands are grouped together in the output of the pwndbg command and in the documentation. Peruse the list of all commands inside a debugger (by running the pwndbg command) and decide in which category your command fits best. The enum of all command categories is defined at the top of the pwndbg/commands/__init__.py file.

Picking a command name¤

Next, the command_name argument. It is optional because if it is not specified the command name will be the same as the function you used to define the command (except the underscores are replaced with dashes). As such, it is generally not needed to specify this argument.

That being said, it is important to pick a good name for your command. Ideally your command name should be one to two words that are not delimited by a dash (e.g. errno, libcinfo, buddydump) since that is easiest to remember and type.

If your command is porting behavior from some other debugger or tool, you should consider using the same name they use so users don't need to relearn it when switching.

If the command name contains three or more words, you should use dashes to make it more legible. If that is the case, or if the name is long, consider providing an alias that makes it quicker to type (like vis-heap-chunks [vis]).

You provide aliases to a command by specifying a list of strings to the aliases argument. Again, you may provide aliases to help users transitioning from other tools/debuggers (e.g. nearpc [pdisass, u]).

The arguments your command will take¤

We are using argparse.ArgumentParser from the python standard library to define command arguments. Take a look at the python documentation to see how it works. Let's take a look at an example from the source (the setflag command):

parser = argparse.ArgumentParser(description="Modify the flags register.")

parser.add_argument(
    "flag",
    type=str,
    help="Flag for which you want to change the value"
 )

parser.add_argument(
    "value",
    type=int,
    help="Value to which you want to set the flag - only valid options are 0 and 1",
)
For usage inside Pwndbg, to instantiate an argparse.ArgumentParser object, you must pass the description argument and may pass the epilog argument. Everything else, including prog, usage, formatter_class etc. will be set up by Pwndbg (by the @pwndbg.commands.Command decorator). Here we see only the description was provided.

Add arguments to your command with parser.add_argument. Again, consult the python documentation for an explanation. One nice thing specific to Pwndbg is that by setting an argument's type to int, it will also accept debugger values and symbols that can resolve to an int. For instance:

pwndbg> setflag ZF (1-1)
Set flag ZF=0 in flag register eflags (old val=0x206, new val=0x206)
pwndbg> setflag ZF $rdi
Set flag ZF=1 in flag register eflags (old val=0x246, new val=0x246)
pwndbg> setflag ZF (int)main^(int)main
Set flag ZF=0 in flag register eflags (old val=0x246, new val=0x206)
Be careful when deciding which arguments are positional, and which are optional. Especially take care if you have positional arguments which are not required, think about which of those will be specified more often by users and put them first.

Your function signature should match the arguments you defined with argparse (and their order!), unsurprisingly the setflag function has this signature:

def setflag(flag: str, value: int) -> None:
You can see the help of your command with my_command -h or help my_command, so for setflag:
pwndbg> help setflag
usage: setflag [-h] flag value

Modify the flags register.

positional arguments:
  flag        Flag for which you want to change the value
  value       Value to which you want to set the flag - only valid options are 0 and 1

options:
  -h, --help  show this help message and exit

Examples:
On X86/X64:
    setflag ZF 1        -- set zero flag
    setflag CF 0        -- unset carry flag

On ARM:
    setflag Z 0         -- unset the Z cpsr/xpsr flag

To see flags registers:
    info reg eflags     -- on x86/x64
    info reg cpsr/xpsr  -- on ARM (specific register may vary)

Notes:
This command supports flags registers that are defined for architectures in the pwndbg/regs.py file.

Alias: flag
Eh? Where is all that extra text coming from? Well the Alias: flag line is being automatically generated by Pwndbg but...

Examples, notes, and debugger support¤

Coming back to the arguments of the pwndbg.commands.Command constructor:

    def __init__(
        self,
        parser_or_desc: argparse.ArgumentParser | str,
        *,  # All further parameters are not positional
        category: CommandCategory,
        command_name: str | None = None,
        aliases: List[str] = [],
        examples: str = "",  #  <--- we left off here
        notes: str = "",
        only_debuggers: Set[pwndbg.dbg_mod.DebuggerType] = None,
        exclude_debuggers: Set[pwndbg.dbg_mod.DebuggerType] = None,
    ) -> None:
        # ...
You may supply the examples and notes arguments to add additional text at the end of the command's help. It is defined like so for setflag:
@pwndbg.commands.Command(
    parser,
    aliases=["flag"],
    category=CommandCategory.REGISTER,
    examples="""
On X86/X64:
    setflag ZF 1        -- set zero flag
    setflag CF 0        -- unset carry flag

On ARM:
    setflag Z 0         -- unset the Z cpsr/xpsr flag

To see flags registers:
    info reg eflags     -- on x86/x64
    info reg cpsr/xpsr  -- on ARM (specific register may vary)
    """,
    notes="""
This command supports flags registers that are defined for architectures in the pwndbg/regs.py file.
    """,
)
@pwndbg.commands.OnlyWhenRunning
def setflag(flag: str, value: int) -> None:
    # ....
When writing this (and the command description for that matter), you should consider what it will look like in the documentation after being parsed as markdown.

As for only_debuggers and exclude_debuggers, you must use (usually one of) them if your command does not work an all debuggers that Pwndbg supports. For instance, if it uses some features from pwndbg.gdblib (which should be avoided if at all possible). In such a case, you probably also need to conditionally import it in the load_commands function.

Can your command be invoked all the time?¤

In most cases a command cannot be legally invoked at every moment in a debugging session, or for every debugging session. For instance, you can't use heap commands if the heap isn't initialized yet, you can't use kernel commands if you're not debugging a kernel.

To make sure these cases are properly handled, Pwndbg provides certain decorators. They are defined in pwndbg/commands/__init__.py. Check the source to see an up-to-date list, but here are some important ones:

OnlyWhenRunning
OnlyWhenLocal
OnlyWithFile
OnlyWhenQemuKernel
OnlyWhenUserspace
OnlyWithKernelDebugSyms
OnlyWhenPagingEnabled
OnlyWithTcache
OnlyWhenHeapIsInitialized
OnlyWithResolvedHeapSyms
Feel free to add more of these decorators yourself!

Another very important one is OnlyWithArch, defined in pwndbg/aglib/proc.py. Does your command work on all architectures? If not, make sure to specify this decorator and pass in the architectures which you do support.

Actually implementing the command¤

There is no single right way to do it. You will want to read the source of some similar commands and see how they work. Check out the general developer notes, and feel free to ask a question on the discord server. Good luck!