Configuring a tmux layout for pwndbg

Posted on Jan 5, 2025

I’ve been wanting to have a more pleasant GDB experience than just plain pwndbg and having all the information shown in the same place. The pwndbg features page contains some info on how to split the context sections using tmux, and a small example, as well as using splitmind to create a layout.

Creating a layout seems easy enough, and since the splitmind project hasn’t been updated in 5 years, I decided to create pwnmux to: a) have a quick way to setup GDB to my liking, and b) understand how the layout creation work, so that it can easily be changed when needed.

In this post we’ll go through the creation of a gdbinit that splits a tmux window into multiple panes, where each pane contains a different context section from pwndbg. If you just want the use the layout, here’s the TLDR of how the layout looks:

Pwnmux layout

And the github repository that you can clone and use: https://github.com/joaogodinho/pwnmux

Requirements

I built the configuration on the following versions:

  • GDB (version 15.0.50.20240403-git)
  • pwndbg (commit b06267d from dev)
  • tmux (version 3.4)

Creating A Custom Configuration

The gdbinit File

Before we move into the actual layout configuration, let’s start by understanding the .gdbinit file, how it will be used in this scenario, and how it can also be used for other useful tasks.

As defined in man gdbinit, the gdbinit file can “contain GDB commands to automatically execute during GDB startup.” Allowing us to customize and automate our debugging session.

The gdbinit file is loaded in the following order:

