Step 183. Assembly 2: gdb Basics — A Microscope for Running Programs
Level 3 — Real-World CTF and Advanced Attack Skills | Difficulty ★★★★☆ | Estimated time: 5 hours
Prerequisites: Step 182 (registers and stack frames). You can read
gcc -Soutput and draw a stack frame.
⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
- What you need: WSL Ubuntu (gcc, gdb). The measured environment is Ubuntu 24.04, gcc 13.3.0, gdb 15.1, x86-64.
- Caution: today’s practice is 100% safe. You’re merely observing a program you compiled with your own debugger. gdb extensions like gef/pwndbg require external downloads, so they’re not installed in this environment and are introduced as "Screen example" only.
gdb is a microscope that freezes a running program at a point of your choosing and lets you peer inside. Until yesterday we read compile outputs (.s files) as still images. Starting today you observe a living program — registers and the stack changing with every instruction. You’ll learn hands-on why Pwn attacks get designed in gdb by observing "where and how input piles up on the stack."
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain what
gcc -g -O0means (-g: debug info, -O0: optimization off) and build a debugging binary - Set a breakpoint (
b) andrunthe program up to that point - Know the difference between
next/ni/si/finishand pick the right one - Observe registers, variables, and memory with
info registers,print, andx/ - Read "where am I now" from the
=>marker indisassembleoutput
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | C + gdb, WSL Ubuntu bash (measured: gcc 13.3.0, gdb 15.1) |
| Today’s commands | b location, run, next/ni/si/finish, print variable, info registers, x/10gx $rsp, disassemble |
| Concepts needed | Breakpoints, instruction-level vs source-line stepping, debug info (-g), stack frames (Step 182) |
| Today’s artifact | gdb command cards + a function-call observation note |
2-1. How a Debugger Works — Freezing and Peering
A debugger does two things. ① Stop the program at a designated point (breakpoints), ② read registers, memory, and variables in that stopped state. The combination of the two "replays execution in slow motion."
If ordinary execution is "screening a film," debugging is "stepping frame by frame with a magnifier." And this microscope’s subject can be your own program, like today, or someone else’s sourceless binary (reversing).
2-2. The Stepping Family — next, ni, si, finish
"Advance one step" has four faces:
| Command | Unit | When it meets a function call |
|---|---|---|
next (n) |
One source line | Skips over (executes the function whole) |
step (s) |
One source line | Steps in |
ni |
One machine instruction | Skips over |
si |
One machine instruction | Steps in |
finish |
— | Runs to the end of the current function |
The difference between ni and si exists only when they hit a call. On flat ground they’re identical. Today we confirm this difference by measurement.
2-3. Reading Memory — The x/ Command’s Grammar
x/ is "examine" — a command for reading memory directly. The grammar is x/count-size-format address:
x/s address → read as a string (s = string)
x/4wx address → 4 items, in words (4 bytes), as hex (w = word, x = hex)
x/10gx $rsp → 10 items, in giant words (8 bytes), as hex — the standard for viewing a 64-bit stack
Variables go through print variable-name (shortened to p), and a variable’s address is &variable-name. x/s &name means "read as a string starting from name’s address."
2-4. gef and pwndbg — Screen Example
In real-world Pwn, gdb extensions like gef or pwndbg are standard. With every command, registers, stack, and disassembly auto-display on one screen (Screen example — not installed in this environment):
# Screen example — gef's stopped-screen layout
───────────────────────── registers ────
$rax: 0x11 $rdi: 0x11 $rsp: 0x7fffffffe640
───────────────────────── stack ────
0x7fffffffe640│+0x0000: 0x00007fffffffe670
───────────────────────── code:x86:64 ────
→ 0x555555555174 <square+11> mov eax, DWORD PTR [rbp-0x14]
Installation is one line after downloading a script from the internet, but getting comfortable with plain gdb first is today’s goal — because what the extensions show is ultimately the output of the commands you learn today.
3. Follow Along
3-1. A Practice Program and a Debugging Compile
Input (test.c)
#include <stdio.h>
int square(int n) {
int r = n * n;
return r;
}
int main(void) {
char name[] = "guardian";
int age = 17;
int sq = square(age);
printf("%s: %d\n", name, sq);
return 0;
}
Compile and test-run
gcc -g -O0 test.c -o test
./test
guardian: 289
(Measured 2026-09-09.)
How to read the options: -g is "load debug info — variable names, line numbers — into the binary," and -O0 is "optimization off." Without both, gdb doesn’t know variable names and the code order gets scrambled — you’ll see the actual symptoms in Wall 1.
3-2. Breakpoints and run — Freezing
gdb ./test
GNU gdb (Ubuntu 15.1-1ubuntu1~24.04.1) 15.1
...
Reading symbols from ./test...
(gdb)
Now you’re at gdb’s interactive prompt. Set a breakpoint on main and run:
(gdb) b main
Breakpoint 1 at 0x118e: file test.c, line 8.
(gdb) run
Breakpoint 1, main () at test.c:8
8 int main(void) {
(Measured 2026-09-09. The session outputs below were measured the same day; addresses differ per run.)
How to read it: b is a breakpoint — a "freeze here" marker. run starts the program. It froze at main’s entrance, and gdb shows "the line about to execute." Note that today’s sessions can also be run by collecting commands in a file with gdb -batch -x command-file ./test — convenient when repeating the same commands.
3-3. Observing Variables and Registers — Peering
(gdb) next
9 char name[] = "guardian";
(gdb) next
10 int age = 17;
(gdb) next
11 int sq = square(age);
(gdb) print age
$1 = 17
(gdb) x/s &name
0x7fffffffe65f: "guardian"
(gdb) info registers rip
rip 0x5555555551b6 0x5555555551b6 <main+52>
How to read it: three nexts passed two assignment statements, and the variables now hold values. $1 in print age is a result number gdb assigns. x/s &name shows the string as-is, and rip is the address of the instruction about to execute — the microscope’s crosshair. By the way, if you print age before enough nexts, you see a garbage value — the assignment hasn’t happened yet. That’s the meaning of "observation timing."
3-4. The ni/si Fork — At a call
Right now you’re at line 11, just before the square(age) call. From here, go instruction by instruction:
The case of skipping with ni
(gdb) ni (mov ... — an instruction loading age into a register)
(gdb) ni (mov edi, ... — the first argument into rdi)
(gdb) ni (call square — skipped! 0x...51c0, still line 11)
(gdb) ni (mov [rbp-...], eax — storing the result into sq)
12 printf("%s: %d\n", name, sq);
(gdb) print sq
$2 = 289
square executed whole and you arrived at line 12. sq holds the result 289. One thing to note — even right after skipping the call, you’re still on line 11. Since "assign to sq" is the same source line’s remaining instruction, you need one more ni to cross to line 12 (measured 2026-09-09).
The case of stepping in with si (from the same spot again)
(gdb) si (mov ...)
(gdb) si (mov edi, ...)
(gdb) si (call square — stepped in!)
square (n=0) at test.c:3
3 int square(int n) {
si met the call and came inside the function. It shows n=0 because this is the moment of entry — the argument hasn’t come down to the stack yet; the prologue hasn’t run (remember Step 182’s prologue).
3-5. Inside the Function — Registers and Stack, Measured
For observing inside a function, going straight there with b square is cleaner:
(gdb) b square
Breakpoint 2 at 0x1174: file test.c, line 4.
(gdb) run
Breakpoint 2, square (n=17) at test.c:4
4 int r = n * n;
(gdb) print n
$1 = 17
(gdb) info registers rdi rip
rdi 0x11 17
rip 0x555555555174 0x555555555174 <square+11>
How to read it: b square breaks on the first source line past the prologue. That’s why n is already 17. The calling convention, measured — the first argument 17 sits in rdi (0x11). rip points at square’s 11-byte mark.
Look at the stack:
(gdb) x/10gx $rsp
0x7fffffffe640: 0x00007fffffffe670 0x00005555555551c0
0x7fffffffe650: 0x0000001100000000 0x67007ffff7fe5af0
0x7fffffffe660: 0x006e616964726175 0xafa9197956204100
0x7fffffffe670: 0x00007fffffffe710 0x00007ffff7c2a1ca
0x7fffffffe680: 0x00007fffffffe6c0 0x00007fffffffe798
How to read the output: each line is an address plus two 8-byte chunks. Find three spots. ① The first chunk at 0x…e640, 0x…e670 — the saved old rbp (what square’s prologue pushed). ② Next to it, 0x5555555551c0 — the return address (the instruction after call in main). ③ 0x006e616964726175 at 0x…e660 — read these bytes backwards (little-endian): 67 75 61 72 64 69 61 6e, i.e., "guardian." main’s local variable name, alive on the stack. The stack isn’t an alien number board — it’s the site where the structures you learned actually pile up.
3-6. disassemble and => — Where Am I Now
(gdb) disassemble square
Dump of assembler code for function square:
0x0000555555555169 <+0>: endbr64
0x000055555555516d <+4>: push rbp
0x000055555555516e <+5>: mov rbp,rsp
0x0000555555555171 <+8>: mov DWORD PTR [rbp-0x14],edi
=> 0x0000555555555174 <+11>: mov eax,DWORD PTR [rbp-0x14]
0x0000555555555177 <+14>: imul eax,eax
0x000055555555517a <+17>: mov DWORD PTR [rbp-0x4],eax
0x000055555555517d <+20>: mov eax,DWORD PTR [rbp-0x4]
0x0000555555555180 <+23>: pop rbp
0x0000555555555181 <+24>: ret
End of assembler dump.
How to read the output: => is the current position — the very instruction rip points at. It matches 3-5, where rip was <square+11>. Every ni moves => down one cell. If you get lost, check your position with disassemble — the debugger’s "you are here" button.
3-7. finish — To the End of the Function
(gdb) finish
0x00005555555551c0 in main () at test.c:11
11 int sq = square(age);
Value returned is $3 = 289
(gdb) next
12 printf("%s: %d\n", name, sq);
(gdb) print sq
$4 = 289
How to read it: finish runs until the current function rets, and even reports the value it came back with. The return value 289 rode rax back to main, and after the assignment statement, sq holds 289 too. The calling convention’s last piece — "results via rax" — is confirmed in execution.
4. Missions & Exercises
Mission — A Function-Call Observation Note
- Compile the code below with
gcc -g -O0:
int add(int a, int b, int c) {
int s = a + b + c;
return s;
}
int main(void) {
int r = add(10, 20, 30);
return r;
}
- Stop with
b add, then confirm the three loaded arguments withinfo registers rdi rsi rdx - Find the return address at the stack top with
x/8gx $rsp, and compare it with the address of the instruction aftercallindisassemble main— do they match? - After
finish, confirm the return value (60) - Organize steps ①–④ into an "observation note" — the command, the observed value, and the agreement it confirmed (which clause of the calling convention)
Exercises
Exercise 1. If you open a binary compiled without -g in gdb and print variable-name, what happens? Why?
Exercise 2. When do ni and si behave differently? What about on flat ground (ordinary instructions)?
Exercise 3. Break down what each letter in x/10gx $rsp (10, g, x) means.
Exercise 4. What does => in disassemble output point at, and which register does it match?
5. Model Answers & Completion Criteria
Mission Model Answer
An example observation note (values follow the same principle as the 2026-09-09 measured test.c session):
[Observation note: add(10,20,30)]
① b add → run → stopped. info registers rdi rsi rdx
rdi=0xa(10), rsi=0x14(20), rdx=0x1e(30)
→ agreement confirmed: arguments ride in rdi, rsi, rdx order
② x/8gx $rsp → stack top = 0x5555...51xx
disassemble main → matches the address of the instruction right after call add
→ agreement confirmed: call pushes the return address onto the stack
③ finish → "Value returned is $N = 60"
→ agreement confirmed: the return value travels via rax
How to verify: ① does the note pair observed values with "agreements confirmed"? ② in the return-address comparison, did you confirm the two addresses match with your own eyes (don’t assume they’re equal)? ③ did you not forget -O0 and -g?
Exercise Answers
Answer 1. You get the error No symbol "age" in current context. (measured 2026-09-09). Compiling without -g leaves no variable-name or line-number info in the binary, so gdb knows only addresses, not the name "age." -g is essential for a debugging compile.
Answer 2. They differ only when hitting a call — ni executes the function whole and skips over, while si steps into the function. On ordinary instructions like mov and add, both are "advance one machine instruction" — identical.
Answer 3. 10 is the count, g is giant word (8 bytes, one cell of the 64-bit stack), x is hex display. In short: "10 items of 8 bytes each, starting where rsp points, in hex."
Answer 4. It points at the next instruction to execute — the current position. It matches the address the rip register points at — in 3-5 and 3-6, both were <square+11>.
Completion Criteria Checklist
- [ ] I can explain the roles of
-gand-O0and do a debugging compile - [ ] I froze a program at a desired function’s entrance with
bandrun - [ ] I can state the differences between
next/step/ni/si/finish - [ ] I read variables with
printandx/s, registers withinfo registers - [ ] I viewed the stack with
x/10gx $rspand found the return address and local variables - [ ] I confirmed the current position via
disassemble‘s=> - [ ] Mission: I completed the observation note for the add function call
6. Common Pitfalls & Fixes
Wall 1. No symbol "age" in current context.
Symptom: print age throws this error (measured 2026-09-09).
Cause: two possibilities. ① you compiled without -g, so there’s no variable-name info. ② you haven’t entered that variable’s scope (function) yet.
Fix: recompile with gcc -g -O0, and check you’re stopped inside the function where the variable lives. Stopped in main but printing square’s n gives the same error.
Wall 2. b function-name won’t set and asks a question
Symptom: it asks Make breakpoint pending on future shared library load? (y or [n]) (measured 2026-09-09 — when breaking on a nonexistent function name).
Cause: no function by that name exists — a typo, or the function got inlined by optimization.
Fix: check the typo. If the name is right but it won’t break, recompile with -O0 — optimization absorbs small functions into their call sites, erasing "the function’s body."
Wall 3. I printed and got a weird value
Symptom: you clearly assigned 17, but you get 0 or garbage.
Cause: you observed before passing the assignment statement. The line next shows is not "the line just executed" but "the line about to execute" (measured 2026-09-09 — while standing on line 10, age was still 0).
Fix: go one more next past the assignment, then observe. "Visible line = not-yet-executed line" is the first rule of reading a gdb screen.
Wall 4. I pressed ni but the line doesn’t change
Symptom: ni keeps displaying the same source line.
Cause: one source line is several machine instructions. A line like char name[] = "guardian" translates into multiple character-copy instructions (measured 2026-09-09 — line 9 needed several nis).
Fix: that’s normal — you asked for instruction-level stepping. If you prefer source-line units, use next.
Wall 5. Every number in the stack dump is unfamiliar
Symptom: you recognize nothing in x/10gx $rsp.
Cause: values the OS used are mixed into the stack too. Recognizing everything isn’t the goal.
Fix: practice finding just three things — ① the saved rbp (an address-shaped value the prologue pushed), ② the return address (starts with 0x5555…, code region), ③ data you know (today, the "guardian" bytes). The rest is background.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Debugger | A tool that freezes a running program and shows its insides |
| Breakpoint | A "freeze here" marker — set by function name, line number, or address |
| Debug info (-g) | A compile option loading variable names and line numbers into the binary |
| next vs step | Skip over a function or step into it (ni/si are their machine-level versions) |
| x/ command | Direct memory reading — specify count, size, format |
| => and rip | The current-position marker in disassembly = where rip points |
| gef/pwndbg | gdb extensions — gather today’s outputs onto one screen |
Today’s Commands
| Command | What it does |
|---|---|
gcc -g -O0 test.c -o test |
Debugging compile |
b main / b *address |
Set a breakpoint |
run |
Run up to the breakpoint |
next / ni / si / finish |
Step — source line / machine (skip) / machine (step in) / to function end |
print variable |
See a variable’s value |
info registers |
See registers |
x/10gx $rsp, x/s &variable |
Read memory and strings |
disassemble function |
See assembly (=> = current position) |
An Instinct More Important Than Commands
A debugger’s essence is not "memorizing commands" but asking questions. "What’s in rdi right now?" — info registers rdi. "What comes back when this function ends?" — finish. In today’s measurements we confirmed the calling convention’s three agreements (arguments, return address, return value) not in a book but in a living program.
This observational skill becomes an attack-design tool next. Observing a buffer overflow challenge in gdb means, just as "guardian" was found on the stack today, seeing which slot of the stack your input lands in. You’ve learned to use the microscope — what remains is training to find things worth looking at.
Once every box is checked, Step 183 is complete. Click the checkbox in the sidebar to save your progress.