GDB

GDB loads a program, stops it wherever you want, and shows you exactly what happens: which instructions run, what the registers hold, how the stack and heap change, and where it crashes. Reach for it the moment static output runs out and you need to watch a reversing or pwn binary behave one step at a time instead of guessing. Front-ends GEF and pwndbg turn the bare prompt into a register, stack, and disassembly dashboard, so most CTF players keep one loaded.

Mental model: gdb [options] ./program, then drive the session with break, run, and step.

New to debuggers? Core concepts
  • Inferior: the process GDB controls; run starts it, continue resumes it, it has its own memory
  • Breakpoint (break / b): stop at a function, a source line, or a raw *address
  • Watchpoint (watch): stop when a variable or memory location is written (rwatch on reads)
  • Stepping: step / next move by source line, stepi / nexti by one instruction
  • Registers: on x86-64, rip is the next instruction, rsp the stack top, rbp the frame base, and the first args sit in rdi rsi rdx rcx r8 r9
  • The stack grows downward; locals and saved return addresses live here, which is where overflow bugs surface
  • Symbols: names from a -g build; a stripped binary has almost none, so you work by address
  • Examine (x/): read memory in a chosen format, for example x/16xg $rsp
When to reach for GDB (and when not)

Reach for it to step through a license or password check and read the expected value straight out of a register, confirm a buffer-overflow offset, inspect heap chunk layout in your own lab, reproduce a segfault in a program you wrote, or verify what a disassembler could only guess. Only debug binaries you own, wrote, or are explicitly authorized to analyze (CTF files, your own builds, dedicated practice VMs).

Reach for something else when a quick static look answers it (strings, objdump, or a decompiler in Ghidra / IDA / Binary Ninja), when you only need the syscalls or library calls a program makes (strace, ltrace), or when heavy anti-debug detects ptrace and you must patch the check before GDB is useful.

Install, update, verify#

sudo apt install -y gdb                    # Debian / Ubuntu / Kali
sudo dnf install -y gdb                    # Fedora / RHEL
brew install gdb                           # macOS (Homebrew)
sudo apt install --only-upgrade -y gdb     # update

command -v gdb            # confirm it is on PATH
gdb --version            # version string
gdb -nx -batch -ex quit  # start and exit cleanly, ignoring init files (self-test)

Install GDB from the distro package, then add one front-end. Do not load both; they conflict.

# GEF: one-line installer, writes ~/.gdbinit for you (re-run to update)
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"

# pwndbg: clone and run setup (git pull && ./setup.sh to update)
git clone https://github.com/pwndbg/pwndbg ~/tools/pwndbg && ~/tools/pwndbg/setup.sh

Confirm the front-end loaded by starting GDB on any binary: GEF prints GEF for linux ready, pwndbg shows a pwndbg> prompt. No banner means ~/.gdbinit is not sourcing it.

Flags you’ll actually use#

Skim these first; the cheat sheet below is these flags combined.

Launch flagDoesSession cmdDoes
-qQuiet start, no bannerbreak / bStop at func, line, or *addr
-nxSkip ~/.gdbinit (plain GDB)run / rStart the program
--argsPass argv to the programcontinue / cResume to the next stop
-p PIDAttach to a running processnext / stepOne source line, over / into calls
-x FILERun GDB commands from a filestepi / nextiOne instruction, into / over
-batchRun commands then exitfinishRun to end of current function
-ex 'CMD'Run one command at startupinfo registersDump CPU registers
./corePost-mortem from a core dumpx/FMTExamine memory in a format

Cheat sheet#

Each block starts simple and adds one line at a time, so you can stop at the line that does the job.

# Launch: stop at the line you need
gdb -q ./chall                        # 1. quiet start on a binary you own
gdb -q --args ./chall input.txt       # 2. + pass argv to the program
gdb -q -x scripts/run.gdb ./chall     # 3. + replay a saved command script
gdb -q ./chall ./core                 # post-mortem from a core dump
gdb -q -p 1234                        # attach to a running process you own
gdb -nx ./chall                       # plain GDB, ignore GEF/pwndbg
# Breakpoints and watchpoints
(gdb) break main                      # stop at a named function
(gdb) break *0x401170                 # stop at a raw address (stripped binaries)
(gdb) break file.c:42                 # stop at a source line (needs -g)
(gdb) tbreak main                     # one-shot breakpoint, auto-deletes after it hits
(gdb) watch counter                   # stop when a variable is written
(gdb) rwatch counter                  # stop when it is read
(gdb) info breakpoints                # list them
(gdb) delete 2                        # remove breakpoint number 2

