x86-64 assembly from the ground up

Bytes, words and quadwords

The four widths a value can have, the data directives that reserve them, and the size word that keeps the assembler from guessing what a store should write.

Part 9 of 12

Advanced18 min readBack to Courses

Four widths, four names

A byte is eight bits, a word is sixteen, a double word is thirty-two and a quad word is sixty-four. The names are old and slightly rude to readers used to languages where an integer is an integer, but they are exact: a value in memory has a width and the width is part of the value.

db stores bytes, dw stores words, dd stores double words and dq stores quad words. resb reserves bytes without giving them a value, and it belongs in the bss section rather than in the data section. The register names carry the same information: al is a byte, ax a word, eax a dword and rax a qword.

The width also decides how many bytes an instruction touches and where the next value begins. A dw at address 100 occupies 100 and 101, so the dd after it starts at 102. Get the width wrong and every following label is read at the wrong offset, which is a fault that looks like corruption rather than a typo.

The assembler will not guess a width

The width of a load is easy: the destination register names it. A store from an immediate has no destination register at all, so the assembler has nothing to go on and says the operation size is not specified. mov byte [buf], 5 tells it, and mov word [buf], 5 writes two bytes instead — a difference that will surprise you later if it is a typo.

Loading a byte into a wider register is the other awkward case. movzx edx, byte [width] reads one byte and fills the rest with zeros, which is exactly what a count needs. movsx fills with copies of the sign bit instead, which is what a signed number needs. Without one of them, a leftover high half turns a small number into a huge one.

Declaring and reading each width

section .data
small db 200          ; one byte
mid   dw 40000        ; two bytes
big   dd 100000       ; four bytes
wide  dq 5            ; eight bytes
neg1  db -1           ; one signed byte
buf   resb 16         ; sixteen bytes, no value

section .text
_start:
  movzx eax, byte [small]   ; one byte, zeros above it
  movsx eax, byte [neg1]    ; one byte, sign above it
  mov byte [buf], 'x'       ; the size word names the store
  mov rax, [wide]           ; eight bytes, from the register

A count kept in one byte

Run it, then change the sixteen to a smaller number and watch the line get shorter. The count lives in one byte of memory, and `movzx` widens it to a full register for the call.

Output
Click "Run" to execute your code

A length stored as a byte and read as a quad word becomes a gigantic count. The write call would then try to send far more bytes than exist, and the answer is a fault rather than a longer line.

Who chooses the width of a memory access

A load has a destination register, and the register names the width: mov al, [x] reads one byte, mov ax, [x] two, mov eax, [x] four and mov rax, [x] eight. The assembler takes the width from the register and encodes it in the instruction.

A store from a register works the same way. A store from an immediate has no register to consult, so the line needs a size word in front of the destination: mov byte [x], 5, mov word [x], 5, and so on. Without it the assembler stops rather than guessing, which is a favour.

Reserved space follows the same units. resb 16 gives sixteen bytes, not sixteen values of some type. If you need eight quad words, reserve sixty-four bytes and keep the count in a register or an equ name.

Little-endian means the low byte comes first

A four-byte value is stored with its least significant byte at the lowest address. The number one is 01 00 00 00, not 00 00 00 01. Read the bytes in address order and you see the number back to front.

This matters the moment you mix widths. Reading a byte from a value that was stored as a double word gives you the low eight bits, which is the part that changes fastest. Reading four bytes starting one byte into the value gives you three bytes of one number and one byte of the next.

It also means a dump of memory is not a list of numbers in reading order. When a program prints raw bytes, the terminal sees whatever characters those byte values happen to name.

The same rule explains why a value built one byte at a time has to be shifted into place. Writing the low byte, then the next, means shifting the second by eight bits before storing it; store it unshifted and the second byte overwrites the first.

Three width mistakes. A store with no size word does not assemble. Loading a byte with mov rdx, [count] reads eight bytes, not one, and drags in the neighbouring data. And movzx on a signed value turns minus one into two hundred and fifty-five, so choose movsx when the sign matters.

Try these next

  • Change a byte count to a word count and update the load from movzx rdx, byte to movzx rdx, word.
  • Declare a byte with the value 300 and read the assembler warning about the truncated value.
  • Store minus one in a byte and load it once with movzx and once with movsx.

A quad-word count and a word count

Run it. The first length is an eight-byte value loaded straight into `rdx`; the second is a two-byte value widened by `movzx`. Then change the second count to `db` and its load to `byte` and check the output is unchanged.

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

How many bits are in a quad word?