C · Systems · Pwn
Step 65. CPU and Registers — Seeing the Heartbeat of Execution
Level 1 — Programming and the Computer’s Interior | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: Steps 56–64 complete; you’ve practiced extracting and reading assembly.
- What you need: a WSL Ubuntu terminal, gcc, gdb. Verified environment: Ubuntu 24.04, gcc 13.3.0, gdb 15.1, x86-64.
- Caution: today’s practice is 100% safe. You’re only pausing and peeking into a program you wrote with a debugger. Even the deliberate crash (segfault) happens only inside your own program.
Last time, names like rax and rdi kept appearing in assembly. Today you formally meet those boxes. They’re called registers, and every calculation the CPU performs happens on top of them. And today’s most important character is the box called rip — the signpost holding "the address of the next instruction to execute." If someone overwrites this box’s contents at will, the program goes wherever that person wants. Today is the day you move that signpost with your own hands.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain why the CPU computes in registers instead of memory
- State the roles of the four registers RAX, RSP, RBP, and RIP
- Pause a running program with gdb and look inside its registers
- Walk one instruction at a time with
siand observe rip’s march - Confirm the scene where arguments (rdi, rsi) and results (rax) travel through registers
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | C language, WSL Ubuntu bash, gdb 15.x (verified: 15.1, x86-64) |
| Today’s commands | gcc -g -O0 (compile for debugging), and inside gdb: b (set a stopping point), r (run), c (continue), si (advance one instruction), i r (view registers), bt (trace the road here), q (quit) |
| Concepts needed | Registers, the relationship between memory and registers, the roles of RAX·RSP·RBP·RIP, debuggers |
2-1. The Warehouse and the Desk — Memory and Registers
Memory (RAM) is big but far from the CPU. Registers are small but right next to the CPU. So the CPU takes ingredients out of the warehouse (memory), puts them on the desk (registers), computes on the desk, and puts the results back in the warehouse.
This is why last chapter’s mov meant "move it." An assembly program is a busy relocation of boxes on the desk.
2-2. The Roster of Major Registers
These are the faces of the 64-bit environment (x86-64). The R at the front of the name means 64-bit; in 32-bit environments names start with E (RAX → EAX).
| Register | Role |
|---|---|
| RAX | The representative box holding calculation results and function return values — "the report card of work" |
| RBX, RCX, RDX | General-purpose boxes. RCX is often used as a repetition counter |
| RSI, RDI | Boxes that hand arguments to functions (first argument RDI, second RSI) |
| RSP | The top position of the stack — this number moves whenever something is stacked or removed |
| RBP | The reference point of the current function — local variables are found by "how many slots from RBP" |
| RIP | The address of the next instruction to execute — the program’s signpost |
2-3. gdb — A Microscope That Pauses Execution and Looks Inside
gdb (GNU debugger) is a debugger — a tool that stops a running program at the point you want and lets you look inside. Today we use only seven commands.
| gdb command | Meaning (full form) |
|---|---|
b main |
Mark a stopping point (break) |
r |
Run (run) |
c |
Keep running — until the next stopping point (continue) |
si |
Advance one machine instruction (step instruction) |
i r |
View registers (info registers) |
bt |
View the path taken to get here (backtrace) |
q |
Quit (quit) |
These seven are enough for today.
2-4. RIP — The Signpost of Execution
The reason a program flows from top to bottom is that RIP walks forward, and the reason it comes back after calling a function is that RIP briefly goes and returns.
So what if someone overwrites this box’s contents? The program’s destination changes. The final goal of an exploit — the technique of using a vulnerability to make a program move however you please — is precisely the hijacking of this RIP. Today you see the face of that goal.
3. Follow Along
3-1. Preparing the Experiment Program
Input (reg.c)
#include <stdio.h>
int add(int a, int b) {
int result = a + b;
return result;
}
int main(void) {
int x = 40;
int y = 2;
int z = add(x, y);
printf("z = %dn", z);
return 0;
}
Compile
gcc -g -O0 reg.c -o reg
How to read it: -g is the option that says "include debugging information." Without it, gdb can’t recognize variable names and line numbers. Remember it as the default option for study-time compiles.
3-2. Entering gdb and the First Stop
gdb ./reg
(gdb) b main
Breakpoint 1 at 0x1173: file reg.c, line 9.
(gdb) r
Starting program: .../reg
Breakpoint 1, main () at reg.c:9
9 int x = 40;
(Verified 2026-09-09. Addresses and line numbers vary with the source. On your first run, a prompt may appear asking whether to enable debuginfod — answering n is fine.)
How to read the output: b main means "stop at main," and r means "run." The program froze at main’s first line. With time stopped, we can look inside. This "ability to stop time" is the essence of a debugger.
3-3. Looking Inside the Registers
(gdb) i r
rax 0x555555555167 93824992235879
rbx 0x7fffffffe648 140737488348744
rcx 0x555555557dc0 93824992247232
rdx 0x7fffffffe658 140737488348760
rsi 0x7fffffffe648 140737488348744
rdi 0x1 1
rbp 0x7fffffffe520 0x7fffffffe520
rsp 0x7fffffffe510 0x7fffffffe510
r8 0x0 0
...
rip 0x555555555173 0x555555555173 <main+12>
eflags 0x202 [ IF ]
(Verified 2026-09-09. Every value differs on each run.)
How to read the output: the current contents of all the boxes are visible. Only three things need pointing out.
<main+12>next torip— it means the place you’re stopped is "12 bytes from the start of the main function." gdb attaches a name tag next to the address like this.rspandrbpstart with0x7fffff...— addresses in the stack region. A moment that connects to the map from Step 60.rdiis 1 — main’s first argument (the argument count) is already loaded. By the convention, the first argument arrives in rdi.
Predict: each time you press
sionce, what happens to rip? Does it grow or shrink? Predict, then verify in the next section.
3-4. Walking One Instruction at a Time — rip’s March
(gdb) si
(gdb) i r rip
rip 0x55555555517a 0x55555555517a <main+19>
(gdb) si
(gdb) i r rip
rip 0x555555555181 0x555555555181 <main+26>
(gdb) si
(gdb) i r rip
rip 0x555555555184 0x555555555184 <main+29>
(Verified 2026-09-09.)
How to read the output: each time you execute one instruction (si), rip walks to the next instruction’s address. main+12 → main+19 → main+26 → main+29. The stride isn’t constant because each instruction occupies a different number of bytes (advancing 3–7 bytes at a time).
What a program’s execution is, was this march of rip. The invisible "flow of execution" is now visible as numbers.
Why: someone who has watched this march with their own eyes knows what "bending the flow" means. If you can send rip somewhere else, the march’s destination changes.
3-5. Following a Function Call — The Delivery of Arguments and Results
(gdb) b add
Breakpoint 2 at 0x1149: file reg.c, line 4.
(gdb) c
Continuing.
Breakpoint 2, add (a=40, b=2) at reg.c:4
4 int result = a + b;
(gdb) i r rdi rsi
rdi 0x28 40
rsi 0x2 2
(Verified 2026-09-09.)
How to read the output: c means "keep running (until the next stopping point)." Stopped at add, and rdi holds 0x28 (40 in hexadecimal), rsi holds 0x2. Exactly per the calling convention, the first argument 40 arrived loaded in rdi, the second 2 in rsi. gdb shows the decimal value alongside the hexadecimal.
Walk a few more steps and:
(gdb) si
(gdb) si
(gdb) si
(gdb) i r rax
rax 0x2a 42
(Verified 2026-09-09.)
How to read the output: 0x2a entered rax — 42 in hexadecimal. The result of 40 + 2 is loaded into rax per the convention, preparing to return. A live broadcast of the promise "results go in rax."
Review: what is hexadecimal 2a in decimal? Compute it with the technique from Step 50, then verify with
python3 -c "print(0x2a)".
3-6. Changing rip — The Legal Version of a Forbidden Experiment
gdb can also change registers. After stopping at main and walking a few steps, let’s turn the signpost back to the start of main.
(gdb) i r rip
rip 0x555555555181 0x555555555181 <main+26>
(gdb) set $rip = main
(gdb) i r rip
rip 0x555555555167 0x555555555167 <main>
(Verified 2026-09-09.)
How to read the output: set $rip = main means "turn the next instruction’s address back to the start of main." rip went back from main+26 to <main> (the starting point). We moved the march’s signpost with our own hands. If you keep running (c), the program executes again from the start of main.
Of course, this is an experiment inside the legal tool called a debugger. An attacker, without a debugger, attempts the same thing — swapping rip — through a vulnerability like a buffer overflow. Today, you have personally done what that thing "is."
Why: the sentence "hijacking RIP" is no longer a figure of speech. Changing the contents of that box. That’s all of it, and that’s why it’s defended so fiercely.
3-7. Segfault Investigation — The Debugger’s True Worth
Let’s deliberately cause a crash and perform the standard investigation of "where it died" with gdb.
Input (crash.c)
#include <stdio.h>
void boom(void) {
int *p = (int *)0;
*p = 1;
}
int main(void) {
printf("before the crashn");
boom();
printf("after the crashn");
return 0;
}
Compile and investigate
gcc -g -O0 crash.c -o crash
gdb ./crash
(gdb) r
Starting program: .../crash
before the crash
Program received signal SIGSEGV, Segmentation fault.
0x000055555555515d in boom () at crash.c:5
5 *p = 1;
(gdb) bt
#0 0x000055555555515d in boom () at crash.c:5
#1 0x0000555555555182 in main () at crash.c:10
(Verified 2026-09-09.)
How to read the output: normally "Segmentation fault" would be one line with the cause unknown, but if it dies inside gdb, the scene is preserved. "It died at line 5 of the boom function, at *p = 1." bt (backtrace) even shows the path — #0 is the scene (line 5 of boom), #1 is where the call departed (line 10 of main). It means "it came in the order main → boom."
Why: preserving a dead program’s scene and climbing back up the path. This is the difference between sprinkling printf and using a debugger, and it’s the starting point of every hard bug you’ll solve. These two steps — running until it dies with r, then viewing the path with bt — are the standard first steps of a death investigation.
4. Missions & Exercises
Mission — A Register Observation Log
- Modify reg.c to add one more function (e.g., add
int mul(int a, int b) { return a * b; }and call it from main) - Compile with
gcc -g -O0, enter gdb, and mark a stop at each of the two functions (add and mul) - Record the scene where arguments arrive in rdi/rsi at both functions (hexadecimal and decimal together)
- Record the value loaded in rax when each function ends, and cross-check by converting the hexadecimal to decimal
- Walk ten steps with
si, writing down rip at every step, and draw that march with arrows in your notes - Organize the roles of the nine registers (RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP, RIP) into a table. Repeat until you can fill it in without looking at the book
Exercises
Q1. Why does the CPU move values into registers to compute, instead of computing directly in memory?
Q2. In 3-4, rip walked in uneven strides: main+19 → main+26 → main+29. Why do the strides differ?
Q3. In 3-5, when add was called, rdi held 0x28 and rsi held 0x2, and when it ended, rax held 0x2a. What convention do these three observations prove?
Q4. If an attacker can change rip to an address of their choosing, what happens? So what are defensive techniques (ASLR, stack canaries) ultimately trying to protect?
Answers & completion criteria · expand/collapse
5. Model Answers & Completion Criteria
Mission Model Answer
An example observation log (verified format, 2026-09-09):
[at add] rdi = 0x28 (40), rsi = 0x2 (2) ← argument delivery confirmed
[after add] rax = 0x2a (42) ← 40 + 2 = 42 cross-checked
[at mul] rdi = 0x6 (6), rsi = 0x7 (7) ← same convention for the second function
[after mul] rax = 0x2a (42) ← 6 × 7 = 42 cross-checked
[rip's march] main+12 → main+19 → main+26 → main+29 → ...
(advancing by each instruction's size)
How to verify: ① Both functions must have records of arguments arriving in rdi/rsi. ② The decimal cross-check of the rax value must match the actual computation result. ③ The arrows in the march drawing must point only to the right (toward larger addresses) — the direction can change where a function call or jump is met, and marking those points makes an even better record.
Exercise Solutions
Q1 solution. Because memory is large but far from (slow for) the CPU. Registers are ultra-fast boxes right next to the CPU, so the CPU moves ingredients from memory into registers, computes, and puts results back into memory. This is why there are so many mov instructions in assembly.
Q2 solution. Because each machine instruction occupies a different number of bytes. Since rip is "the address of the next instruction," it advances by the size of the instruction just executed. Passing a 3-byte instruction means +3; passing a 7-byte one means +7.
Q3 solution. The calling convention. It confirms that the promise — "hand the first argument in rdi, the second in rsi, and receive the result in rax" — is kept exactly in a live execution. 0x28 = 40, 0x2 = 2, 0x2a = 42.
Q4 solution. The program jumps to and executes code designated by the attacker — a complete hijack of the execution flow. Defensive techniques ultimately protect this one thing. ASLR shuffles addresses so the attacker "doesn’t know the address to write," and stack canaries detect "overwriting up to the place where the address is written." Both exist to protect rip.
Completion Criteria Checklist
- [ ] I can explain the relationship between registers and memory as a warehouse and a desk
- [ ] I can state the roles of RAX, RSP, RBP, and RIP
- [ ] I can stop a program with gdb (
b,r) and view registers (i r) - [ ] I walked one instruction at a time with
siand observed rip’s march - [ ] I recorded the scene where arguments (rdi, rsi) and results (rax) travel through registers
- [ ] I personally performed the segfault investigation procedure (
rthenbt) - [ ] Mission: I completed the register observation log (argument/result/march records + role table)
6. Common Pitfalls & Fixes
Wall 1. Variable names and line numbers don’t appear
Symptom: b main works, but when it stops, all you see is this (verified 2026-09-09):
Breakpoint 1, 0x000055555555516f in main ()
Cause: you compiled without -g. With no debugging information, gdb doesn’t know line numbers or variable names.
Fix: recompile with gcc -g -O0. -g builds the debugger’s eyes.
Wall 2. Reading hexadecimal is hard
Symptom: that 0x2a is 42 doesn’t come to you right away.
Cause: your hands just aren’t used to hexadecimal yet. It’s normal.
Fix: use python3 -c "print(0x2a)" as a calculator. The reverse is python3 -c "print(hex(42))". Leaning on this calculator for a while is fine.
Wall 3. Confusing si and ni
Symptom: you try to advance one line but end up entering a function or skipping over it.
Cause: si advances "one machine instruction"; ni advances "one source line" (skipping over a function call as a single line).
Fix: today, use only si. Even when you want to enter a function, si is enough. Remember ni for the day you want to skip over a long function.
Wall 4. Trying to memorize the registers wholesale
Symptom: you try to cram the nine roles like an exam and get stuck.
Cause: the order is wrong. Memorization is the result of understanding, not the method.
Fix: nail down just four (RAX = report card, RSP = stack top, RBP = reference point, RIP = signpost), and look at the table for the rest whenever you experiment. You’ll memorize them by using them.
Wall 5. The set $rip command says nothing back
Symptom: you typed set $rip = main, but rip doesn’t look changed.
Cause: you may simply not have checked. This command prints nothing (verified 2026-09-09).
Fix: right after the command, always verify with i r rip. If it’s back at the <main> name tag, you succeeded.
7. Summary
Today’s Concepts
| Concept | One-line description |
|---|---|
| Register | Ultra-small boxes inside the CPU — all computation happens here |
| RAX | The report card of results and return values |
| RSP / RBP | Stack top / the current function’s reference point |
| RDI / RSI | Argument delivery boxes (first, second) |
| RIP | The address of the next instruction to execute — the signpost of execution |
| Debugger | A microscope that pauses execution and looks inside (gdb) |
Today’s Commands
| Command | What it does |
|---|---|
gcc -g -O0 |
Compile for debugging (builds the debugger’s eyes) |
gdb ./program |
Enter the debugger |
b name / r / c |
Mark a stopping point / run / continue |
si |
Advance one machine instruction |
i r (or i r rax) |
View registers |
set $rip = main |
Turn back the signpost (inside the debugger only) |
bt |
View the path taken to get here |
q |
Quit |
The Instinct That Matters More Than Commands
The scene that will stay longest from today’s experiments is rip walking each time you press si. Those few steps are the entirety of "execution." A computer being fast means it walks those steps billions of times per second, and a program going somewhere strange means the destination of those steps changed. A phenomenon that looked complex reduces to the story of a single signpost — this eye, seeing big things as a sequence of small ones, is the real skill in this field.
One more thing. These registers’ names and the promise "arguments go in rdi and rsi" are not laws of nature but promises people made and left in documents. So when the promise differs (a different CPU like ARM), the shapes differ too. The principle is the same; only the promises differ — carry this perspective, and unfamiliar environments stop being scary.
Once every box is checked, Step 65 is complete.
ONE STEP FURTHER
Finished this lesson?
Check the completion criteria, then mark your progress.