ᛉ Yggdrasil

Binary

GDB (GNU Debugger)

15 Sep 2026 · 6,813 words · 31 min

Contents

Legend: [x86] = 32-bit | [x64] = 64-bit | [GEF] = GDB Enhanced Features | [PWNDBG] = pwndbg extension | [VANILLA] = stock GDB, no plugin | [LIN] = Linux


1. What is GDB and Why It Matters

What it means: GDB (GNU Debugger) is the standard command-line debugger for Linux binaries (ELF). It lets you pause a running program, inspect its registers and memory, step through instructions one at a time, and modify its state on the fly. For exploit development and CTF pwn challenges, GDB is where you actually watch a buffer overflow happen — confirm the offset, see EIP/RIP get overwritten, inspect the stack layout, and verify your payload lands correctly before firing it at a real target.

Stock ("vanilla") GDB is functional but low-level and unfriendly — no color, no register/stack view, cryptic x syntax. In practice almost nobody uses vanilla GDB for pwn work; everyone layers an extension on top:

┌───────────────────────────────────────────────────────────┐
│                     GDB Ecosystem                          │
│                                                             │
│  Vanilla GDB        →  works, but bare-bones               │
│       +                                                     │
│  GEF / pwndbg / PEDA → adds:                                │
│       - Colorized register/stack/disas context on each stop│
│       - checksec, vmmap, heap-chunk, got, plt views         │
│       - cyclic pattern generation built-in                  │
│       - ROP gadget search                                   │
│       - Better `x`/examine syntax                           │
└───────────────────────────────────────────────────────────┘

Why Learn GDB?

