Why bother with pwndbg?

When I first started doing binary exploitation, I was using stock GDB. It works, but it's honestly awful for anything beyond basic stepping. You're constantly typing x/20gx $rsp manually, there's no automatic context, and figuring out heap state mid-exploit is a nightmare. A classmate pointed me to pwndbg during a CTF and I haven't looked back.

pwndbg is a GDB plugin written in Python. It doesn't replace GDB — it sits on top and adds a context panel that auto-prints on every break: registers, stack, disassembly, and code. For heap exploitation it adds dedicated heap inspection commands. If you're doing any kind of binary work — CTF, fuzzer debugging, reverse engineering — it's the first thing I install.

pwndbg is not the same as peda or GEF. They're alternatives. pwndbg tends to be more actively maintained and has better heap support, which is why I use it.

Installation

It's straightforward. You need GDB already installed (most distros ship it, Kali definitely has it):

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

The setup script installs the Python dependencies and writes a source line into your ~/.gdbinit. After that, every time you launch GDB, pwndbg loads automatically. If you have multiple GDB configs already, check your ~/.gdbinit to make sure nothing conflicts.

On Ubuntu/Debian you might need to install some extras first:

sudo apt install gdb python3-pip
# then run setup.sh

On Kali it's usually clean out of the box.

What you see when it loads

Launch any binary with gdb ./binary and set a breakpoint. When execution stops, pwndbg auto-prints the context panel:

─────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────
 RAX  0x0
 RBX  0x0
 RCX  0x7ffff7af2151 (write+17) ◂— cmp rax, -0x1000 /* 'H=' */
 RDX  0x0
 RDI  0x1
 RSI  0x602260 ◂— 'Hello, world!\n'
─────────────────────────────[ STACK ]──────────────────────────────────────────
00:0000│ rsp  0x7fffffffe1e0 ◂— 0x1
01:0008│      0x7fffffffe1e8 —▸ 0x7fffffffe424
─────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────
 ► 0x401176 <main+34>    call   puts@plt                <puts@plt>

That's the default output on every break. Registers, stack, and disasm — all in one shot, no manual commands needed.

The commands I actually use

context

Reprints the full context panel. Useful after you scroll up and want to see registers again without stepping.

pwndbg> context

heap

Dumps the current heap state — chunks, sizes, which bin they're in. Essential for any heap exploitation work:

pwndbg> heap
pwndbg> bins         # shows fastbins, smallbins, tcache
pwndbg> malloc_chunk addr  # inspect a specific chunk

When I was debugging my black-box fuzzer, this is how I confirmed UAF crashes — pause on SIGSEGV, run heap, and you can see the chunk that got freed with a dangling reference pointing into it.

vmmap

Shows the full virtual memory map with permissions. Saves you constantly running info proc mappings:

pwndbg> vmmap

Especially useful for checking if a region is executable — important when you're checking ASLR layout or looking for ROP gadget regions.

telescope

Dereferences a chain of pointers automatically. Way better than manually chasing pointers:

pwndbg> telescope $rsp 20
pwndbg> telescope 0x602260

search

Searches process memory for a pattern — string, bytes, or value. Useful for finding where your shellcode or padding landed:

pwndbg> search -s "AAAA"
pwndbg> search -x 0x41414141

cyclic

Generates a De Bruijn pattern for offset finding, built right into pwndbg:

pwndbg> cyclic 200
pwndbg> cyclic -l 0x61616168   # find offset of that pattern in rbp/rip

Using it with pwntools

pwndbg pairs well with pwntools. When you attach GDB to a running process during exploit development:

from pwn import *
p = process('./binary')
gdb.attach(p, gdbscript='''
break *main+0x50
continue
''')

The gdbscript runs pwndbg commands — you can automate breakpoints, telescope inspection, heap dumps, all of it.

Common gotchas

A few things that tripped me up early on. If context stops printing, just run context manually — sometimes it gets suppressed after certain signals. If heap commands fail, the binary might not be using glibc malloc — they won't work on jemalloc or custom allocators. And if you're on a system with GDB < 8.x, upgrade — older versions have plugin compatibility issues with pwndbg.

Final thought

pwndbg is not magic — it's still GDB underneath. But the difference in usability is real. Being able to see heap state, registers, and disasm in one glance instead of manually printing everything cuts debugging time significantly. If you're doing any binary exploitation and you're still on stock GDB, spend the five minutes to install it.