Seeing It Run: gdb
Watch registers and the stack change live
- › 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:
| Command | Does |
|---|---|
| break main / b file.c:12 | set a breakpoint (stop when execution reaches here) |
| run / r | start the program; it stops at the first breakpoint |
| next / n | run the next SOURCE line (stepping over calls) |
| step / s | next source line, but step INTO calls |
| stepi / si | run ONE machine instruction (the low-level view) |
| continue / c | resume until the next breakpoint or the end |
| print x / p x | show the value of a variable or expression |
| info registers / i r | show all registers |
| x/8gx $rsp | examine memory: 8 giant (8-byte) hex words at rsp |
| backtrace / bt | the 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.
1$ gcc -O0 -g frame.c -o frame2$ gdb ./frame3(gdb) break greet # stop when we enter greet()4(gdb) run5(gdb) info registers rsp rbp rip6(gdb) x/8gx $rsp # the top of the stack: locals, saved rbp, return addr7(gdb) x/gx $rbp+8 # <- the return address (day 14)8(gdb) stepi # one instruction at a time; re-check rsp after push/pop9(gdb) print buf # a local variable, by name (thanks to -g)10(gdb) backtrace # confirm greet was called from mainx/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.
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?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.
$ gcc -O0 -g yourprog.c -o yourprog$ gdb ./yourprog # break; run; i r; x/8gx $rsp; x/gx $rbp+8; stepi; p <local>; bt- › 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