Assembly (x86-64, AArch64, RISC-V) Module 3 – How a CPU runs your program
Registers on x86-64, AArch64 and RISC-V
The register files of the three ISAs: how many registers each has, the stack, zero and link registers, and why writing eax clears rax's top half.
What you will learn
- List the general-purpose registers of each ISA and their narrower names
- Identify the special registers: stack pointer, zero register, link register and flags
- Predict what a 32-bit, 16-bit or 8-bit write leaves in the full 64-bit register
- Explain why writing eax clears the top half of rax while writing al does not
- Name the registers each platform reserves and why you must not clobber them
Before you start
What you’ll learn · Before you start
What you will learn
- List the general-purpose registers of each ISA and their narrower names
- Identify the special registers: stack pointer, zero register, link register and flags
- Predict what a 32-bit, 16-bit or 8-bit write leaves in the full 64-bit register
- Explain why writing eax clears the top half of rax while writing al does not
- Name the registers each platform reserves and why you must not clobber them
Before you start
On this page
A register file: The small set of named slots inside a processor that instructions read and write directly. It is the fastest storage a program has and holds the machine's visible integer state between memory accesses. is the small set of named slots inside a processor that instructions
read and write directly; it is the fastest storage a program has and the whole visible state of the machine between
memory accesses. x86-64 gives you 16 general-purpose register: A register a program may freely use for integer values and addresses, as opposed to a special register with a fixed job. x86-64 has 16 (rax..r15), AArch64 31 (x0..x30) and RISC-V 32 (x0..x31). (rax to
r15), AArch64 gives 31 (x0 to x30), and RISC-V gives 32 (x0 to x31). Each also keeps a few special
registers: a stack pointer: The register that addresses the top of the stack: rsp on x86-64, sp on AArch64 and sp (x2) on RISC-V. Push, pop and function prologues move it; losing it loses the stack., a return-address or link register: The register that holds the return address a call left behind: x30 on AArch64 and ra (x1) on RISC-V. A leaf function can return through it without using the stack; x86-64 instead keeps the return address on the stack., and on two of them a flags register.
This lesson lays the three files side by side, names the special registers, and then settles the rule that trips up
beginners most: on x86-64, writing the 32-bit name eax clears the top half of rax, while writing the 8-bit name
al leaves the rest alone. A model shows the bits change, and a real AArch64 run proves the same clearing there.
How many registers, and their narrow names
Every register comes in more than one width. On x86-64, rax is 64 bits, eax is its low 32, ax its low 16 and
al its low 8; the newer registers follow the pattern as r8/r8d/r8w/r8b. On AArch64, x0 is 64 bits and
w0 is its low 32, and there is no 16- or 8-bit name. On RISC-V every integer register is XLEN bits wide (64 on
RV64), and the instructions whose names end in w work on the low 32 bits. Walk through the three files one kind of
register at a time:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- General-purpose registers. x86-64 has 16 general registers, rax to r15. AArch64 has 31, x0 to x30. RISC-V has 32, x0 to x31, with ABI names like a0 and sp. Wider files mean fewer trips to memory.
- The stack pointer. Each file keeps one register for the top of the stack: rsp on x86-64, sp on AArch64 (register number 31 in some encodings), and sp (x2) on RISC-V. Functions grow and shrink the stack through it.
- The return address. x86-64 pushes the return address on the stack with call. AArch64 and RISC-V put it in a register first: x30 (the link register) and ra (x1). A leaf function then needs no stack at all.
- The zero register. RISC-V hard-wires x0 to zero: reads give 0 and writes are dropped. AArch64 has xzr/wzr in the same role for many instructions. x86-64 has no zero register; code uses xor eax, eax to make a zero.
- The flags. x86-64 keeps condition bits in RFLAGS and AArch64 in NZCV, both set by arithmetic. RISC-V has no flags register at all; it compares straight into a register with slt. The next lesson but one returns to this.
The figure counts the general-purpose registers, then the stack pointer, the return-address register, the zero register and the flags, for each ISA. x86-64’s 16 registers are a legacy of its history; AArch64 and RISC-V were designed later with room for about twice as many, which lets a function keep more values in registers before it has to spill them to memory.
Key idea
The narrow names are windows onto one register, not separate registers: al, ax and eax all name parts of the
same rax, and w0 is the low half of x0.
Checkpoint
Check: How many registers and their narrow names
Partial writes change the whole register
Because the narrow names are windows onto one 64-bit register, writing through a window raises a question: what happens to the bits outside it? The answer differs by width and by ISA, and it is the source of real bugs. A model holds one register and applies the writes each ISA allows:
"""Partial-register writes, modelled for MySmartCoPilot Learn.
x86-64 and AArch64 treat a narrow write of a register differently, and the rule surprises people. This model holds
one 64-bit register and applies the writes the two ISAs allow, printing the full 64-bit value after each. It states
the documented rule (Intel SDM for x86-64, Arm's A64 guide for AArch64); the AArch64 rule is also proved by a real
run in zeroext.s on this page.
"""
MASK64 = (1 << 64) - 1
def show(name, value):
print(f"{name:<22} -> 0x{value & MASK64:016x}")
def x86_writes():
# x86-64 rule (Intel SDM, vol. 1): writing a 32-bit name (eax) zero-extends into the full 64 bits; writing an
# 8- or 16-bit name (al, ax) leaves the other bits of the 64-bit register unchanged.
rax = 0xDEADBEEFCAFEF00D
show("start rax", rax)
rax = (rax & ~0xFF) | 0x11 # mov al, 0x11 : low 8 bits only
show("after mov al, 0x11", rax)
rax = (rax & ~0xFFFF) | 0x2222 # mov ax, 0x2222 : low 16 bits only
show("after mov ax, 0x2222", rax)
rax = 0x33333333 # mov eax, 0x33333333 : zero-extends to 64 bits
show("after mov eax, ...", rax)
def arm_writes():
# AArch64 rule (Arm A64 guide): writing the 32-bit view w0 always zeroes the top 32 bits of x0. There is no 8-
# or 16-bit register view that preserves the other bits; you use byte and halfword loads/stores for that.
x0 = 0xDEADBEEFCAFEF00D
show("start x0", x0)
x0 = 0x00000000_000000AB & MASK64 # mov w0, 0xAB : whole x0 becomes 0x...000000AB
show("after mov w0, 0xAB", x0)
print("x86-64: narrow writes")
x86_writes()
print()
print("AArch64: a 32-bit write clears the top half")
arm_writes() Output
x86-64: narrow writes start rax -> 0xdeadbeefcafef00d after mov al, 0x11 -> 0xdeadbeefcafef011 after mov ax, 0x2222 -> 0xdeadbeefcafe2222 after mov eax, ... -> 0x0000000033333333 AArch64: a 32-bit write clears the top half start x0 -> 0xdeadbeefcafef00d after mov w0, 0xAB -> 0x00000000000000ab
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 regfile_model.py
On x86-64 the rule is split. Writing an 8-bit name (al) or a 16-bit name (ax) leaves the other bits of rax
unchanged, so 0xDEADBEEFCAFEF00D becomes 0xDEADBEEFCAFEF011 after mov al, 0x11. But writing the 32-bit name
eax zeroes the top 32 bits: the same register becomes 0x0000000033333333, not 0xDEADBEEF33333333. Intel’s
manual defines exactly this: 32-bit operations clear bits 63:32, while 8- and 16-bit operations preserve them.
AArch64 has only the eax-style rule, and a real run proves it. x0 is set so only bit 32 is on; writing w0 to
zero should clear the whole register, and the exit status confirms it did:
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
# A real AArch64 run that proves a 32-bit write clears the top half of the 64-bit register. x0 is set to
# 0x0000000100000000 (only bit 32 set); then mov w0, #0 writes the 32-bit view. If the top half were preserved, x0
# would still be non-zero and the program would exit 1. It exits 0, so writing w0 cleared all 64 bits.
session.run("""
cat zeroext.s
clang -o zeroext zeroext.s
./zeroext; echo "exit=$?"
""") Output
$ cat zeroext.s
.text
.globl _main
.align 2
_main:
movz x0, #1, lsl #32 // x0 = 0x0000000100000000 (bit 32 set, low 32 all zero)
mov w0, #0 // write the 32-bit view w0: AArch64 clears the top 32 bits of x0
cmp x0, #0 // is x0 now completely zero?
cset w0, ne // w0 = 1 if x0 is non-zero, 0 if the top half was cleared
ret // exit status = w0
$ clang -o zeroext zeroext.s
$ ./zeroext; echo "exit=$?"
exit=0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 zeroext_session.py
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The register starts full. rax holds 0xDEADBEEFCAFEF00D. The names al, ax and eax are not separate registers; they are windows onto the low 8, 16 and 32 bits of the same rax.
- mov al, 0x11. Writing the 8-bit name al changes only the low byte. The rest of rax is left alone, so it becomes 0xDEADBEEFCAFEF011. Intel's manual defines this preserve-the-rest behaviour for 8- and 16-bit writes.
- mov ax, 0x2222. Writing the 16-bit name ax changes only the low two bytes, leaving the top 48 bits: rax becomes 0xDEADBEEFCAFE2222. Still the same register, still the rest untouched.
- mov eax, 0x33333333. Writing the 32-bit name eax is the exception: x86-64 zeroes the top 32 bits. rax becomes 0x0000000033333333, not 0xDEADBEEF33333333. This one rule catches people out.
- AArch64 clears the top 32 bits. AArch64 offers just x0 (64 bits) and w0 (low 32). Writing w0 always clears the top 32 bits, like eax; there is no al/ax-style view that preserves them. zeroext.s on this page proves it with a real run.
The program exits with 0, which the function only reaches when x0 became completely zero after mov w0, #0. So a
32-bit write on AArch64 always clears the top half, the same choice x86-64 makes for eax. There is no AArch64 name
that preserves the upper bits the way al and ax do; to change only part of a register you use byte and halfword
loads and stores, which the next lesson but one covers.
The zeroing eax that eats your high bits
A common bug is to build a 64-bit value in rax, then do a 32-bit operation such as add eax, ecx to update the
low half, expecting the top half to survive. It does not: the 32-bit result is zero-extended and the top 32 bits
are gone. Use the 64-bit names (rax, rcx) when the value is 64 bits wide.
Checkpoint
Check: Partial-register writes
The stack pointer, zero and link registers
A few registers have fixed jobs the hardware or the calling convention assigns. The stack pointer addresses the
top of the stack: rsp on x86-64, sp on AArch64 and sp (which is x2) on RISC-V. The link register holds
the return address a call left behind: AArch64’s x30 and RISC-V’s ra (x1); x86-64 has none and keeps the
return address on the stack instead. The zero register is RISC-V’s x0, hard-wired so reads give 0 and writes
are silently dropped, which is why addi x0, x0, 5 in the previous lesson’s encoding disassembled to li zero, 5
and left x0 at 0; AArch64 has xzr/wzr in the same role for many instructions. The figure above stepped through
each of these.
RISC-V also fixes gp (the global pointer) and tp (the thread pointer) by convention. Because RISC-V has no flags
register, it compares values straight into a general register with slt instead of setting condition bits, a design
difference the flags lesson returns to.
| Fixed job | x86-64 | AArch64 | RISC-V |
|---|---|---|---|
| Stack pointer | rsp |
sp |
sp (x2) |
| Return address | on the stack | x30 (link register) |
ra (x1) |
| Reads as zero | none | xzr / wzr |
x0, hard-wired |
| Condition flags | rflags |
nzcv |
none: compare into a register |
Read the table by row, not by column: every ISA needs a stack pointer, but only two keep the return address in a register, and only RISC-V answers “where are the flags?” with “there are none”.
Checkpoint
Check: Stack, zero and link registers
Registers the platform reserves
Not every general register is yours to use freely. Each platform reserves some so that shared code and the operating system keep working:
- AArch64
x18. AAPCS64 callsr18the platform register. Apple’s ARM64 document says not to usex18on Apple platforms, and on Windows on Arm it points at the thread environment block in user mode. On Linux it is not reserved, but portable code leaves it alone. - RISC-V
gpandtp. The psABI gives the global pointer and thread pointer fixed roles; clobbering them breaks global-variable access and thread-local storage. - The stack pointer and link register. Overwriting
sploses the stack; overwriting the link register before you save it means a function cannot return.
Reading across platforms:
| Reservation | x86-64 (System V) | AArch64 (AAPCS64) | RISC-V (psABI) |
|---|---|---|---|
| Platform register | — | x18 (reserved by the ABI) |
— |
| Global pointer | — | — | gp |
| Thread pointer | — | — | tp |
| Must never clobber | rsp; the pushed return address |
sp; x30 before you save it |
sp; ra (x1) before you save it |
The track’s exercises guard against this by filling callee-saved registers with known values before a call and checking them afterwards, so a function that quietly clobbers a reserved or callee-saved register is caught.
Key idea
Treat x18 on AArch64 and gp/tp on RISC-V as off-limits unless you own the whole program, and never destroy
the stack pointer or an unsaved link register.
Checkpoint
Check: Platform-reserved registers
Practise: read the register file
Use the model and the figures to answer these by hand. Start a register at 0x1122334455667788. What does it hold
after mov al, 0xFF? After mov eax, 0xFF on x86-64? After mov w0, #0xFF on AArch64? The practice questions walk
through each, then ask which ISA has no flags register and which register RISC-V hard-wires to zero.
Practice that adapts to you
Practise this lesson
10 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
On x86-64, why does writing eax clear the top 32 bits of rax, while writing al leaves the rest of rax
unchanged?
They are two different rules in the instruction set, and Intel’s manual states both. A 32-bit operation on a
general register (writing eax, ebx, and so on) zero-extends its result into the full 64-bit register, clearing
bits 63:32. An 8-bit or 16-bit operation (writing al or ax) writes only those low bits and leaves the rest of the
register unchanged. The reason behind the 32-bit rule is that it removes a false dependency: because the result does
not depend on the old top half, the processor need not wait for the previous value of rax before writing eax,
which helps out-of-order execution. The practical effect is a classic bug: code that builds a 64-bit value and then
uses a 32-bit operation such as add eax, ecx to tweak the low half loses the top half to zero. The model on this
page shows 0xDEADBEEFCAFEF00D becoming 0x0000000033333333 after mov eax, 0x33333333, and 0xDEADBEEFCAFEF011
after mov al, 0x11. AArch64 made the simpler choice: it has only x0 and w0, and a w0 write always clears the
top half, which the native run on this page proves.
These two pair a register fact with the slip it causes; the bank lists openers and follow-ups:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A 64-bit counter in hand-written x86-64 assembly keeps losing its high 32 bits after an update. Walk through how you would find and fix the bug, and say what the AArch64 port must watch for.
-
Mid to senior
A function written in assembly returns correct results alone but corrupts its caller after a call. How do reserved and callee-saved registers explain this, and how would you prevent it?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How many general-purpose registers do x86-64, AArch64 and RISC-V have?
16 on x86-64 (rax to r15), 31 on AArch64 (x0 to x30) and 32 on RISC-V (x0 to x31), where x0 always reads as zero.
-
Are al, ax, eax and rax four registers?
No: they are windows onto one 64-bit register, its low 8, 16, 32 and all 64 bits. In the same way w0 is the low half of x0 on AArch64.
-
Why does writing eax clear the top half of rax on x86-64 while writing al leaves it alone?
They are two separate rules in Intel's manual. A 32-bit write zero-extends into the whole register, which also spares the core from waiting on the old top half; 8-bit and 16-bit writes change only their own bits. Code that tweaks a 64-bit value with a 32-bit add loses the top half.
-
What happens to x0 when an AArch64 instruction writes w0?
The top 32 bits of x0 are cleared, every time. AArch64 has no 8-bit or 16-bit register names, so the 32-bit view is the only narrow write.
-
Which registers have fixed jobs: the stack pointer, the link register and the zero register?
The stack pointer is rsp on x86-64 and sp on AArch64 and RISC-V. The return address goes in x30 on AArch64 and ra on RISC-V, while x86-64 keeps it on the stack. RISC-V x0 and AArch64 xzr or wzr always read as zero, and RISC-V has no flags register.
-
Which registers should ordinary code never clobber?
x18 on AArch64, the platform register, and gp and tp on RISC-V, unless you own the whole program. Never destroy the stack pointer, and never lose a link register you have not saved.
Key takeaways
- x86-64 has 16 general registers (
rax..r15), AArch64 has 31 (x0..x30) and RISC-V has 32 (x0..x31); the narrow names (eax/ax/al,w0) are windows onto one register. - On x86-64, writing a 32-bit name clears the top 32 bits, but writing an 8- or 16-bit name leaves the other bits alone; the model shows both.
- AArch64 has only the 32-bit view
w0, and writing it always clears the top half ofx0, as the native run proves by exiting 0. - Special registers: the stack pointer (
rsp,sp,sp), the link register (x30,ra; x86-64 uses the stack), RISC-V’s zero registerx0and AArch64’sxzr/wzr; RISC-V has no flags register. - Each platform reserves registers:
x18on AArch64 (the platform register) andgp/tpon RISC-V; never clobber them, the stack pointer, or an unsaved link register.
Terms in this lesson
- general-purpose register
- A register a program may freely use for integer values and addresses, as opposed to a special register with a fixed job. x86-64 has 16 (rax..r15), AArch64 31 (x0..x30) and RISC-V 32 (x0..x31).
- link register
- The register that holds the return address a call left behind: x30 on AArch64 and ra (x1) on RISC-V. A leaf function can return through it without using the stack; x86-64 instead keeps the return address on the stack.
- register file
- The small set of named slots inside a processor that instructions read and write directly. It is the fastest storage a program has and holds the machine's visible integer state between memory accesses.
- stack pointer
- The register that addresses the top of the stack: rsp on x86-64, sp on AArch64 and sp (x2) on RISC-V. Push, pop and function prologues move it; losing it loses the stack.
References
- Intel 64 and IA-32 Architectures Software Developer Manuals (general registers, partial-register writes) (Intel Corporation)
- Arm A64 Instruction Set Architecture Guide (general registers, wzr/xzr, the link register) (Arm Limited)
- Procedure Call Standard for the Arm 64-bit Architecture (AAPCS64): the platform register (Arm Limited)
- Writing ARM64 code for Apple platforms (x18 is reserved) (Apple Developer)
- Overview of ARM64 ABI conventions (x18 and the TEB on Windows) (Microsoft)
- RISC-V ELF psABI: integer register ABI names (ra, sp, gp, tp, a0..a7, s0..s11, t0..t6) (RISC-V International)
- The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA (x0 hard-wired to zero) (RISC-V International)
Report a problem with this lesson
Kept only in this browser. Your Learn progress