armviz

Writing a W register does more than it looks

The aliasing between an X register and its W counterpart is usually explained as "the W register is the low half". That is true about the read, and actively misleading about the write.

Reading W0 gives the low 32 bits of X0, sign or zero extended depending on the instruction. Writing W0 does not merely replace those 32 bits. It resets bits 63:32 to zero.

That asymmetry is the whole point. It means a value written through a W register is always a well-defined 32-bit quantity, never something with stale garbage in the top half.

Why the compiler depends on it

Consider a function taking a 32-bit argument. The convention says that argument arrives in W0, and that the upper 32 bits of X0 are don't care. A compiler that has just tested whether the argument was zero can now compare the whole 64-bit register:

and x8, x0, x0       // 64-bit test
cbnz x8, nonzero     // branch on the 64-bit value

If writing the argument had left unpredictable bits above bit 31, that test would be wrong. Because the caller's write zeroed them, the test is free and correct.

The same reasoning is why and w0, w0, w0 is the idiomatic way to zero a register. It reads as a no-op on a 32-bit register and in fact clears the top half too.

Where it bites

The trap is code that treats the two names as two views of one value and reads the X register after writing the W one, expecting the old top half to survive. It never does:

mov  x0, #0x1122334455667788
mov  w1, #0x0          // clears bits 63:32 of x1

If x1 had held something meaningful above bit 31, that value is gone. The W register is not a window onto the X register; it is a 32-bit register that happens to share storage.

The flag-setting detail

There is one more wrinkle worth knowing. Instructions that set the condition flags treat a W-form operation as a 32-bit operation in full: adds w0, w1, w2 sets NZCV from the 32-bit result, not the 64-bit one. So the flags and the register width agree, which is what lets a 32-bit comparison be tested with a single cmp and a single conditional branch.

Stack pointers and the same rule

WSP is subject to the same rule, and this is where it causes real bugs. Narrowing the stack pointer to 32 bits with a W register clears bits 63:32 of SP, which is almost never what anyone wants. Language runtimes that touch SP or hand 32-bit stack arguments across a call boundary are the places to look when this goes wrong, which is why the ABI keeps the stack pointer 64 bits wide even when every argument is 32 bits.