x86-64 assembly from the ground up
Memory addressing and lea
Brackets turn a number into a place, a base register plus an index reaches into a block of bytes, and lea computes an address without reading one.
Brackets mean the memory at this address
mov rsi, msg copies an address into a register. mov al, [msg] reads the byte stored at that address. The brackets are the whole difference between a number and the memory the number points at, and this is the line where assembly starts to look like what it is.
How many bytes a load reads is decided by the destination or by a size word. mov al, [rbx] takes one byte, mov rax, [rbx] takes eight. When neither side says how wide, the assembler refuses the line rather than guessing, which is the sizes chapter in one sentence.
A register used this way is a pointer, and it can be moved. add rbx, 4 walks four bytes along the block, and the same [rbx] now reads the next value. Pointers in assembly are not a feature of the language; they are just registers holding addresses.
Base, index and what lea is for
An address inside the brackets can be built from three parts: a base register, an index register, and a fixed offset. [rbx] is one of them, [msg + 4] is a fixed offset from a label, and [rbx + rcx] adds a register, which is how a loop reaches the third byte or the tenth record.
lea loads an effective address: it does the same arithmetic as a bracket but stores the address itself instead of reading from it. lea rsi, [rbx + 1] puts the address one byte past the start into rsi, with no memory touched at all. The instruction is a small calculator that happens to use the bracket syntax.
That makes lea the natural way to pass a slice of a message to the write call: no copying, no second label, just an address that starts a little further in.
Four ways to reach the same bytes
mov rsi, msg ; the address of the message
mov al, [msg] ; the first byte, read through it
mov rbx, msg
mov al, [rbx] ; the same byte, via a register
lea rsi, [rbx + 1] ; an address one byte further on
; walk four bytes, one at a time
mov rcx, 4
.next:
dec rcx
lea rsi, [rbx + rcx]
mov rdx, 1
call write
test rcx, rcx
jnz .nextThe same message from two addresses
Run it, then change the one in `lea rsi, [rbx + 1]` to two and predict the output. The label `msg` never moves; only the address handed to the write call does.
The second line of output is the first line with its first letter missing, and not one byte was copied to produce it. Only the address in rsi and the count in rdx changed.
An address is arithmetic
Every operand inside brackets is built from the same pieces: an optional base register, an optional index register multiplied by a scale, and a fixed displacement. [rbx + rcx * 4 + 8] means start at rbx, add eight, then add four bytes for every unit in rcx. Write the parts in any order; the processor computes the same number.
The scale may only be one, two, four or eight, because those are the widths the hardware can shift cheaply. That is not a limitation in practice: a table of double words uses four, a table of quad words uses eight, and a byte array uses one and no scale at all.
The displacement is added last and can be negative. A loop that walks backwards through a table is the same addressing form with a negative step, not a different instruction.
lea without the load
lea runs the address arithmetic and stores the result instead of reading memory. That makes it a general small calculator: lea rax, [rbx + rcx * 4] is an add and a shift in one instruction, and it does not touch the flags the way add would.
It is also the clearest way to make a pointer to the middle of a block. lea rsi, [msg + 5] points five bytes into the message without a second label and without copying anything. The write call then sees a shorter message starting at that address.
The distinction to keep straight is the brackets. With mov, brackets mean read or write the memory. With lea, brackets mean calculate the address and stop there.
Three addressing mistakes. Dropping the brackets turns a load into a copy of the address. A scale other than one, two, four or eight will not assemble. And an index that runs one past the end of the block reads the next byte in memory, which may be another variable rather than a fault.
Try these next
- Walk a five-letter message and print one character per line using
[rbx + r8]. - Use
leato point at the last character of a message and write just that byte. - Build an address with a scale of four and read the second double word of a small table.
Walk a message one byte at a time
Run it, then change `asm` to `five`. The loop count comes from the label subtraction, so a longer word prints more lines with no other edit. Try removing the newline write to see the letters join.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What is the difference between mov rsi, rbx and mov rsi, [rbx]?
