roadmap
week 4 · day 27

Seeing It Run: gdb

Watch registers and the stack change live

Hard 36 min 180 xp
After this you can
  • Use gdb's core loop: break, run, step/next, stepi, continue
  • Inspect state: registers, memory (x/), variables (print), and the stack
  • Turn 'I think the code does X' into 'I watched it do X'
  • Debug at both the C level and the instruction level, and switch between them

The tool that ends guessing

Everything in this course has pushed one habit: predict, then verify. gdb is the verifier. It stops a program mid-execution and lets you look — at registers, at memory, at the stack, at variables — and step forward one line or one *instruction* at a time, watching state change. It turns "I think rsp does this" into "I watched rsp do this." For low-level work and for exploitation, gdb is not optional; it's how you *see*.

The core loop

Compile with -g (include debug info so gdb can map machine code to your C), then:

CommandDoes
break main / b file.c:12set a breakpoint (stop when execution reaches here)
run / rstart the program; it stops at the first breakpoint
next / nrun the next SOURCE line (stepping over calls)
step / snext source line, but step INTO calls
stepi / sirun ONE machine instruction (the low-level view)
continue / cresume until the next breakpoint or the end
print x / p xshow the value of a variable or expression
info registers / i rshow all registers
x/8gx $rspexamine memory: 8 giant (8-byte) hex words at rsp
backtrace / btthe call stack: who called whom

The examine command, decoded

x (examine) is the workhorse and its format looks cryptic until you decode it: x/ then a count, a format, and a size. x/8gx $rsp = show 8 items, as hex (x), each a giant 8-byte word, starting at the address in rsp. Common sizes: b (1), w (4), g (8); common formats: x hex, d decimal, i instruction, s string. So x/4i $rip disassembles the next 4 instructions; x/s buf prints buf as a string. Read the format as count-format-size and it's no longer magic.

a session: watch a stack frameout
1$ gcc -O0 -g frame.c -o frame
2$ gdb ./frame
3(gdb) break greet # stop when we enter greet()
4(gdb) run
5(gdb) info registers rsp rbp rip
6(gdb) x/8gx $rsp # the top of the stack: locals, saved rbp, return addr
7(gdb) x/gx $rbp+8 # <- the return address (day 14)
8(gdb) stepi # one instruction at a time; re-check rsp after push/pop
9(gdb) print buf # a local variable, by name (thanks to -g)
10(gdb) backtrace # confirm greet was called from main
Key idea
Everything you learned in weeks 1–2 becomes *visible* here. Break in a function and x/gx $rbp+8 is the return address from day 14. stepi over a push and watch rsp drop by 8 (day 11). i r after an add shows the flags from day 8. x/4i $rip shows the loop back-edge from day 10. gdb is where the abstractions you built become things you can point at — which is exactly why it's also the attacker's primary tool for understanding a target.

Two levels, one debugger

You can debug at the C level (next, print buf, list) when you have source, or drop to the instruction level (stepi, info registers, x/i $rip) when you don't — or when the bug is in the assembly itself. layout asm / layout regs (or the tui mode) show source, disassembly, and registers side by side. The ability to slide between "what my C is doing" and "what the machine is doing" is the whole skill, and gdb is built for exactly that switch.

Predict first
You're stopped just after a function's prologue. You run x/gx $rbp+8. Based on day 14's frame layout, what does that command show you, and how could you confirm it's correct?
Note
Install pwndbg (or GEF) — a gdb plugin that, on every stop, auto-displays registers, the stack, the disassembly around rip, and flags, and adds commands built for this kind of work. Plain gdb is enough to learn on (and you should, so you know what's underneath), but pwndbg removes friction once the fundamentals are yours. The offensive path assumes it.
finished reading?
Your task, you write the code

See what you've only reasoned about

Take your frame.c or callret.c from week 2 (or write a fresh function with a couple of locals and a nested call). Compile with -O0 -g and, in gdb, do ALL of: set a breakpoint in the function; print rsp/rbp/rip; examine the stack with x/8gx $rsp; find the return address at $rbp+8 and confirm it matches the post-`call` instruction in the caller's disassembly; stepi across a push or pop and watch rsp change by 8; print a local by name; and run backtrace to see the call chain. Write down what each command showed. This consolidates weeks 1-2 by making them visible.

deliverable: your program + a gdb session transcript with notes
build & run
$ gcc -O0 -g yourprog.c -o yourprog
$ gdb ./yourprog # break; run; i r; x/8gx $rsp; x/gx $rbp+8; stepi; p <local>; bt
self-review before running
  • You located the return address at $rbp+8 and matched it to the caller's post-call instruction
  • You watched rsp change by exactly 8 across a push/pop with stepi
  • You printed a local variable by name and inspected the stack with x/
  • backtrace showed the correct call chain
stretchSet a watchpoint (`watch myvar`) on a variable and let gdb stop the instant it changes — then read the instruction that changed it. Then try `layout regs` / `layout asm` to see registers and disassembly update live as you stepi. This is the view you'll use constantly in exploitation.

Self-check

01What does `stepi` do that `next` doesn't?
02In `x/8gx $rsp`, what do the 8, g, and x mean?
03Where do you find the return address of the current function in gdb?
04Why compile with -g before debugging?
0/4 correct · 0/4 checked