I still remember the exact bug that taught me to respect byte ordering: a network packet parser I wrote worked flawlessly on my x86 development machine and produced complete garbage the moment I ran it against data captured from a different kind of device. The values weren’t wrong — they were just stored in a different byte order than I expected. That bug is what pushed me to really understand how multi-byte data types are represented at the assembly level, and it’s a topic that quietly underlies almost everything from network programming to file formats to cross-platform debugging.
What Is a Multi-Byte Data Type?
A multi-byte data type is any value that requires more than a single byte (8 bits) of storage — words (16 bits), doublewords (32 bits), quadwords (64 bits), and beyond. Since memory is fundamentally addressed byte-by-byte, the CPU and its instruction set must define a consistent convention for exactly which byte goes where when a multi-byte value is stored or loaded.
Naming Conventions Across Architectures
| Size (bits) | x86/x86-64 Name | ARM Name | C Equivalent |
|---|---|---|---|
| 8 | Byte | Byte | char |
| 16 | Word | Halfword | short |
| 32 | Doubleword (dword) | Word | int |
| 64 | Quadword (qword) | Doubleword | long long / pointer (64-bit) |
| 128 | Double quadword (dqword) / XMM register | Quadword (Q register, SIMD) | __int128 / SIMD vector |
Notice that x86 calls a 32-bit value a “doubleword” while ARM calls it simply a “word” — the same bit-width has different names depending on which architecture’s manual you’re reading, which trips up a lot of newcomers moving between the two.
Endianness: The Core Concept
The single most important idea in multi-byte representation is endianness — the order in which the individual bytes of a multi-byte value are stored in memory.
Little-Endian (x86, x86-64, and ARM in its default mode)
The least significant byte is stored at the lowest memory address.
Big-Endian (network byte order, and ARM when configured for it)
The most significant byte is stored at the lowest memory address.
Let’s represent the 32-bit value 0x12345678 in memory, starting at address 0x1000:
| Address | Little-Endian Byte | Big-Endian Byte |
|---|---|---|
| 0x1000 | 0x78 | 0x12 |
| 0x1001 | 0x56 | 0x34 |
| 0x1002 | 0x34 | 0x56 |
| 0x1003 | 0x12 | 0x78 |
flowchart LR
subgraph LittleEndian["Little-Endian Memory Layout (0x12345678)"]
A1["Addr 0x1000: 0x78 (LSB)"] --> A2["Addr 0x1001: 0x56"]
A2 --> A3["Addr 0x1002: 0x34"]
A3 --> A4["Addr 0x1003: 0x12 (MSB)"]
end
subgraph BigEndian["Big-Endian Memory Layout (0x12345678)"]
B1["Addr 0x1000: 0x12 (MSB)"] --> B2["Addr 0x1001: 0x34"]
B2 --> B3["Addr 0x1002: 0x56"]
B3 --> B4["Addr 0x1003: 0x78 (LSB)"]
end
x86 and x86-64 are strictly little-endian. ARM is bi-endian — it defaults to little-endian in virtually all modern operating systems (Linux, Android, iOS, Windows on ARM), but the architecture technically supports a configurable big-endian mode as well.
Assembly Examples: Declaring and Accessing Multi-Byte Data
x86-64 (NASM Syntax)
section .data
my_byte db 0x7F ; 1 byte
my_word dw 0x1234 ; 2 bytes
my_dword dd 0x12345678 ; 4 bytes
my_qword dq 0x123456789ABCDEF0 ; 8 bytes
section .text
mov al, [my_byte] ; load 1 byte into AL
mov ax, [my_word] ; load 2 bytes into AX
mov eax, [my_dword] ; load 4 bytes into EAX
mov rax, [my_qword] ; load 8 bytes into RAX
ARM64 (GAS Syntax)
.section .data
my_byte: .byte 0x7F
my_half: .hword 0x1234
my_word: .word 0x12345678
my_dword: .quad 0x123456789ABCDEF0
.section .text
ldrb w0, my_byte ; load 1 byte, zero-extended into W0
ldrh w0, my_half ; load 2 bytes (halfword)
ldr w0, my_word ; load 4 bytes (word)
ldr x0, my_dword ; load 8 bytes (doubleword)
Sign Extension vs. Zero Extension
When loading a smaller multi-byte value into a larger register, the CPU must decide what to fill the extra bits with — this is where sign vs. zero extension matters.
x86-64
movzx eax, byte [my_byte] ; zero-extend: fills upper bits with 0
movsx eax, byte [my_byte] ; sign-extend: fills upper bits with the sign bit
ARM64
LDRB W0, [X1] ; load byte, zero-extended (implicit for LDRB)
LDRSB W0, [X1] ; load byte, sign-extended
Getting this wrong is a classic source of bugs — treating an unsigned byte as signed (or vice versa) silently corrupts arithmetic once the value is extended into a larger register.
Internal Working: Loading a Multi-Byte Value
sequenceDiagram
participant CPU as CPU Execution Unit
participant Bus as Memory Bus
participant Mem as Main Memory (byte-addressable)
CPU->>Bus: Request N bytes starting at address X
Bus->>Mem: Read bytes X, X+1, ... X+N-1
Mem-->>Bus: Return raw bytes
Bus-->>CPU: Deliver bytes according to endianness rule
CPU->>CPU: Assemble bytes into register value (LSB-first or MSB-first)
The key insight is that memory itself has no concept of “words” or “endianness” — it’s just an array of individually addressable bytes. Endianness is purely a convention the CPU (and the software reading raw memory) applies when interpreting a sequence of bytes as a single multi-byte number.
Practical Use Cases
- Network programming: Network protocols (TCP/IP) conventionally use big-endian (“network byte order”), while x86 hosts are little-endian — requiring explicit byte-swapping (
htons/ntohs/htonl/ntohlin C, orBSWAPin x86 assembly) before sending/receiving multi-byte fields. - File format parsing: Formats like PNG use big-endian fields, while formats like BMP use little-endian — assembly-level or low-level parsers must respect the specific format’s declared endianness.
- Cross-platform binary data exchange: Serializing data structures between a little-endian x86-64 server and a big-endian legacy system (or vice versa) requires deliberate conversion.
- Reverse engineering: Recognizing byte-swap instructions (
BSWAP,REVon ARM) in disassembly is a strong hint that the code is handling network or cross-endian data.
Byte-Swapping Instructions
x86-64
mov eax, 0x12345678
bswap eax ; eax becomes 0x78563412
ARM64
MOV W0, #0x5678
MOVK W0, #0x1234, LSL #16 ; W0 = 0x12345678
REV W0, W0 ; W0 becomes 0x78563412
Both BSWAP and REV exist specifically because byte-order conversion is such a common operation that hardware vendors provide a single dedicated instruction rather than forcing programmers to shift and mask manually.
Comparison: Endianness Trade-offs
| Aspect | Little-Endian | Big-Endian |
|---|---|---|
| Human readability in memory dumps | Less intuitive (least significant byte first) | More intuitive (reads left-to-right like written numbers) |
| Casting between sizes | Simpler — reading a smaller type from the start address of a larger one requires no offset | Requires offset adjustment when reinterpreting a smaller type from a larger one |
| Historical prevalence | Dominant in modern desktop, mobile, and server CPUs (x86, x86-64, most ARM deployments) | Common in network protocols and some legacy/embedded systems |
Debugging Multi-Byte Data
In GDB, you can inspect raw memory bytes and manually verify endianness assumptions:
(gdb) x/4xb &my_dword
0x404020: 0x78 0x56 0x34 0x12
(gdb) p/x my_dword
$1 = 0x12345678
The raw byte dump shows the little-endian storage order, while GDB’s p/x command correctly reassembles and displays the logical value — a good sanity check when debugging suspected endianness mismatches.
Common Mistakes
- Forgetting to convert endianness when reading/writing network protocol fields, leading to wildly incorrect values (e.g., a port number displayed as
13330instead of80). - Mixing sign-extension and zero-extension when widening smaller multi-byte types, corrupting arithmetic on negative values.
- Assuming ARM is always little-endian in every context — while overwhelmingly true for modern general-purpose OSes, the architecture does support big-endian modes in specialized configurations.
Best Practices
- Always explicitly document and, where necessary, convert endianness at protocol or file-format boundaries.
- Use the architecture’s dedicated byte-swap instruction (
BSWAP,REV) rather than manual shifting/masking for both clarity and performance. - Double-check sign vs. zero extension whenever widening a smaller data type, especially when the source value could be negative.
FAQs
Why is x86 little-endian? It traces back to design decisions in Intel’s early processors; little-endian simplifies certain hardware operations like incrementing a multi-byte counter starting from its lowest-address byte.
Is ARM little-endian or big-endian? ARM is bi-endian at the architecture level, but virtually all modern operating systems (Linux, Android, iOS, Windows on ARM) run it in little-endian mode by convention and default configuration.
Does endianness affect floating-point numbers too? Yes — floating-point values are also multi-byte and follow the same endianness convention as integers on a given architecture.
Summary and Key Takeaways
- Multi-byte data types (words, doublewords, quadwords) are built from sequences of individually addressable bytes, and their internal byte order is defined by endianness.
- x86/x86-64 is strictly little-endian; ARM defaults to little-endian in essentially all modern deployments but technically supports big-endian too.
- Sign extension vs. zero extension matters when widening smaller types into larger registers.
- Byte-swap instructions (
BSWAPon x86,REVon ARM) exist specifically to handle cross-endian data efficiently. - Understanding this topic is essential for network programming, file format parsing, and cross-platform debugging.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1 (data types and byte ordering)
- Arm® Architecture Reference Manual for A-profile architecture (endianness and data types chapter)
- IETF RFC 1700 and related networking standards documentation on network byte order
- GNU Assembler (GAS) documentation on data directives (
.byte,.hword,.word,.quad)
