Where the Work Happens

In Chapter 1 you read your first listing and saw names like eax, edi, and rbp fly past. Those are registers, and they are the single most important thing to understand about reading assembly. The CPU cannot do arithmetic on values sitting in memory the way C++ lets you write x + y on any variable. It works almost entirely on a tiny set of fast storage slots built directly into the processor. Those slots are the registers.

Think of registers as the CPU's working hands. To add two numbers, the processor first has to get both of them into registers, add them there, and then (maybe) put the result back into memory. Almost every line of assembly you read is moving values into registers, doing something to them, and moving the results out again.

x86-64 gives you 16 general-purpose registers. "General-purpose" means you can use them to hold integers, addresses, or anything else, as opposed to the specialised registers reserved for floating-point or vector work. This lesson introduces all 16 by name and the roles convention assigns to them.

The Sixteen Registers

Here they are, in their full 64-bit form:

Register Conventional role
rax Return value; also used as a scratch accumulator
rbx Callee-saved scratch register
rcx 4th integer argument
rdx 3rd integer argument; also return-value helper
rsi 2nd integer argument
rdi 1st integer argument
rbp Base/frame pointer
rsp Stack pointer
r8 5th integer argument
r9 6th integer argument
r10 Scratch register
r11 Scratch register
r12r15 Callee-saved scratch registers

Two things jump out. The first eight have names (rax, rbx, ...) that are holdovers from the 16-bit and 32-bit ancestors of this architecture. The second eight are simply numbered r8 through r15, added when the architecture grew to 64 bits. They are all equally usable; the names are just history.

The "conventional role" column matters more than it might look. Nothing in the hardware forces rax to hold a return value or rdi to hold the first argument. These are agreements, part of a contract called the calling convention that the whole system follows so functions can call each other. Chapter 4 covers that contract in full. For now, learning the roles is what lets you read a listing and immediately guess what each register is for.

Spotting the Argument Registers

The most useful pattern to memorise is the order arguments arrive in. The first six integer (or pointer) arguments to a function are passed in these registers, in this exact order:

rdi, rsi, rdx, rcx, r8, r9

It looks random, but you will internalise it quickly because you see it constantly. Here is a function that takes six arguments so we can watch them all land:

int sum6(int a, int b, int c, int d, int e, int f)
{
    return a + b + c + d + e + f;
}

At -O0 the compiler immediately stores each incoming argument into the stack frame, and the source register reveals the order:

sum6(int, int, int, int, int, int):
	push	rbp
	mov	rbp, rsp
	mov	DWORD PTR -4[rbp], edi
	mov	DWORD PTR -8[rbp], esi
	mov	DWORD PTR -12[rbp], edx
	mov	DWORD PTR -16[rbp], ecx
	mov	DWORD PTR -20[rbp], r8d
	mov	DWORD PTR -24[rbp], r9d
	mov	edx, DWORD PTR -4[rbp]
	mov	eax, DWORD PTR -8[rbp]
	add	edx, eax
	mov	eax, DWORD PTR -12[rbp]
	add	edx, eax
	mov	eax, DWORD PTR -16[rbp]
	add	edx, eax
	mov	eax, DWORD PTR -20[rbp]
	add	edx, eax
	mov	eax, DWORD PTR -24[rbp]
	add	eax, edx
	pop	rbp
	ret

Read down the first six mov instructions: edi, esi, edx, ecx, r8d, r9d. That is exactly the argument order. (Those are the 32-bit halves of rdi, rsi, ... because the arguments are ints; the next lesson explains why a 32-bit name appears for what is really a 64-bit register.)

Now look at the caller, main, setting the arguments up before the call:

main:
	push	rbp
	mov	rbp, rsp
	mov	r9d, 6
	mov	r8d, 5
	mov	ecx, 4
	mov	edx, 3
	mov	esi, 2
	mov	edi, 1
	call	sum6(int, int, int, int, int, int)
	nop
	pop	rbp
	ret

The compiler loads the constants 1 through 6 into exactly those argument registers (here in reverse order, which is harmless since they are independent), then calls the function. This is the calling convention in action: the caller puts arguments where the callee expects to find them.

The Two Pointers: rsp and rbp

Two registers in the table are not really for your data at all. rsp is the stack pointer; it always points at the top of the call stack. rbp is the base pointer (or frame pointer); at -O0 it marks a fixed reference point within the current function's stack frame. You saw both in every Chapter 1 listing:

	push	rbp
	mov	rbp, rsp

That prologue is the function setting up its frame. Because rsp and rbp have these dedicated jobs, you will rarely see arithmetic done on them the way you would on rax. When you spot them, think "stack bookkeeping," not "computation." Chapter 4 is devoted to exactly how they work.

rax: The Return Register

The other register you cannot avoid is rax. By convention a function's integer (or pointer) return value is left in rax (or its 32-bit half eax) when the function returns. Every Chapter 1 example ended with the result in eax right before ret, and you will see that pattern in nearly every function from here on. When you want to know what a function produces, look at what ends up in rax/eax just before the ret.

Caller-Saved vs Callee-Saved

One last distinction worth meeting now, even though it matters most in Chapter 4. Some registers are callee-saved: if a function wants to use rbx or r12r15, it must save the old value first and restore it before returning, so the caller never notices. The rest are caller-saved (scratch): a called function is free to clobber them, so if the caller needs a value preserved across a call, the caller must save it.

You can sometimes see this directly. Here is a function that calls two other functions and has to keep a value alive across the first call:

main:
	push	rbp
	mov	rbp, rsp
	push	rbx
	mov	edi, 5
	call	bump(int)
	mov	ebx, eax
	mov	edi, 2
	call	negate(int)
	add	eax, ebx
	mov	rbx, QWORD PTR -8[rbp]
	leave
	ret

Notice push rbx near the top and the restore (mov rbx, ...) before returning. The compiler chose rbx to stash the first result because rbx is callee-saved: it will survive the second call even though negate is allowed to trash the scratch registers. Recognising why a particular register was chosen is a real reading skill, and it starts with knowing which registers fall into which group.

Key Takeaways

  • The CPU does its work in registers: small, fast storage slots built into the processor. Most assembly is moving data into and out of them.
  • x86-64 has 16 general-purpose registers: the named eight (rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp) and the numbered eight (r8r15).
  • The first six integer/pointer arguments arrive in rdi, rsi, rdx, rcx, r8, r9, in that order.
  • A function's return value comes back in rax (or eax).
  • rsp (stack pointer) and rbp (frame pointer) are reserved for stack bookkeeping, not your data.
  • Registers are split into callee-saved (must be preserved across a call, e.g. rbx, r12r15) and caller-saved scratch registers.