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;
runstarts it,continueresumes 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 (rwatchon reads) - Stepping:
step/nextmove by source line,stepi/nextiby one instruction - Registers: on x86-64,
ripis the next instruction,rspthe stack top,rbpthe frame base, and the first args sit inrdi 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
-gbuild; a stripped binary has almost none, so you work by address - Examine (
x/): read memory in a chosen format, for examplex/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 flag | Does | Session cmd | Does |
|---|---|---|---|
-q | Quiet start, no banner | break / b | Stop at func, line, or *addr |
-nx | Skip ~/.gdbinit (plain GDB) | run / r | Start the program |
--args | Pass argv to the program | continue / c | Resume to the next stop |
-p PID | Attach to a running process | next / step | One source line, over / into calls |
-x FILE | Run GDB commands from a file | stepi / nexti | One instruction, into / over |
-batch | Run commands then exit | finish | Run to end of current function |
-ex 'CMD' | Run one command at startup | info registers | Dump CPU registers |
./core | Post-mortem from a core dump | x/FMT | Examine 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:5confirms where you stopped. A wrong-looking line number means an optimized or stripped build.- Register dump is ground truth:
ripis the next instruction,rspthe stack top, andrdi/rsi/rdxthe first call arguments. x/output depends entirely on the format letter:sstring,iinstruction,xg8-byte hex. Wrong letter, wrong meaning.Program received signal SIGSEGVis a bad memory access. Ifriporrsphold bytes you fed in, you control the crash in your lab binary.No symbol table is loadedis normal for stripped CTF binaries; work by address withdisassembleandstepi.- Empty or surprising values usually mean the wrong frame (
up/down) or a variable out of scope at this point.
Troubleshooting#
| Problem | Cause | Fix |
|---|---|---|
command not found | GDB not installed / stale PATH | Reinstall; open a fresh shell |
| No GEF/pwndbg banner | ~/.gdbinit not sourcing it | Re-run the installer; check ~/.gdbinit |
No symbol table is loaded | Binary is stripped | Work by address: disassemble, stepi, break *addr |
| Breakpoint never hits | Wrong address or dead code | Confirm the address from disassemble; check the call path |
ptrace: Operation not permitted | yama ptrace_scope restriction | echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope (lab box) |
| Wrong lines / missing locals | Optimized build (-O2) | Recompile your own code with -g -O0, or accept asm level |
No core file written | Core dumps disabled | ulimit -c unlimited, then reproduce the crash |
x/ prints junk | Wrong format letter | Match format to data: s string, i instruction, xg hex |
| Program detects the debugger | Anti-debug via ptrace | Patch the check or use a different approach, in your lab |
Gotchas#
- Load only one front-end. GEF and pwndbg both rewrite
~/.gdbinitand clash if sourced together; keep two init files and switch, or comment one out. stepsteps into library calls and can strand you deep inprintfinternals with no source. Usenextover calls, orfinishto climb back out.- x86-64 passes arguments in registers, not on the stack. Reading
$rspfor a function’s first argument is the classic wrong-place mistake; readrdi rsi rdxinstead. - On a PIE binary, static addresses are only offsets. The base is randomized at load, so
break *0x1170may bind nowhere; break by symbol and let GDB resolve the real address, or disable ASLR for the session. set $rip = maindoes not fix up the stack. Jumping execution skips prologues and leavesrspandrbpinconsistent, so use it to probe a path, not to run cleanly to the end.set logging onwas renamed toset logging enabled onin 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.