x86-64 assembly from the ground up

Registers and mov

The sixteen registers, why one of them has four names, and what mov really does: it copies a value without touching the place the value came from.

Part 2 of 12

Beginner12 min readBack to Courses

One register, four names

The processor has sixteen general registers, and each is sixty-four bits wide. Their names begin with r and the first eight are the historic ones: rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp. The rest are numbered, r8 through r15, because they were added later without letters left to give them.

Every one of those registers can also be named by part of itself. For rax the low thirty-two bits are eax, the low sixteen are ax, and the low eight are al. The names stack, so a program that moves a number into al has changed one byte of rax and left the other seven alone.

The one exception is worth memorising: writing the thirty-two-bit name clears the upper half. mov eax, 5 sets all sixty-four bits of rax to five, while mov al, 5 changes only the bottom byte.

The stack pointer rsp and the frame pointer rbp are ordinary registers with a convention attached: the processor uses rsp when a push or a call moves the stack, so writing to it by hand changes where the next return address lands.

mov copies, it never moves

mov is a copy. mov rbx, rax puts a copy of the value in rax into rbx and leaves rax exactly as it was. The name of the instruction is a small historical lie, and it is the first thing to unlearn.

The source can be an immediate, another register, or a place in memory. A label such as msg is not the text it names: it is the address of that text, a number. So mov rsi, msg loads an address, and mov al, [msg] loads the first byte found at that address.

Values also travel in the other direction. mov rsi, rbx puts the address held in rbx into rsi, which is how a program can keep one address and hand it to several calls without writing the label again.

The forms of mov

mov rax, 30        ; an immediate into a register
mov rbx, rax       ; a copy: rax still holds 30
mov eax, 30        ; also clears the top 32 bits of rax
mov al, 30         ; only the low byte changes
mov rsi, msg       ; a label is an address
mov al, [msg]      ; brackets read the byte at that address

Two lines through two registers

Run it, then delete the middle register: pass `one` straight to `rsi` for the first line and `two` for the second. The output is the same, which is the point of a copy.

Output
Click "Run" to execute your code

The two messages are written with one mov each. The program keeps each address in an ordinary register and then copies that register into rsi, which is exactly what a compiler does with a local variable.

Why the thirty-two-bit name clears the top

The rule is not an accident of NASM. When x86-64 arrived, it kept the old thirty-two-bit instructions working and made every write to a thirty-two-bit register clear the upper half. That removed a dependency on a value the new program had probably never set, and it is why mov eax, 5 produces a clean five in rax.

The eight-bit and sixteen-bit names are older and do not get the same treatment. They replace only their own bytes, because the other bytes are still meaningful to code that is using the wider register. That is useful when you are editing one field of a value and a trap when you expected a fresh start.

In practice, choose the name for the job. Use the full register for an address, and the thirty-two-bit name when you want a small number and a known zero above it. Reach for al only when you really mean the low byte.

Registers are not variables

A variable carries a name, a type and a lifetime. A register carries bits and nothing else: the same sixty-four bits can be an address, a count, a character or four packed bytes, and the instruction decides how to read them. The name only says which slice of the register you are touching.

There is also no automatic saving. Calling a function does not preserve registers for you, and spilling a value to the stack is a line you write yourself. That is why assembly code tends to keep a small number of values live and reuse them deliberately.

The processor does not track which register holds what. Nothing stops one line from treating rax as a count and the next from treating it as an address; the instruction decides, and a wrong choice reads a number as a pointer. That is why assembly rewards a short list of values and a comment beside each one.

Three habits cause most register bugs. Do not assume mov empties the source; it copies. Do not use al when you meant eax, or the top of the register keeps its old value. And do not keep anything in rcx or r11 across a syscall, because the kernel overwrites both on the way through.

Try these next

  • Fill rax with minus one, then set eax to five and check the whole value with a comparison.
  • Do the same with al and explain why the top bits survive.
  • Copy one label into three different registers and write the same message from each of them.

The same five, written two ways

Run it, then swap `mov eax, 5` for `mov al, 5` in the first block. Predict both lines before you run again: one works on the whole register and one edits a single byte.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

After mov rbx, rax, what does rax hold?