Skip to main content
Be Bitwise

GDB commands worth learning past break and continue

A short tour of the GDB commands that turn an interactive session into something faster than printf debugging — watchpoints, commands, display, tui, find, and conditional breakpoints.

#gdb#debugging#tooling

There's a plateau every GDB user hits. You learn break, run, next, step, continue, maybe bt and info locals, and then you stop adding to your repertoire. The problem is that everything past that plateau is genuinely useful, and the help text isn't great at advertising it. Here's a tour of what I actually use, day to day.

Watchpoints catch the writer, not the reader

If a value is changing and you don't know who's writing it, set a watchpoint.

text
(gdb) watch variable
Hardware watchpoint 2: variable
(gdb) continue

Execution stops the instant the value changes. GDB prints both the old and the new value, which usually points at the offending writer immediately. Hardware watchpoints are limited (x86 has four debug registers, so four total), and if GDB can't get one, it'll fall back to a software watchpoint, which works but slows your program down by orders of magnitude. Worth knowing before you wonder why your binary suddenly takes a minute to reach main.

For reads, use rwatch. For both, awatch. Watching a memory address rather than a symbol works too:

text
(gdb) watch *(int*)0x7fffffffe1c4

Combine with conditions to narrow further:

text
(gdb) watch counter if counter > 100

The first time I learned watchpoints I had been hunting a corruption bug for three days using printf. The watchpoint hit on the third instruction.

commands automates what you would otherwise type ten times

Once a breakpoint exists, you can attach a script to it.

text
(gdb) break do_thing
(gdb) commands
> silent
> printf "do_thing called with %d\n", arg
> continue
> end

silent suppresses the usual "Breakpoint N hit" line. continue resumes after the script runs, so the breakpoint behaves like a tracepoint. This is printf debugging, except you didn't have to recompile.

A more useful pattern: dump a struct and the call stack on every hit, but only when an interesting condition holds.

text
(gdb) break process_packet if pkt->len > 1500
(gdb) commands
> silent
> p *pkt
> bt 5
> continue
> end

You walk away, come back, and grep the GDB log for the cases that mattered.

display shows you context after every step

You can ask GDB to print an expression after every stopping command without retyping it.

text
(gdb) display/i $pc
(gdb) display $rax
(gdb) display *((int*)$rsp)

After every stepi or nexti, GDB will reprint these. The /i $pc trick is the standard way to step through unfamiliar assembly without committing to TUI mode. info display lists what you've added; undisplay N removes one.

Pair it with set disassemble-next-line on and the prompt becomes self-documenting:

text
(gdb) set disassemble-next-line on

Now every stop shows the next instruction inline.

TUI is rough but worth ten minutes of acclimatisation

Press Ctrl-X A and GDB splits into a curses interface with a source window, a command window, and (on Ctrl-X 2) a register or assembly pane. It's ugly. It will fight your terminal. The keybindings come from a 1996 sensibility. None of that matters once you've spent ten minutes with it, because seeing the cursor advance through source is so much better than reading a stream of list output that you stop using the non-TUI mode for anything but quick one-offs.

text
(gdb) tui enable
(gdb) layout split
(gdb) layout regs
(gdb) focus next

The single most useful keybinding is Ctrl-L, which redraws the screen when something corrupts it (something always corrupts it). If you can't make TUI behave, gdb -tui from the start sometimes fares better than enabling it mid-session.

There's also gdb-dashboard and pwndbg and gef, all of which are arguably nicer. Try them. But know what TUI gives you first, because every machine you SSH into has it.

info proc mappings shows you what's loaded where

When you're chasing a wild pointer or wondering whether ASLR moved your library somewhere unexpected:

text
(gdb) info proc mappings

You get the full /proc/<pid>/maps for the inferior, with addresses, permissions, and backing file. Cross-reference with info sharedlibrary to see the load addresses of every shared object the loader brought in. When a segfault address sits in the middle of nothing, this is how you confirm it.

info files is the static cousin: section addresses for the loaded executable and any libraries.

find searches process memory

If you know a pattern is in there somewhere but not where:

text
(gdb) find /b 0x400000, +0x100000, 0xde, 0xad, 0xbe, 0xef

The /b is the search granularity (byte, halfword, word, giant). The address range is start, length. The remaining arguments are the bytes to look for. Strings work too:

text
(gdb) find /s 0x7ffff0000000, +0x10000000, "/etc/passwd"

This is the GDB version of grepping a core dump in flight. Useful for reverse engineering, useful for hunting credentials accidentally left in memory, useful for finding the buffer your overflow landed in.

Conditional breakpoints take real expressions

Most users learn break foo if x == 5, then stop. The condition is a full GDB expression and can dereference, call functions, and compare against globals.

text
(gdb) break alloc if size > 4096 && getpid() == 1234
(gdb) break free if ptr == g_suspicious_pointer
(gdb) break parse_request if strncmp(buf, "POST", 4) == 0

That last one calls strncmp in the inferior, which is occasionally dangerous (the call has side effects in the inferior's process state) and occasionally exactly what you need. Use it deliberately.

For breakpoints inside loops that fire too often, ignore N M skips the next M hits before stopping:

text
(gdb) ignore 3 999

Stops on the thousandth hit of breakpoint 3. Cheaper than a condition involving a counter.

A small habit that compounds

Put the things you actually use in ~/.gdbinit. Defining a few short aliases pays for itself within a week:

text
define hook-stop
printf "PC: "
x/i $pc
end
set print pretty on
set pagination off
set history save on
set history filename ~/.gdb_history

Tasteful additions only — the moment your .gdbinit becomes a 500-line config you copy from machine to machine, you've crossed into the territory where pwndbg or gef are honestly a better choice.

Where to take this further

The full picture of dynamic analysis (debuggers, dynamic instrumentation, sanitizers, fuzzing as a runtime feedback loop) is laid out in Module 82: Dynamic Analysis & Debugging. GDB is the workhorse, but it sits inside a wider toolbox, and knowing when to reach for rr or valgrind or DynamoRIO instead is the difference between solving a bug in an hour and chasing it for a week.