✔ Confirm buffer overflow offsets and EIP/RIP control (OSCP BOF room)
✔ Step through shellcode instruction-by-instruction to catch bugs
✔ Inspect stack/heap layout for ROP chain construction
✔ Reverse engineer CTF binaries (crackmes, pwn challenges)
✔ Debug crashes and understand why a program segfaults
✔ Attach to processes launched by pwntools mid-exploit (gdb.attach)
✔ Read AT&T disassembly (GDB's default) and switch to Intel when needed

This note assumes Linux x86/x64 ELF binaries — the primary target for OSCP, CPTS, and CTF pwn challenges. GEF and pwndbg command sets are both covered since you'll encounter both in the wild.


2. Installation & Extensions (GEF / pwndbg / PEDA)

What it means: GDB ships with every Linux distro but is unusable for serious exploit dev without an extension. The three major ones — GEF, pwndbg, PEDA — all do roughly the same job (context display, pattern generation, better memory inspection) but have different command names and philosophies. Pick one and learn it deeply; you'll recognize the others' commands easily once you know one.

# GDB itself
sudo apt install gdb

# ── GEF (GDB Enhanced Features) — recommended, actively maintained ──
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
# Installs and auto-configures ~/.gdbinit

# ── pwndbg — most popular for CTF, deep pwntools integration ──
git clone https://github.com/pwndbg/pwndbg
cd pwndbg && ./setup.sh

# ── PEDA (Python Exploit Development Assistance) — older, still used ──
git clone https://github.com/longld/peda.git ~/.peda
echo "source ~/.peda/peda.py" >> ~/.gdbinit

# Verify GDB version
gdb --version

# Only ONE extension should be active at a time — they conflict if
# sourced together in .gdbinit. Comment out the ones you're not using.

2.1 Which One Should You Use?

Extension Best for Command style Notes
GEF General exploit dev, modern, actively developed gef> ... Best default choice in 2024+, Python 3
pwndbg CTF pwn, tightest pwntools integration pwndbg> ... Excellent heap analysis (bins, tcache)
PEDA Older tutorials/courses reference it peda> ... Less maintained, still fine to know

For OSCP/CPTS/CTF work: GEF or pwndbg are both excellent. This note gives the vanilla command first (works everywhere), then the GEF/pwndbg shortcut.


3. Starting GDB — Launch Options

What it means: How you invoke GDB determines what you're debugging (a fresh binary, a running process, a core dump) and with what arguments/input. Getting comfortable with launch flags saves huge amounts of time versus starting GDB plain and manually loading everything.

# Basic launch
gdb ./binary

# Quiet mode (skip the GPL banner — cleaner output)
gdb -q ./binary
gdb --quiet ./binary

# Pass arguments to the target program
gdb --args ./binary arg1 arg2
(gdb) run arg1 arg2              # equivalent, set at runtime

# Attach to an already-running process (by PID)
gdb -p 1234
sudo gdb -p $(pidof binary_name)

# Load a core dump for post-mortem analysis
gdb ./binary core

# Load a binary and immediately execute a GDB script
gdb -x commands.gdb ./binary

# Batch mode — run commands and exit (scripting/automation)
gdb -batch -ex "run" -ex "bt" ./binary

# Debug a binary with stdin redirected from a file
gdb ./binary
(gdb) run < input.txt

# Debug a setuid/setgid binary (needed for local privesc CTFs)
sudo gdb ./binary

3.1 First Things to Run Every Session

(gdb) file ./binary          # load a binary (if not passed at launch)
(gdb) set disassembly-flavor intel   # switch AT&T → Intel syntax (see §9)
(gdb) starti                 # start and break at the very first instruction
(gdb) start                  # start and break at main()

4. Breakpoints

What it means: A breakpoint pauses execution at a specific point so you can inspect state. This is the single most-used GDB feature — almost every debugging session is "set a breakpoint, run, inspect, step."

# Break at a function name
(gdb) break main
(gdb) break vulnerable_function
(gdb) b main                      # 'b' is shorthand for 'break'

# Break at a specific address
(gdb) break *0x08048560
(gdb) break *0x555555555169

# Break at a specific address relative to a symbol (useful with PIE)
(gdb) break *main+42
(gdb) break *vuln+0x1a

# Break at a line number in source (if compiled with -g / debug symbols)
(gdb) break file.c:25
(gdb) break 25                    # if already in that file's context

# Conditional breakpoint — only stops if condition is true
(gdb) break vuln if $eax == 0
(gdb) break vuln if i == 5

# Temporary breakpoint — deleted automatically after it's hit once
(gdb) tbreak main

# List all breakpoints
(gdb) info breakpoints
(gdb) info b

# Delete breakpoints
(gdb) delete 1                    # delete breakpoint number 1
(gdb) delete                      # delete ALL breakpoints
(gdb) clear main                  # delete breakpoint(s) at 'main'

# Enable / disable without deleting
(gdb) disable 1
(gdb) enable 1

# Run commands automatically every time a breakpoint hits
(gdb) commands 1
> print $eax
> continue
> end

4.1 Breaking on PIE Binaries (No Fixed Base Address)

# With PIE (Position Independent Executable) enabled, the binary loads
# at a randomized base each run — you CANNOT break on a raw address
# from a previous run. Use symbol names or offsets from the base instead.

(gdb) break main                  # symbol names still work fine
(gdb) break *main+0x20            # offset from a symbol also works

# To break at a raw offset into the binary (as shown by objdump),
# add the runtime base first:
(gdb) info proc mappings          # find the loaded base address
(gdb) break *(0x555555554000 + 0x1189)   # base + offset from objdump

5. Execution Control

What it means: Once stopped at a breakpoint, these commands move execution forward — one line, one instruction, into a function, or out of one. Knowing the difference between "step" and "next" (does it dive into function calls or skip over them?) is essential for not getting lost in library code.

# Start execution
(gdb) run                         # run freely until breakpoint/crash/exit
(gdb) run < input.txt             # with stdin redirected from a file
(gdb) run $(python3 -c "print('A'*200)")   # with generated argv input
(gdb) r                           # shorthand

# Continue after a breakpoint
(gdb) continue
(gdb) c
(gdb) continue 5                  # ignore breakpoint next 5 times, then stop

# Step by SOURCE LINE (requires debug symbols, -g)
(gdb) next                        # step over — does NOT enter function calls
(gdb) n
(gdb) step                        # step into — DOES enter function calls
(gdb) s

# Step by single MACHINE INSTRUCTION (works without debug symbols — key for BOF)
(gdb) nexti                       # step over one instruction (skip past 'call')
(gdb) ni
(gdb) stepi                       # step into one instruction (enter 'call')
(gdb) si

# Run until the current function returns
(gdb) finish
# Prints the return value in $eax/$rax on x86/x64 (cdecl / SysV convention)

# Run until a specific line/address is reached (without setting a breakpoint)
(gdb) until 30
(gdb) until *0x08048560

# Kill the currently running program (without exiting GDB)
(gdb) kill

# Restart from scratch
(gdb) kill
(gdb) run

Rule of thumb for exploit dev: use nexti/stepi (instruction-level), not next/step (source-line level) — stripped binaries and shellcode have no debug symbols, so source-line stepping won't work anyway.


6. Examining Memory — the x command

What it means: x (examine) is GDB's core memory-inspection command. Its syntax x/NFU ADDRESS looks cryptic at first but is extremely dense and fast once memorized: Number of units, Format, Unit size.

# Syntax: x/NFU address
#   N = count (how many units to display)
#   F = format: x=hex, d=decimal, u=unsigned, o=octal, t=binary,
#               s=string, i=instruction, c=char, a=address, f=float
#   U = unit size: b=byte, h=halfword(2B), w=word(4B), g=giant(8B)

# Examples
(gdb) x/20x $esp          # 20 hex words (4B each) from ESP     [x86]
(gdb) x/20x $rsp          # 20 hex giants (8B each) from RSP    [x64]  ← note: still 'x' fmt, size defaults per-arch
(gdb) x/20xg $rsp         # explicitly 20 hex QWORDS from RSP   [x64]
(gdb) x/20xw $esp         # explicitly 20 hex DWORDS from ESP   [x86]

(gdb) x/s   0x804a000     # interpret memory as a C string
(gdb) x/s   $rdi          # string pointed to by RDI (e.g. syscall arg)

(gdb) x/i   $eip          # disassemble ONE instruction at EIP
(gdb) x/i   $rip          # disassemble ONE instruction at RIP
(gdb) x/20i main          # disassemble 20 instructions starting at main
(gdb) x/5i  $pc-10         # 5 instructions around current PC

(gdb) x/4dw &myvar        # 4 decimal dwords at address of myvar
(gdb) x/8cb buf            # 8 bytes as characters — good for spotting ASCII

(gdb) x/1gx $rbp           # 1 hex giant (8B) at RBP — read saved RBP value
(gdb) x/1gx $rbp+8         # the QWORD just above saved RBP == saved return addr (x64)

# GDB remembers the last format/size — repeating 'x' or hitting Enter
# continues from where the last one left off
(gdb) x/20x $esp
(gdb) x                    # shows next 20 words automatically

6.1 Format & Unit Quick Reference

Format char Meaning Unit char Size
x hex b byte (1)
d signed decimal h halfword (2)
u unsigned decimal w word (4)
o octal g giant/quadword (8)
t binary
s null-terminated string
i machine instruction
c char
a address (resolves to nearest symbol)
f float

6.2 print vs x

# 'print' (p) evaluates a C-like expression and shows a typed value
(gdb) print $eax
(gdb) print/x $eax               # print in hex
(gdb) print buf                  # if 'buf' is a known variable
(gdb) print *(int*)$esp          # dereference ESP as an int pointer

# 'x' examines raw memory at an address — no type needed, just format+size
(gdb) x/4xb $esp

# Rule of thumb: use 'print' for values/expressions, 'x' for raw memory dumps

7. Registers

What it means: Registers hold the CPU's current working values — the instruction pointer (where execution is), the stack pointer, and general-purpose data. In exploit dev, watching EIP/RIP get overwritten by your payload is the literal moment you confirm control of execution flow.

# Show all general-purpose registers + flags
(gdb) info registers
(gdb) info r
(gdb) i r

# Show a single register
(gdb) info registers eax
(gdb) info registers rip

# print a register (can apply a format)
(gdb) print $eax
(gdb) print $rip
(gdb) print/x $eax               # hex
(gdb) print/d $eax               # decimal
(gdb) print/t $eax               # binary

# Register names — prefix with $ always
$eax $ebx $ecx $edx $esi $edi $esp $ebp $eip $eflags     # x86
$rax $rbx $rcx $rdx $rsi $rdi $rsp $rbp $rip              # x64
$r8  $r9  $r10 $r11 $r12 $r13 $r14 $r15                   # x64 extra
$pc                                # alias for $eip/$rip (arch-agnostic)
$sp                                # alias for $esp/$rsp

# Extended/vector registers (relevant for some CTF crypto/SIMD challenges)
(gdb) info all-registers          # includes FPU/SSE/AVX registers
(gdb) print $xmm0

7.1 EFLAGS / RFLAGS Bits

(gdb) info registers eflags
# Example output: [ CF PF AF ZF SF IF ]
#   CF = Carry Flag        SF = Sign Flag
#   PF = Parity Flag       TF = Trap Flag
#   AF = Auxiliary Flag    IF = Interrupt Flag
#   ZF = Zero Flag         DF = Direction Flag
#   OF = Overflow Flag

8. Examining the Stack

What it means: The stack holds local variables, saved registers, and return addresses — it's the primary target in a stack-based buffer overflow. Reading the stack correctly (and knowing which QWORD/DWORD is the saved return address) is the core mechanical skill of BOF exploitation.

# Dump the top of the stack
(gdb) x/20x $esp                  # 20 words from ESP    [x86]
(gdb) x/20gx $rsp                 # 20 giants from RSP   [x64]

# View stack as a "frame" — saved RBP + saved return address are always
# right after the local variables buffer, in this order (low → high addr):
#   [ locals / buffer ] [ saved RBP ] [ saved return address ] [ args ]
(gdb) x/2gx $rbp                  # QWORD at RBP = saved RBP; RBP+8 = return addr

# Vanilla GDB has no dedicated "stack view" — GEF/pwndbg print it automatically
# on every stop (see §14/§15). In vanilla GDB, x/Nx $esp is your stack view.

# Print current stack frame info
(gdb) info frame
(gdb) frame                       # show current frame
(gdb) frame 1                     # switch to frame 1 (caller)

# Show local variables of current frame (needs debug symbols)
(gdb) info locals

# Show function arguments of current frame (needs debug symbols)
(gdb) info args

8.1 Reading Off a Stack Frame Manually (Vanilla GDB, x64 example)

# Classic frame layout after 'push rbp; mov rbp, rsp; sub rsp, N':
#
#   High addr  ┌─────────────────────────┐
#              │ Caller's frame / args   │
#              ├─────────────────────────┤
#              │ Saved return address    │ ← RBP + 8
#              ├─────────────────────────┤
#              │ Saved RBP               │ ← RBP  (this is [rbp])
#              ├─────────────────────────┤
#              │ Local vars / buffer     │ ← RSP .. RBP-1
#   Low addr   └─────────────────────────┘

(gdb) x/1gx $rbp        # saved RBP
(gdb) x/1gx $rbp+8      # saved return address — THIS is what BOF overwrites
(gdb) x/20gx $rsp       # dump everything from RSP up through the frame

9. Disassembly

What it means: Disassembly turns raw machine code back into readable assembly. GDB's default output is AT&T syntax (mov %eax, %ebx) — most CTF and OSCP material uses Intel syntax (mov ebx, eax), so switching flavor is usually the first thing you do in a new session.

# Switch to Intel syntax (do this immediately — or put it in .gdbinit, see §17)
(gdb) set disassembly-flavor intel
(gdb) set disassembly-flavor att       # switch back if needed

# Disassemble a function
(gdb) disassemble main
(gdb) disas main                        # shorthand
(gdb) disas vulnerable_function

# Disassemble with raw opcode bytes shown (useful for finding bad chars/gadgets)
(gdb) disas /r main

# Disassemble with source interleaved (needs debug symbols)
(gdb) disas /s main

# Disassemble a specific address range
(gdb) disas 0x08048490,0x080484c0
(gdb) disas 0x08048490,+50            # 50 bytes from start address

# Disassemble around current PC (context view)
(gdb) x/10i $pc
(gdb) x/10i $pc-20                     # instructions before current PC too

# Show the CURRENT instruction only (what's about to execute)
(gdb) x/i $eip
(gdb) x/i $rip

9.1 TUI Mode — Split-Screen Disassembly View

# GDB's built-in Text UI — shows source/asm + registers in split panes
# without needing GEF/pwndbg. Handy on a bare install.

(gdb) layout asm              # assembly view
(gdb) layout regs             # + registers pane
(gdb) layout src              # source code view (needs debug symbols)
(gdb) layout split            # source + asm together

# Navigate within TUI
Ctrl+X, A       # toggle TUI on/off
Ctrl+X, 2       # cycle layouts
Ctrl+L          # refresh screen (if garbled)

# TUI can be flaky over SSH — GEF/pwndbg's context-on-stop is usually
# preferred by pwn practitioners instead of TUI mode.

10. Backtrace & Call Stack

What it means: A backtrace shows the chain of function calls that got you to the current point — "who called whom." Essential for understanding how you reached a crash, especially in larger binaries or after a SIGSEGV.

(gdb) backtrace
(gdb) bt                           # shorthand
(gdb) bt full                      # + local variables at each frame
(gdb) bt 5                         # only the innermost 5 frames
(gdb) bt -5                        # only the outermost 5 frames

# Move between frames to inspect their local state
(gdb) frame 2                      # jump to frame #2
(gdb) up                           # move one frame OUT (toward caller)
(gdb) down                         # move one frame IN (toward callee)

# Show where you are after moving frames
(gdb) info frame
(gdb) frame                        # print current frame summary

11. Watchpoints

What it means: A watchpoint pauses execution when a value CHANGES (or is read/written), rather than at a fixed location like a breakpoint. Useful for finding exactly which instruction corrupts a variable — e.g. tracking down where a canary gets overwritten, or catching a use-after-free write.

# Break when a variable's VALUE changes (write watchpoint)
(gdb) watch myvar
(gdb) watch *0x804a010            # watch a raw memory address
(gdb) watch $eax                  # watch a register (less common, but works)

# Break when a variable is READ (not just written)
(gdb) rwatch myvar

# Break on either read OR write
(gdb) awatch myvar

# List active watchpoints
(gdb) info watchpoints

# Delete a watchpoint (same 'delete N' as breakpoints — shared numbering)
(gdb) delete 2

12. Searching Memory & Patterns

What it means: In BOF work you constantly need to (a) generate a unique cyclic pattern to find your exact offset to EIP/RIP, and (b) search memory for strings like /bin/sh or a leaked address. Vanilla GDB can search memory natively; pattern generation is normally left to Metasploit's pattern_create/ pattern_offset, pwntools' cyclic(), or GEF/pwndbg's built-in pattern commands.

# ── Vanilla GDB: search memory for a byte string ──
(gdb) find 0x08048000, 0x08050000, "/bin/sh"
(gdb) find &buf, &buf+200, 0x41414141      # search for a 4-byte value

# ── Generate cyclic pattern (outside GDB, then feed into GDB's run) ──
msf-pattern_create -l 3000
python3 -c "from pwn import *; print(cyclic(3000).decode())"

(gdb) run $(python3 -c "from pwn import *; print(cyclic(3000).decode())")
# ... crashes ...
(gdb) print $eip                            # note the value, e.g. 0x6161616c
# find offset OUTSIDE gdb:
# msf-pattern_offset -l 3000 -q 0x6161616c
# python3 -c "from pwn import *; print(cyclic_find(0x6161616c))"

# ── GEF has this built-in (no external tool needed) — see §14 ──
gef> pattern create 200
gef> pattern search $eip

# ── pwndbg has this built-in too — see §15 ──
pwndbg> cyclic 200
pwndbg> cyclic -l 0x61616164

13. Modifying State (Registers, Memory, Flow)

What it means: GDB isn't read-only — you can rewrite registers, patch memory, and force execution to jump to an arbitrary address. This is invaluable for testing "if I control EIP, can I actually reach my shellcode?" without needing a full working exploit yet, and for patching out annoying checks (like alarm() calls or anti-debug tricks) while you work.

# Set a register
(gdb) set $eip = 0x08048490
(gdb) set $rip = 0x555555555169
(gdb) set $eax = 0
(gdb) p $eip = 0x08048490          # 'print' as an assignment also works

# Write to memory
(gdb) set {int}0x804a010 = 0x41414141
(gdb) set {char}0x804a010 = 0x41
(gdb) set {long}$rsp = 0xdeadbeef

# Patch a byte in memory (e.g., NOP-out a call to alarm())
(gdb) set {unsigned char}0x0804852a = 0x90     # single NOP byte

# Force a jump — redirect execution to an arbitrary address right now
(gdb) jump *0x08048490
(gdb) jump *main

# Return immediately from the current function with a chosen value
(gdb) return 0
(gdb) return

# Call a function directly from within GDB (useful to test 'win()' functions)
(gdb) call win()
(gdb) print win()
(gdb) call (int)system("/bin/sh")

14. GEF — Command Reference

What it means: GEF replaces GDB's bare prompt with a rich, colorized context screen every time execution stops — registers, stack, disassembly, and code all visible at once, plus dozens of extra commands (checksec, vmmap, heap, pattern generation, ROP search) that vanilla GDB doesn't have.

# ── Session control ──
gef> start                       # run and pause at main (like GDB's 'start')
gef> run                         # run freely
gef> run <<< "AAAA"              # run with piped stdin
gef> run $(python3 -c "print('A'*200)")   # run with generated argv

# ── Context display ──
gef> context                     # manually re-print the context screen
gef> ctx                         # shorthand
# Context auto-prints after every breakpoint/step/stepi by default

# ── Breakpoints & execution (same as vanilla, GEF just prettifies output) ──
gef> break main
gef> break *0x08048560
gef> continue
gef> nexti / ni
gef> stepi / si
gef> finish

# ── Examine memory (GEF adds smart type-following) ──
gef> x/20x $esp
gef> x/20i $pc
gef> hexdump $rsp --size 64      # nicer hexdump than raw x/

# ── Registers ──
gef> info registers
gef> print $eip
gef> registers                    # GEF's own colorized register dump

# ── checksec — binary protections at a glance (huge time-saver) ──
gef> checksec
# Shows: Canary / NX / PIE / RELRO / Fortify — all in one command

# ── vmmap — memory map of the running process ──
gef> vmmap
gef> vmmap libc                  # filter to libc mappings only

# ── heap analysis ──
gef> heap chunks
gef> heap chunk 0x55550000       # inspect a specific chunk
gef> heap bins                   # tcache/fastbin/unsorted bin state

# ── Pattern generation (replaces msf-pattern_create) ──
gef> pattern create 200
gef> pattern search $esp
gef> pattern search $eip

# ── Search memory for a string/bytes ──
gef> grep "/bin/sh"
gef> search-pattern "/bin/sh"
gef> search-pattern 0x41414141

# ── GOT / PLT ──
gef> got                         # show GOT entries and resolution state

# ── ROP gadget search (basic — ROPgadget/pwntools still better for complex chains) ──
gef> rop --grep "pop rdi"

# ── Process info ──
gef> pid
gef> process-search bash

14.1 GEF Config Tips

# Change context sections shown (fewer panes = less scroll)
gef> gef config context.layout "regs stack code"

# Save config permanently
gef> gef save

15. pwndbg — Command Reference

What it means: pwndbg covers similar ground to GEF but leans harder into CTF pwn workflows — its cyclic, heap, and ROP tooling integrate tightly with pwntools conventions, which is why many CTF players prefer it.

# ── Session control (same base commands as vanilla GDB) ──
pwndbg> run
pwndbg> start
pwndbg> break main
pwndbg> continue
pwndbg> nexti
pwndbg> stepi

# ── Context display ──
pwndbg> context                  # manually reprint (auto-shown on stop)
pwndbg> context regs
pwndbg> context stack
pwndbg> context disasm

# ── checksec ──
pwndbg> checksec

# ── vmmap ──
pwndbg> vmmap
pwndbg> vmmap libc

# ── Cyclic pattern (built-in, matches pwntools cyclic() exactly) ──
pwndbg> cyclic 200
pwndbg> cyclic -l 0x61616164     # find offset from a leaked/crashed value

# ── Heap analysis (pwndbg's strongest area) ──
pwndbg> heap
pwndbg> bins                     # all bins (tcache/fast/unsorted/small/large)
pwndbg> tcache
pwndbg> fastbins
pwndbg> malloc_chunk 0x55550000  # inspect one chunk in detail

# ── ROP gadget search ──
pwndbg> rop --grep "pop rdi"
pwndbg> ropgadget                # alias, if configured

# ── Search memory ──
pwndbg> search "/bin/sh"
pwndbg> search -t string "flag"

# ── GOT / PLT / symbols ──
pwndbg> got
pwndbg> plt

# ── Telescope — smart stack/memory dump, follows pointers recursively ──
pwndbg> telescope $rsp
pwndbg> telescope $rsp 30        # 30 entries
pwndbg> telescope $rbp-0x40

# ── Stepping until a specific condition (useful in loops) ──
pwndbg> nextcall                 # run until the next 'call' instruction
pwndbg> nextjmp                  # run until the next jump

# ── Print canary value directly ──
pwndbg> canary

15.1 telescope — Why It's pwndbg's Signature Command

# 'telescope' dumps a memory region and, for each QWORD/DWORD, shows what
# it POINTS TO recursively (string? another pointer? code?). This single
# command replaces manually chaining x/gx + x/s over and over.

pwndbg> telescope $rsp 20
# 00:0000│ rsp  0x7ffee...  —▸ 0x7ffee...  ◂— 'AAAABBBBCCCC'
# 01:0008│      0x7ffee...  —▸ 0x555555555169 (main+42) ◂— endbr64
# ...

16. checksec, vmmap & Binary Protections

What it means: Before touching a binary, you need to know what mitigations are active — this dictates your entire exploitation strategy (shellcode vs ROP, whether you need an info leak, whether SEH/canary defeats are needed).

# Inside GDB with GEF or pwndbg loaded
gef> checksec
pwndbg> checksec

# Outside GDB — standalone tools, same info
checksec --file=./binary            # from pwntools' checksec CLI
pwn checksec ./binary

# What each protection means:
Protection What it means Exploitation impact
Canary Random value before saved return address, checked before ret Must leak canary or find a non-overflow write primitive
NX / DEP Stack/heap not executable Can't jump to shellcode on stack — need ROP/ret2libc
PIE Binary itself loads at a randomized base Need an info leak to compute real addresses
RELRO (Full) GOT is read-only after linking Can't do GOT overwrite — target other primitives
RELRO (Partial) GOT is writable GOT overwrite is viable
ASLR (OS-level, not per-binary) Libraries/stack load at random base each run Need a leak to compute libc/stack addresses
# Check/toggle ASLR system-wide (for LOCAL testing only — never affects remote target)
cat /proc/sys/kernel/randomize_va_space
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space   # disable (0=off,1=on,2=on+heap)

# Memory map of a running/stopped process
gef> vmmap
pwndbg> vmmap
(gdb) info proc mappings          # vanilla GDB equivalent — no extension needed

17. .gdbinit — Configuration File

What it means: .gdbinit is GDB's startup script — every command in it runs automatically each time GDB launches. This is where you set Intel syntax, load GEF/pwndbg, and define custom aliases so you don't retype setup commands every session.

# Location: ~/.gdbinit (global) or ./gdbinit / .gdbinit in a project dir (local)

# ── Typical minimal .gdbinit for pwn work ──
cat ~/.gdbinit
# ~/.gdbinit example
set disassembly-flavor intel

# Load ONE extension only (uncomment the one you use)
source ~/.gef.py          # if using GEF
# source ~/pwndbg/gdbinit.py   # if using pwndbg

# Useful defaults
set pagination off         # don't pause output with "---Type <return>---"
set confirm off            # don't ask y/n before quitting with running inferior

# Custom command example — break at main and show initial context automatically
define pwn-start
    start
    context
end
# Reload .gdbinit changes without restarting GDB
(gdb) source ~/.gdbinit

Local .gdbinit safety note: by default, GDB refuses to auto-load a .gdbinit from the current directory (security feature — prevents a malicious binary's folder from running arbitrary GDB Python on you). If you want a project-local one, add to ~/.gdbinit: add-auto-load-safe-path /path/to/project


18. GDB Scripting — Python API

What it means: GDB embeds a full Python interpreter, letting you script complex, repeated inspection tasks — automating a "run until crash, dump registers, extract offset" loop instead of doing it by hand every time. GEF and pwndbg are themselves built almost entirely on this API.

# Enter GDB's Python interpreter interactively
(gdb) python
>print("hello from gdb python")
>end

# One-liner Python from the GDB prompt
(gdb) python print("hello")

# Run a Python script file
(gdb) source myscript.py

# ── Example: dump EIP every time a breakpoint hits ──
# myscript.py
import gdb

class PrintEIP(gdb.Command):
    """Custom command: print current EIP/RIP."""
    def __init__(self):
        super().__init__("print-pc", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        pc = gdb.parse_and_eval("$pc")
        print(f"[+] Current PC: {pc}")

PrintEIP()
(gdb) source myscript.py
(gdb) print-pc
# ── Common gdb.* API patterns ──
gdb.execute("break main")              # run a GDB command from Python
gdb.execute("run", to_string=True)     # capture output as a string
val = gdb.parse_and_eval("$eax")       # read a register/expression
gdb.Breakpoint("vuln_function")        # set a breakpoint programmatically

# Read process memory directly
inferior = gdb.selected_inferior()
mem = inferior.read_memory(0x08048000, 16)   # read 16 bytes

# Write process memory directly
inferior.write_memory(0x08048000, b"\x90"*4)

19. GDB with pwntools (gdb.attach)

What it means: pwntools can launch GDB, attach it to a process it just started, plant breakpoints, and drop you into an interactive GDB session — all from your exploit script. This is the standard CTF workflow: write the exploit, attach GDB before sending the trigger, step through it live.

from pwn import *

context.terminal = ['tmux', 'splitw', '-h']   # how GDB window opens — REQUIRED setting
# alternatives:
# context.terminal = ['gnome-terminal', '-e']
# context.terminal = ['tmux', 'splitw', '-v']   # horizontal split instead

elf = ELF('./binary')
context.binary = elf

# Start the process
p = process('./binary')

# Attach GDB to the already-running process, with breakpoints preset
gdb.attach(p, '''
break *main+42
break vuln_function
continue
''')

# ── Or attach + break with symbol offsets ──
gdb.attach(p, f'break *{elf.sym["vuln"]}\ncontinue')

# ── Debug from the very start (before any code runs) ──
gdb.debug('./binary', '''
break main
run
''')
# gdb.debug() = process() + gdb.attach() in one call, paused at entry

# ── Attaching to a REMOTE target is NOT possible the same way — ──
# ── you can only gdb.attach() to a LOCAL process pwntools started ──

p.interactive()   # after your GDB window is done, resume driving the tube here

19.1 Common gdb.attach() Patterns

# Break right before a suspected vulnerable call, inspect stack, then continue
gdb.attach(p, 'break *0x555555555169\ncontinue')

# Break, don't auto-continue — inspect manually then type 'c' yourself
gdb.attach(p, 'break vuln')

# Attach with GEF/pwndbg context auto-shown (works automatically if it's
# in your ~/.gdbinit — pwntools just launches gdb normally)

20. Remote Debugging (gdbserver)

What it means: gdbserver runs on the TARGET machine (e.g. a CTF box, a VM, an embedded device) and exposes a debugging port; your local GDB connects to it over the network. This lets you fully debug a binary running on a different machine/architecture than your attacking box — common for embedded/ARM CTF challenges or when you have shell access to a box but want to debug locally.

# ── On the TARGET machine ──
gdbserver 0.0.0.0:1234 ./binary          # start binary under gdbserver, listen on 1234
gdbserver 0.0.0.0:1234 --attach 1337     # attach to already-running PID 1337

# ── On your LOCAL attacking machine ──
gdb ./binary                              # load a local COPY of the same binary
(gdb) target remote 192.168.1.50:1234    # connect to the gdbserver

# From here, all normal GDB commands work exactly as if it were local:
(gdb) break main
(gdb) continue
(gdb) info registers

# Detach without killing the remote process
(gdb) detach

# pwntools equivalent for spawning gdbserver + connecting
from pwn import *
# ssh session to target, then run gdbserver over it, then connect local gdb —
# typically scripted per-environment; the manual two-terminal method above
# is far more common in CTF/OSCP practice than automating this.

21. Core Dumps

What it means: A core dump is a snapshot of a crashed process's full memory state saved to disk. Instead of reproducing a crash live, you can load the core dump straight into GDB and inspect exactly what the registers/stack/memory looked like at the moment of the crash — useful when a crash is hard to reproduce or happened during automated fuzzing.

# Enable core dumps (often disabled by default — ulimit is 0)
ulimit -c unlimited

# Check current core dump size limit
ulimit -c

# Where core dumps get written (varies by distro)
cat /proc/sys/kernel/core_pattern
# Common: 'core' in cwd, or piped to apport/systemd-coredump

# Run the binary until it crashes and produces a core file
./binary < crashing_input
ls core*                          # find the dump (e.g. core, core.1234)

# Load the binary + core dump together into GDB
gdb ./binary core
gdb ./binary core.1234

# Once loaded, inspect exactly like a live crash
(gdb) bt                          # see the call chain at crash time
(gdb) info registers              # register state at crash
(gdb) x/20x $rsp                  # stack at crash time

# systemd-coredump systems — retrieve a dump by PID
coredumpctl list
coredumpctl gdb 1234              # launches gdb on that core directly

22. Common BOF/CTF Workflow in GDB

What it means: This is the practical, step-by-step sequence you'll run in GDB for almost every stack-based buffer overflow challenge — whether it's a CTF pwn binary or an OSCP-style BOF. Pairs directly with the methodology in the Buffer Overflow reference note.

# STEP 1 — Load and check protections
gdb ./binary
gef> checksec                      # or: pwndbg> checksec

# STEP 2 — Find the crash / confirm the vuln
gef> run $(python3 -c "print('A'*500)")
# or with pwntools cyclic pattern:
gef> run $(python3 -c "from pwn import *; print(cyclic(500).decode())")

# STEP 3 — Read the crash state
gef> info registers            # note EIP/RIP value — is it your pattern?
gef> print $eip                 # e.g. 0x6161616c

# STEP 4 — Find exact offset
gef> pattern search $eip        # GEF tells you the offset directly
# or externally: cyclic_find(0x6161616c)

# STEP 5 — Confirm exact control
gef> run $(python3 -c "print('A'*OFFSET + 'BBBB')")
gef> info registers             # EIP should now read exactly 0x42424242

# STEP 6 — Inspect the stack for a landing spot (shellcode injection case)
gef> x/40x $esp                 # find where your 'C's / shellcode start
gef> print $esp                 # this address, if stable, may be your return target

# STEP 7 — If ASLR/PIE is involved, find the runtime base
gef> vmmap
gef> info proc mappings

# STEP 8 — Set a breakpoint right before 'ret' to verify stack just before jump
gef> break *vuln+123             # address of the 'ret' or 'leave;ret'
gef> continue
gef> x/gx $rsp                   # what's about to be popped into RIP

# STEP 9 — Step through your shellcode/ROP chain instruction by instruction
gef> stepi
gef> x/i $pc                     # confirm each instruction executes as expected

# STEP 10 — Once it works reliably in GDB, remove the debugger and fire at the
# real target (GDB changes stack layout slightly vs. running standalone —
# always do a final un-debugged local test before going remote)

23. Troubleshooting & Gotchas

What it means: A handful of GDB quirks trip up almost everyone early on — knowing them ahead of time saves real debugging time later.

# ── "No symbol table is loaded" ──
# The binary is stripped, or you forgot to load it.
(gdb) file ./binary

# ── Offsets found in GDB don't match the offset outside GDB ──
# GDB slightly changes the stack (extra env vars, different argv[0] length).
# ALWAYS verify your final offset with the binary run standalone (no GDB)
# before trusting it for a remote exploit. This is especially true for
# Windows Immunity Debugger too, not just GDB.

# ── Breakpoint address doesn't exist anymore after rerun (PIE) ──
# PIE binaries re-randomize their base every 'run'. Use symbol-based
# breakpoints (break main) or offsets from a symbol, not raw addresses
# from objdump, unless ASLR is disabled locally (see §16).

# ── 'run' seems to hang / no output ──
# The target may be waiting on stdin — check if it needs input, or if
# stdout is buffered (common with C's printf without a newline/flush).

# ── GEF/pwndbg commands "not found" ──
# Only one extension should be sourced in .gdbinit — check for conflicting
# 'source' lines if commands from both seem half-broken.

# ── Can't set a breakpoint on a stripped, no-PIE binary at a raw address ──
(gdb) break *0x08048490          # raw addresses always work regardless of symbols

# ── ptrace / permission denied when attaching to a PID ──
cat /proc/sys/kernel/yama/ptrace_scope
# 0 = anyone can ptrace any process owned by same user (needed for gdb -p)
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

# ── Disassembly shows AT&T syntax and it's confusing ──
(gdb) set disassembly-flavor intel      # do this every session, or put in .gdbinit

# ── "Program received signal SIGSEGV" but you don't know why ──
gef> context                     # re-print full context immediately after crash
gef> bt                          # backtrace to see the call chain

24. Cheat Sheet

Absolute Essentials (memorize these first)

gdb -q ./binary                       # launch quiet
(gdb) set disassembly-flavor intel    # Intel syntax
(gdb) start                           # run, break at main
(gdb) break FUNC | *ADDR              # breakpoint
(gdb) run [args] / run < file         # start execution
(gdb) continue / c                    # resume
(gdb) nexti / ni   stepi / si         # step 1 instruction (over / into)
(gdb) info registers / i r            # all registers
(gdb) x/20x $rsp                      # dump stack (x64)
(gdb) x/i $rip                        # current instruction
(gdb) disas main                      # disassemble a function
(gdb) bt                              # backtrace
(gdb) set $rip = 0x...                # overwrite a register

GEF Quick Reference

gef> checksec
gef> vmmap
gef> pattern create 200
gef> pattern search $eip
gef> heap chunks / heap bins
gef> got
gef> search-pattern "/bin/sh"
gef> rop --grep "pop rdi"

pwndbg Quick Reference

pwndbg> checksec
pwndbg> vmmap
pwndbg> cyclic 200
pwndbg> cyclic -l 0x61616164
pwndbg> telescope $rsp 20
pwndbg> bins / tcache / fastbins
pwndbg> rop --grep "pop rdi"
pwndbg> canary

x/NFU Format Reference

# N = count, F = format, U = unit
# Format:  x=hex d=dec u=udec o=oct t=bin s=string i=instr c=char a=addr f=float
# Unit:    b=byte(1) h=half(2) w=word(4) g=giant(8)

x/20xg $rsp     # 20 hex QWORDS from RSP     [x64 stack dump]
x/20xw $esp     # 20 hex DWORDS from ESP     [x86 stack dump]
x/s   $rdi      # string at RDI
x/10i $pc       # 10 instructions from PC

Stack Frame Cheat (x64, after push rbp; mov rbp,rsp)

[rbp]     = saved RBP
[rbp+8]   = saved return address   ← BOF target
[rbp-N]   = local variables / buffer

Register Names by Arch

x86: eax ebx ecx edx esi edi esp ebp eip eflags
x64: rax rbx rcx rdx rsi rdi rsp rbp rip
     r8 r9 r10 r11 r12 r13 r14 r15

pwntools GDB Integration One-Liner

from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./binary'); context.binary = elf
p = process('./binary')
gdb.attach(p, f'break *{elf.sym["vuln"]}\ncontinue')
p.interactive()

Setup Checklist Every New Binary

1. gdb -q ./binary
2. set disassembly-flavor intel      (or bake into ~/.gdbinit)
3. checksec                          (GEF/pwndbg)
4. start                             (break at main, see initial context)
5. disas main                        (get the lay of the land)
6. info functions                    (list all functions, spot 'win'/'vuln'/'flag')

Key tips for OSCP/CPTS/CTF work:

  • Always run checksec first — it decides your entire strategy (shellcode vs ROP, leak-needed vs not)
  • Use nexti/stepi, not next/step — stripped CTF binaries have no debug symbols for source-line stepping
  • GDB's stack layout is NOT identical to a standalone run — always confirm your final offset/payload against the binary run without GDB before trusting it against a remote target
  • set disassembly-flavor intel immediately, every session — or bake it into ~/.gdbinit and never think about it again
  • gdb.attach() from pwntools is the fastest CTF loop: write exploit → attach → break before the trigger → step through → confirm → fire for real
  • Disable ASLR locally (echo 0 > /proc/sys/kernel/randomize_va_space) only for your own local testing — never assume it's off on the real target

← Binary top ↑