The General-Purpose Registers
Meet the 16 general-purpose registers, their conventional roles, and how to recognise them in a listing.
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 |
r12–r15 |
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 r12–r15, 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 (r8–r15). - 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(oreax). rsp(stack pointer) andrbp(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,r12–r15) and caller-saved scratch registers.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
The General-Purpose Registers - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!