/etc/gdb/gdbinit
/etc/gdb/gdbinit.d/*
~/.config/gdb/gdbinit
~/.gdbinit
./.gdbinit

We’re just interested in using the ~/.gdbinit, but it’s worth noting that using ./.gdbinit is super useful for setting custom commands when debugging a specific executable.

⚠️ Warning
To load the `./.gdinit` file, the `~/.gdbinit` file must have `set auto-load safe-path .`. This has safety concerns.

One other interesting feature of gdbinit (and GDB in general) is the availability of Python scripts. This is available directly from inside GDB, but can also be used from scripts, or more specifically for our case, from .gdbinit.

Furthermore, GDB also provides a Python module that can be used to call GDB commands from the Python script. Below we show an example .gdbinit file that uses Python:

# We're running GDB commands, so we need to tell GDB to run the following from python
python
import gdb
print('Hello from python')
# Same as running `start` from GDB
gdb.execute('start')
# Tell GDB we're done sending Python
end

An alternative way of loading this Python script would be move the Python code into its own file and then add the following in .gdbinit:

source <path to script>.py

For pwnmux we’ll use the loading via the source command.

Creating the layout

With a better understanding on how we’ll build pwnmux, let’s move into the tmux layout we want. As shown in the Pwnmux layout picture above, we want to have 6 panes with different context sections.

💡 A context section in pwndbg refers to information about the current execution (e.g.: disassembly, registers).

Using tmux split-window we can create the panes, but we have to take into account from where the window will be split, its size, how to identify it, and what command to run inside the new pane:

  • For the position, we can control it using the -b and -t flag. With -b we can create the window to the left or above (default is right and below), and with the -t flag we can specify the target pane from where the split will happen (e.g.: top-left, bottom-right).
  • For the size we’ll use the -l followed by a percentage for the new pane.
  • To identify it we’ll use -P to return information about the created pane, and -F "#{pane_id}:#{pane_tty}" to specify the output format as the pane ID and TTY.
  • As for the command to be run, this will use the -d flag with the cat -d command for all panes, with the exception of the ipython pane that will have ipython as the command.

Based on this, we can create the following:

import os

panes = {
  # Split horizontal to make the main window at the bottom
  'disasm': os.popen('tmux split-window -vb -P -F "#{pane_id}:#{pane_tty}" -l 75% -d "cat -"').read().strip().split(":"),
  # Split horizontal to make the disasm + regs on the top, stack + stacktrace on bottom
  'stack': os.popen('tmux split-window -v -P -F "#{pane_id}:#{pane_tty}" -l 40% -t {top} -d "cat -"').read().strip().split(":"),
  # Split vertical next to the stack for the backtrace
  'backtrace': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t -1 -l 30% -d "cat -"').read().strip().split(":"),
  # Split vertical next to the disassemble for the registers
  'regs': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t {top} -l 30% -d "cat -"').read().strip().split(":"),
  'ipython': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t {bottom} -l 30% -d "ipython"').read().strip().split(":"),
}

We’re using a dict to be able to easily reference all the created panes. The .read().strip().spit(":") allows us to get a tuple where the first position is the pane ID and the second is the pane TTY.

Having created the panes, we must now tell pwndbg what to place in each. To do this we can simply iterate the panes dict and call contextoutput from pwndbg.commands.context.

from pwndbg.commands.context import contextoutput

# Tell pwndbg which panes are to be used for what
for section, p in panes.items():
  contextoutput(section, p[1], True, 'top', False)

With the exception of the ipython dict key, all other keys are valid sections in pwndbg. The third argument specifies that the output should be cleared, the fourth argument specifies the position of the banner, and the fifth argument specifies the width of the banner. By setting it to False, it will just print the name of the section.

If we place all of the above in a Python script and source it from the .gdbinit we’ll get the layout we want, but there are some things to take care of still.

First we want to also have a section for the legend (tells us what type of data each color means) and a section for the expressions. We add those with:

# Also add the sections legend and expressions to already existing panes
contextoutput("legend", panes['stack'][1], True)
contextoutput("expressions", panes['regs'][1], True, 'top', False)

By calling contextoutput with panes that are already being used, we can append sections to them.

Second we want to add some customization to GDB (increase the number of lines in some sections). For this we have two options: use the gdb module to run GDB commands (as previously described), or use pwndbg.config object to change the configuration. We’ll use the pwndbg.config object to make the script debugger agnostic (thanks for the hint @disconnect3d):

# To see more options to customize run `theme` and `config` in gdb
# Increase the amount of lines shown in disasm and stack
pwndbg.config.context_disasm_lines.value = 25
pwndbg.config.context_stack_lines.value = "18"
# Give backtrace a little more color
pwndbg.config.backtrace_prefix_color.value = "red,bold"
pwndbg.config.backtrace_address_color.value = "gray"
pwndbg.config.backtrace_symbol_color.value = "red"
pwndbg.config.backtrace_frame_label_color.value = "green"

In this case we modify the number of lines to 25 and 18 for the disasm and stack to fit our screen, and change the backtrace colors.

Lastly we want to close the panes when we exit GDB, which can be done with atexit:

import atexit
# Remove the panes when gdb is exited
atexit.register(lambda: [os.popen(F"tmux kill-pane -t {p[0]}").read() for p in panes.values()])

We iterate the values of the panes dict and use the first tuple entry (the pane ID) as the argument to the target pane to kill. When we exit GDB the lambdas are called and the panes we created are killed.

And that’s it… The final script should like like this:

import os
import atexit

import pwndbg
from pwndbg.commands.context import contextoutput

panes = {
  # Split horizontal to make the main window at the bottom
  'disasm': os.popen('tmux split-window -vb -P -F "#{pane_id}:#{pane_tty}" -l 75% -d "cat -"').read().strip().split(":"),
  # Split horizontal to make the disasm + regs on the top, stack + stacktrace on bottom
  'stack': os.popen('tmux split-window -v -P -F "#{pane_id}:#{pane_tty}" -l 40% -t {top} -d "cat -"').read().strip().split(":"),
  # Split vertical next to the stack for the backtrace
  'backtrace': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t -1 -l 30% -d "cat -"').read().strip().split(":"),
  # Split vertical next to the disassemble for the registers
  'regs': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t {top} -l 30% -d "cat -"').read().strip().split(":"),
  'ipython': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t {bottom} -l 30% -d "ipython"').read().strip().split(":"),
}

# Tell pwndbg which panes are to be used for what
for section, p in panes.items():
  contextoutput(section, p[1], True, 'top', False)

# Also add the sections legend and expressions to already existing panes
contextoutput("legend", panes['stack'][1], True)
contextoutput("expressions", panes['regs'][1], True, 'top', False)

# To see more options to customize run `theme` and `config` in gdb
# Increase the amount of lines shown in disasm and stack
pwndbg.config.context_disasm_lines.value = 25
pwndbg.config.context_stack_lines.value = "18"
# Give backtrace a little more color
pwndbg.config.backtrace_prefix_color.value = "red,bold"
pwndbg.config.backtrace_address_color.value = "gray"
pwndbg.config.backtrace_symbol_color.value = "red"
pwndbg.config.backtrace_frame_label_color.value = "green"
# Remove the panes when gdb is exited
atexit.register(lambda: [os.popen(F"tmux kill-pane -t {p[0]}").read() for p in panes.values()])

This can be easily customizable for specific needs (e.g.: screen sizes, colors), and can also be easily extended.