Step 65. CPU and Registers — Seeing the Heartbeat of Execution

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 si and 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 = %d\n", 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 to rip — 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.
  • rsp and rbp start with 0x7fffff... — addresses in the stack region. A moment that connects to the map from Step 60.
  • rdi is 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 si once, 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+12main+19main+26main+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 crash\n");
    boom();
    printf("after the crash\n");
    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

  1. 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)
  2. Compile with gcc -g -O0, enter gdb, and mark a stop at each of the two functions (add and mul)
  3. Record the scene where arguments arrive in rdi/rsi at both functions (hexadecimal and decimal together)
  4. Record the value loaded in rax when each function ends, and cross-check by converting the hexadecimal to decimal
  5. Walk ten steps with si, writing down rip at every step, and draw that march with arrows in your notes
  6. 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+19main+26main+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?


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 si and 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 (r then bt)
  • [ ] 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. Click the checkbox in the sidebar to save your progress.