# Run and step: stop at the depth you need
(gdb) run                             # 1. start (run < in.txt to feed stdin)
(gdb) continue                        # 2. resume to the next stop
(gdb) next                            # 3. one source line, over calls
(gdb) step                            # 3. one source line, into calls
(gdb) nexti                           # 4. one instruction, over calls
(gdb) stepi                           # 4. one instruction, into calls
(gdb) finish                          # run to the end of the current function

# Inspect state
(gdb) info registers                  # all registers
(gdb) info registers rdi rsi rdx      # just the first three integer args (x86-64)
(gdb) backtrace                       # the call stack (bt)
(gdb) info locals                     # locals in the current frame
(gdb) info args                       # arguments of the current frame
(gdb) frame 1                         # switch frames (also up / down)

# Examine memory: x/[count][format][size]
(gdb) x/16xg $rsp                     # 16 hex giant-words at the stack top
(gdb) x/s $rdi                        # the string a register points to
(gdb) x/8i $rip                       # the next 8 instructions
(gdb) x/4dw &count                    # 4 decimal 4-byte words at a symbol

# Disassemble
(gdb) disassemble main                # a whole function
(gdb) disassemble $rip,+32            # 32 bytes from the current instruction
(gdb) set disassembly-flavor intel    # Intel syntax instead of AT&T

# Modify (lab binaries you own only)
(gdb) set var password = 5            # overwrite a local variable
(gdb) set $rip = main                 # redirect execution to test a path
(gdb) set {int}0x601040 = 1           # write a value into an address

# Offsets, heap, search (pwndbg / GEF)
(gdb) cyclic 200                      # pwndbg: De Bruijn pattern (GEF: pattern create 200)
(gdb) cyclic -l 0x6161616c            # pwndbg: look up the offset of a crash value
(gdb) heap                            # pwndbg: list heap chunks (GEF: heap chunks)
(gdb) bins                            # pwndbg: show the free lists (GEF: heap bins)
(gdb) vmmap                           # memory map and segment permissions
(gdb) find $rsp, +0x2000, "flag"      # native: scan a range for bytes

# Logging and scripting
(gdb) set logging file notes/gdb.log
(gdb) set logging on                  # capture the session to a file

Command breakdowns#

Each block below explains one situation, the exact command, and what to expect back.

Load a binary and stop where it matters#

$ gdb -q ./chall
(gdb) break main
(gdb) run

Halts at main and the front-end prints registers, code, and the stack. break also takes a source line like file.c:42 when the binary was built with -g.

Break on a stripped binary with no symbols#

(gdb) disassemble _start
(gdb) break *0x401170
(gdb) run

When break main fails, read an address out of the disassembly and break on it with the * prefix. This is the normal path for stripped CTF binaries.

Step by source line vs by instruction#

(gdb) next        # one source line, over calls
(gdb) step        # one source line, into calls
(gdb) nexti       # one instruction, over calls
(gdb) stepi       # one instruction, into calls

Use the source-level pair on -g builds; switch to the instruction pair on stripped or optimized code, where source lines are meaningless.

Read the function arguments in registers#

(gdb) info registers rdi rsi rdx rcx

On the x86-64 System V ABI the first integer and pointer arguments land in rdi rsi rdx rcx r8 r9, in that order. Read them at a call to see exactly what is being passed.

Examine the stack and find return addresses#

(gdb) x/16xg $rsp

Prints 16 giant (8-byte) words in hex from the stack top. Saved return addresses and locals live here; this is where an overflow pattern shows up after a crash.

Read the string a pointer is compared against#

(gdb) x/s $rsi

If a register holds a char* at a strcmp or cmp, this prints the expected value directly, which is often the password or flag a check wants.

Disassemble a function to find the branches#

(gdb) disassemble main

Prints the instructions of main. Scan for a cmp or test followed by a conditional jmp: those are the decision points worth breaking on. Add set disassembly-flavor intel if you prefer Intel syntax.

Watch a variable and stop the moment it changes#

(gdb) watch counter
(gdb) continue

Execution halts the instant counter is written, printing the old and new value. Ideal for “something corrupts this and I do not know where”. Use rwatch to stop on reads.

Steer execution to test a path (lab binaries you own)#

(gdb) set var password = 5
(gdb) set $rip = main

set var overwrites a local; set $rip jumps execution. Use this to force a branch and confirm a code path without re-running, only on binaries you own.

Measure a buffer-overflow offset with a cyclic pattern#

