pwndbg/DEVELOPING.md

6.3 KiB

Environment setup

After installing pwndbg by running setup.sh, you additionally need to run ./setup-test-tools.sh to install the necessary development dependencies.

If you would like to use Docker, you can create a Docker image with everything already installed for you. To do this, run the following command:

docker run -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v `pwd`:/pwndbg pwndbg bash

If you'd like to use docker compose, you can run

docker compose run -i main

Testing

It's highly recommended you write a new test or update an existing test whenever adding new functionality to pwndbg.

Tests are located in tests/gdb-tests. tests/unit-tests also exists, but the unit testing framework is not complete and so it should not be used.

To run the tests, run ./tests.sh. You can filter the tests to run by providing an argument to the script, such as ./tests.sh heap, which will only run tests that contain "heap" in the name. You can also drop into the PDB debugger when a test fails with ./tests.sh --pdb.

Our tests are written using pytest. It uses some magic so that Python's assert can be used for asserting things in tests and it injects dependencies which are called fixtures, into test functions. These fixtures are defined in tests/conftest.py.

We can take a look at tests/gdb-tests/tests/test_hexdump.py for an example of a simple test. Looking at a simplified version of the top-level code, we have this:

import gdb
import tests

BINARY = tests.binaries.get("reference-binary.out")

Since these tests run inside GDB, we can import the gdb Python library. We also import the tests module, which makes it easy to get the path to the test binaries located in tests/gdb-tests/tests/binaries. You should be able to reuse the binaries in this folder for most tests, but if not feel free to add a new one.

Here's a small snippet of the actual test:

def test_hexdump(start_binary):
    start_binary(BINARY)
    pwndbg.gdblib.config.hexdump_group_width = -1

    gdb.execute("set hexdump-byte-separator")
    stack_addr = pwndbg.gdblib.regs.rsp - 0x100

pytest will run any function that starts with test_ as a new test, so there is no need to register your new test anywhere. The start_binary argument is a function that will run the binary you give it, and it will set some common options before starting the binary. Using start_binary is recommended if you don't need any additional customization to GDB settings before starting the binary, but if you do it's fine to not use it.

Note that in the test, we can access pwndbg library code like pwndbg.gdblib.regs.rsp as well as execute GDB commands with gdb.execute().

Linting

The lint.sh script runs isort, black, flake8, and shfmt. isort and black are able to automatically fix any issues they detect, and you can enable this by running ./lint.sh -f. You can find the configuration files for these tools in setup.cfg and pyproject.toml.

When submitting a PR, the CI job defined in .github/workflows/lint.yml will verify that running ./lint.sh succeeds, otherwise the job will fail and we won't be able to merge your PR.

You can optionally set the contents of .git/hooks/pre-push to the following if you would like lint.sh to automatically be run before every push:

#!/bin/sh

./lint.sh || exit 1

Random developer notes

Feel free to update the list below!

  • If you want to play with pwndbg functions under GDB, you can always use GDB's pi which launches python interpreter or just py <some python line>.

  • If there is possibility, don't use gdb.execute as this requires us to parse the string and so on; there are some cases in which there is no other choice. Most of the time we try to wrap GDB's API to our own/easier API.

  • We have our own pwndbg.config.Parameter (which extends gdb.Parameter) - all of our parameters can be seen using config or theme commands. If we want to do something when user changes config/theme - we can do it defining a function and decorating it with pwndbg.config.Trigger.

  • The dashboard/display/context we are displaying is done by pwndbg/commands/context.py which is invoked through GDB's prompt hook (which we defined in pwndbg/prompt.py as prompt_hook_on_stop).

  • All commands should be defined in pwndbg/commands - most of them lie in separate files but some files contains many of them (e.g. commands corresponding to windbg debugger - in windbg.py or some misc commands in misc.py). We would also want to make all of them to use ArgparsedCommand (instead of Command).

  • We change a bit GDB settings - this can be seen in pwndbg/__init__.py - there are also imports for all pwndbg submodules

  • We have a wrapper for GDB's events in pwndbg/events.py - thx to that we can e.g. invoke something based upon some event

  • We have a caching mechanism ("memoization") which we use through Python's decorators - those are defined in pwndbg/memoize.py - just check its usages

  • To block a function before the first prompt was displayed use the pwndbg.decorators.only_after_first_prompt decorator.

  • Memory accesses should be done through pwndbg/memory.py functions

  • Process properties can be retrieved thx to pwndbg/gdblib/proc.py - e.g. using pwndbg.gdblib.proc.pid will give us current process pid

  • We have a wrapper for handling exceptions that are thrown by commands - defined in pwndbg/exception.py - current approach seems to work fine - by using set exception-verbose on - we get a stacktrace. If we want to debug stuff we can always do set exception-debugger on.

  • Some of pwndbg's functionality - e.g. memory fetching - require us to have an instance of proper gdb.Type - the problem with that is that there is no way to define our own types - we have to ask gdb if it detected particular type in this particular binary (that sucks). We do it in pwndbg/typeinfo.py and it works most of the time. The known bug with that is that it might not work properly for Golang binaries compiled with debugging symbols.

  • We would like to add proper tests for pwndbg - see tests framework PR if you want to help on that.