Memory, little endian and sizes

Memory, little endian and sizes

Registers are the processor's small working storage, but a program usually has more values than fit in its sixteen general-purpose register families. Memory holds instructions, variables, and larger collections of data.

Memory is byte-addressed: each byte has a numerical address. An x86-64 general-purpose register can hold an address, and neighboring addresses select neighboring bytes. For example, the byte after the one at address 0x402000 is at 0x402001.

Names for places in memory

Assembly source uses labels so that you do not have to program with numeric addresses. A label ends with a colon and names the address at that point in the program. In this declaration, value names the address of the first byte of an eight-byte value:

value:  dq 0x1122334455667788

The assembler directives used in this lesson describe the program's layout:

sourcemeaning
section .textthe instructions
section .databytes with initial values
section .bssspace reserved for data, initially filled with zero bytes
db, dw, dd, dqput 1, 2, 4, or 8 bytes in .data
resq 1reserve one 8-byte quantity in .bss

These directives tell the assembler how to build the program; the processor does not execute them as instructions. The names word, dword, and qword mean 2, 4, and 8 bytes. They come from x86's history: a dword is a double word, and a qword is a quadruple word.

The other setup lines in the examples have small jobs too. global _start exposes the _start symbol to the linker as part of the standard runnable-program setup. default rel tells NASM to use relative addressing for label-based memory references such as [value], which is the usual choice for these programs.

Reading bytes through an address

Square brackets mean “access memory at this address.” If total is a label, [total] means the bytes beginning at the address named total:

    mov rax, [total]        ; read 8 bytes into rax
    add rax, [total]        ; read 8 bytes and add their value to rax

The register's width says how many bytes to read. rax is eight bytes wide, so both lines access eight consecutive bytes beginning at total. The ordinary two-operand mov and arithmetic forms used here allow one memory operand. For example, mov rax, [total] is valid, while mov [destination], [source] is not.

Little-endian byte order

x86 is little endian. For a multi-byte integer, the least-significant byte goes at the lowest address, the next byte goes at the next address, and so on. The least-significant byte is the rightmost pair of hexadecimal digits when a number is written normally.

Build this example, then enter 402000 in the memory panel's address box:

At successive increasing addresses, the fifteen bytes are:

address rangelabelbytes from low to high address
0x4020000x402007q88 77 66 55 44 33 22 11
0x4020080x40200Bd44 33 22 11
0x40200C0x40200Dw22 11
0x40200Eb11

The low byte 0x88 of q is at the lowest address, followed by 0x77, through to the high byte 0x11. An eight-byte load from q interprets the same sequence using little-endian order, so r12 receives 0x1122334455667788. A one-byte value has only one byte, so there is no ordering choice for b.

The exact addresses in that table belong to this playground. It deliberately gives the small examples a predictable layout, with their data beginning at 0x402000, so the bytes are easy to inspect. A real process can have many mapped regions, and its code, data, and stack may be placed at different addresses by its tools and operating system. Programs use labels such as [q]; they do not build in an address such as 0x402000.

The operand width selects the bytes

A label names a starting address; it does not force every later access to use the size from its declaration. These three instructions all begin at value, while their destination registers select three different widths:

    mov r8b, [value]        ; 1 byte:  0x88
    mov ebx, [value]        ; 4 bytes: 0x55667788
    mov r12, [value]        ; 8 bytes: 0x1122334455667788

The four-byte load starts with bytes 88 77 66 55 at increasing addresses. Little-endian interpretation makes their integer value 0x55667788. As in the previous lesson, writing ebx also clears the upper half of rbx.

Give immediate-to-memory stores a width

A register operand gives NASM a width in a line such as mov [total], rax. A numeric immediate does not. NASM therefore rejects this line because the memory operand's size is unspecified:

    mov [total], 5          ; assembly error: byte, word, dword, or qword?

Write byte, word, dword, or qword before the brackets to select 1, 2, 4, or 8 bytes. This example first fills two reserved qwords with 0xFF bytes, then performs stores of different widths:

With no .data section in this example, the playground places a at 0x402000. The sixteen bytes beginning there become:

05 FF FF FF FF FF FF FF   07 00 00 00 FF FF FF FF

The byte store changes only the first byte of a. The dword store changes the first four bytes of b; little-endian order places 07 first, followed by three zero bytes.

The chosen width also limits the value that can be stored. For example, 5000 = 0x1388. In mov byte [a], 5000, the explicit width is one byte, so only the low byte 0x88 can be encoded. NASM warns that the number overflows the byte field. Use a value that fits the selected width unless discarding the upper bits is deliberate.

Your turn

value holds the qword 0x1122334455667788. From the same starting address, load its lowest byte into r8b, its lowest four bytes into ebx, and all eight bytes into r12. The starting value in r8 makes the preserved upper bytes visible after the r8b write.

Show solution

Now make a partial store. slot begins as eight bytes of 0xFF. Put the word 0x1234 into its first two bytes and leave the other six unchanged. The resulting bytes should be 34 12 FF FF FF FF FF FF.

Show solution