Registers
The Register File
x86-64 gives you sixteen general-purpose registers, each 64 bits wide. They are called "general-purpose" because the CPU does not care what you keep in them — the roles below are conventions, not rules enforced by hardware.
| 64-bit | 32-bit | 16-bit | 8-bit | Traditional role |
|---|---|---|---|---|
rax | eax | ax | al/ah | Accumulator; return value |
rbx | ebx | bx | bl/bh | Base; callee-saved general use |
rcx | ecx | cx | cl/ch | Counter (loop, shift counts) |
rdx | edx | dx | dl/dh | Data; high half of multiply/divide |
rsi | esi | si | sil | Source index (string ops) |
rdi | edi | di | dil | Destination index (string ops) |
rbp | ebp | bp | bpl | Base/frame pointer |
rsp | esp | sp | spl | Stack pointer |
r8 … r15 | r8d … | r8w … | r8b … | Added by x86-64; no legacy names |
Sub-Registers: The Same 64 Bits
The names are not separate storage — they are windows onto the same register. eax is the low 32 bits of rax, ax the low 16, al the low 8. Writing to a narrow window leaves the bits above it unchanged, except for the 32-bit names, which clear the upper 32 bits to zero.
mov rax, 0x1122334455667788 ; rax = 11223344 55667788
mov al, 0xFF ; low byte replaced: rax = 11223344 556677FF
mov eax, 0x00000000 ; writing a 32-bit name ZEROES the top half:
; rax = 00000000 00000000
; This rule is why compilers frequently use the 32-bit form on purpose:
; `mov eax, 5` is a shorter encoding than `mov rax, 5`.
Figure 1 — one register, five names, and the zeroing rule that catches everyone once.
Why the Odd Legacy Names
ax meant "accumulator", bx "base", cx "counter", dx "data" — names from the 16-bit era when each register had one job and instructions encoded that register preferentially. The r8–r15 registers were added in 2003 with the 64-bit extension and have no such history.
Practical advice: do not name registers by their old role. Follow the calling convention instead, and use r8–r11 as scratch space when you need somewhere temporary to work.
Special-Purpose Registers
Four registers are not general-purpose at all — the CPU uses them for its own bookkeeping, and misusing them corrupts the program's control flow.
rip — The Instruction Pointer
rip holds the address of the next instruction. You never assign to it directly; instead you change it with a jump, a call, or a return. It is what makes branching, loops, and function calls possible.
rsp — The Stack Pointer
rsp points at the current top of the stack. On x86 the stack grows downward: push subtracts 8 from rsp and writes the value there; pop reads it and adds 8 back. Because call and ret use the same mechanism to store return addresses, rsp must be perfectly balanced — every push needs a matching pop.
push rax ; rsp -= 8; [rsp] = rax
push rbx ; rsp -= 8; [rsp] = rbx
pop rbx ; rbx = [rsp]; rsp += 8
pop rax ; rax = [rsp]; rsp += 8 (LIFO order!)
rbp — The Frame Pointer
rbp is a general-purpose register with a convention: functions copy rsp into rbp on entry so that local variables keep fixed offsets even as rsp moves. Compilers may skip this optimization at higher levels, but hand-written assembly usually keeps the frame pointer for readability.
The Flags Register
rflags is not a number you compute with — it is a collection of single bits describing the result of the last arithmetic or logical operation. It is the bridge between arithmetic and control flow.
| Flag | Name | Set when… |
|---|---|---|
ZF | Zero | the result was exactly zero |
SF | Sign | the result's top bit is 1 (negative, if signed) |
CF | Carry | an unsigned result carried out of the top bit |
OF | Overflow | a signed result has the wrong sign |
PF | Parity | the low byte has an even number of 1 bits |
Conditional jumps do nothing but read these bits. sub rax, rbx followed by je means "jump if the subtraction produced zero", which is how an equality test is spelled in machine code. Because almost every arithmetic instruction sets the flags as a side effect, competent assembly avoids redundant cmp instructions.
Registers and the Calling Convention
When your code calls a function written by someone else — or when someone else's code calls yours — you both must agree on where arguments go, where the result comes back, and which registers must survive the call. That agreement is the calling convention; on Linux x86-64 it is the System V AMD64 ABI.
| Role | Registers | Must survive a call? |
|---|---|---|
| First six integer/pointer arguments | rdi, rsi, rdx, rcx, r8, r9 | No — caller-saved |
| Return value | rax (and rdx for a 128-bit result) | No — caller-saved |
| Scratch registers | rax, rcx, rdx, rsi, rdi, r8–r11 | No — caller-saved |
| Preserved across calls | rbx, rbp, r12–r15, rsp | Yes — callee-saved |
| Stack pointer | rsp | Yes — must be restored exactly |
The distinction is a contract, and breaking it produces bugs that appear far from the mistake:
- Caller-saved (volatile): the callee may destroy these freely, so you must save them before a call if you still need the values.
- Callee-saved (non-volatile): if your function wants to use
rbxorr12–r15, you must push them on entry and pop them before returning.
my_func:
push rbx ; we intend to use rbx, so preserve it
push r12 ; ...and r12
mov rbx, rdi ; now safe to clobber rbx and r12
mov r12, rsi
; ... do work, result in rax ...
pop r12 ; restore in reverse order (LIFO)
pop rbx
ret
A Look at ARM64 Registers
If you learn one register file, the others become easy to read. ARM64 names its thirty-one general-purpose registers x0–x30, and the same convention idea applies:
| Role | ARM64 register | x86-64 analogue |
|---|---|---|
| Arguments 0–7 and return value | x0–x7 | rdi, rsi, … rax for the result |
| Link register (return address) | x30 / lr | stored on the stack by call |
| Stack pointer | sp | rsp |
| Zero register | xzr | none — write a throwaway register |
| Status flags | nzcv | rflags |
The biggest structural difference: ARM has a dedicated link register holding the return address, while x86 pushes it onto the stack automatically. That single design choice explains a lot of ARM assembly's shape.
Summary
- x86-64 has sixteen 64-bit general-purpose registers; each one is addressable at 64, 32, 16, and 8 bits.
- Writing to a 32-bit name like
eaxzeroes the upper half ofrax— a detail that matters when your pointers look wrong. rsp,rbp,rip, andrflagsare special: never repurpose them casually.- The System V ABI fixes argument registers, the return register, and which registers must be preserved across calls.
- ARM64 uses
x0–x7for arguments,xzrfor a zero register, andlrfor the return address.
Next: Data Definitions — how values leave the registers and take up residence in memory.