(gdb) cyclic 200                 # pwndbg (GEF: pattern create 200)
(gdb) run                        # feed the pattern, let it crash
(gdb) cyclic -l 0x6161616c       # look up the offset from the value in rsp/rip

Generates a De Bruijn pattern, then reverses the crash value into the exact offset to the return address. This measures an offset in your own lab binary; it is not an exploit by itself.

Inspect the heap layout#

(gdb) heap                       # pwndbg: list chunks (GEF: heap chunks)
(gdb) bins                       # pwndbg: free lists (GEF: heap bins)

Shows allocated chunks and the free lists so you can reason about malloc and free behavior in a lab, for example a use-after-free or overlapping chunks.

Debug a crash post-mortem from a core dump#

$ ulimit -c unlimited            # enable cores first, then reproduce the crash
$ gdb -q ./chall ./core
(gdb) backtrace

Loads the memory image from the moment of the crash, with no live process needed. backtrace shows the call stack that led to the segfault, so you see the failing frame directly.

Attach to a running lab process#

$ gdb -q -p "$(pgrep -f chall)"
(gdb) backtrace

Attaches to a live process you own and freezes it. A ptrace: Operation not permitted error means yama is blocking it (see Troubleshooting). Release it with detach or quit.

Replay a session from a saved script#

$ gdb -q -x scripts/run.gdb ./chall

Runs the commands in scripts/run.gdb (for example break main, run, info registers) automatically. Makes a session repeatable and diffable instead of retyping it every time.

Reading the output#

  • Breakpoint 1, main () at chall.c:5 confirms where you stopped. A wrong-looking line number means an optimized or stripped build.
  • Register dump is ground truth: rip is the next instruction, rsp the stack top, and rdi / rsi / rdx the first call arguments.
  • x/ output depends entirely on the format letter: s string, i instruction, xg 8-byte hex. Wrong letter, wrong meaning.
  • Program received signal SIGSEGV is a bad memory access. If rip or rsp hold bytes you fed in, you control the crash in your lab binary.
  • No symbol table is loaded is normal for stripped CTF binaries; work by address with disassemble and stepi.
  • Empty or surprising values usually mean the wrong frame (up / down) or a variable out of scope at this point.

Troubleshooting#

ProblemCauseFix
command not foundGDB not installed / stale PATHReinstall; open a fresh shell
No GEF/pwndbg banner~/.gdbinit not sourcing itRe-run the installer; check ~/.gdbinit
No symbol table is loadedBinary is strippedWork by address: disassemble, stepi, break *addr
Breakpoint never hitsWrong address or dead codeConfirm the address from disassemble; check the call path
ptrace: Operation not permittedyama ptrace_scope restrictionecho 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope (lab box)
Wrong lines / missing localsOptimized build (-O2)Recompile your own code with -g -O0, or accept asm level
No core file writtenCore dumps disabledulimit -c unlimited, then reproduce the crash
x/ prints junkWrong format letterMatch format to data: s string, i instruction, xg hex
Program detects the debuggerAnti-debug via ptracePatch the check or use a different approach, in your lab

Gotchas#

  • Load only one front-end. GEF and pwndbg both rewrite ~/.gdbinit and clash if sourced together; keep two init files and switch, or comment one out.
  • step steps into library calls and can strand you deep in printf internals with no source. Use next over calls, or finish to climb back out.
  • x86-64 passes arguments in registers, not on the stack. Reading $rsp for a function’s first argument is the classic wrong-place mistake; read rdi rsi rdx instead.
  • On a PIE binary, static addresses are only offsets. The base is randomized at load, so break *0x1170 may bind nowhere; break by symbol and let GDB resolve the real address, or disable ASLR for the session.
  • set $rip = main does not fix up the stack. Jumping execution skips prologues and leaves rsp and rbp inconsistent, so use it to probe a path, not to run cleanly to the end.
  • set logging on was renamed to set logging enabled on in GDB 12; the old form still works but prints a deprecation notice.

Pairs with#

Ghidra, IDA, or Binary Ninja give a decompiler and graph view for the big picture, then GDB confirms what the code actually does at runtime, so most workflows use both. objdump, readelf, and strings do fast static triage of sections, symbols, and strings before you attach. strace and ltrace show which syscalls and library calls run when you do not need the internal logic. pwntools scripts the input once GDB has measured the offsets, and radare2 / rizin (with Cutter) or lldb are the common alternatives if you prefer their command style.

Find us elsewhere

Merch, stickers, and moreSupport the work at the Solvere Labs shop