I still remember the first time a network protocol completely broke on me because I forgot that the value I sent from my x86 machine wasn’t going to be read the same way on the receiving end. The bytes were all there, correctly, but in the “wrong” order — and that one bug taught me more about computer architecture than a semester of lectures. That bug was, of course, endianness.
In this post, I’ll break down exactly what little-endian and big-endian mean, why they exist, how they show up in real Assembly code on x86/x86-64 and ARM, and how to actually detect and convert between them when it matters — which, trust me, it eventually will.
What Is Endianness?
Endianness describes the order in which a computer architecture stores the bytes of a multi-byte value (like a 16-bit word, 32-bit doubleword, or 64-bit quadword) in memory.
Take the 32-bit hexadecimal value 0x12345678. It’s made up of four bytes: 12, 34, 56, 78. The question is: which byte gets stored at the lowest memory address?
- Little-endian: the least significant byte (LSB) is stored at the lowest address.
- Big-endian: the most significant byte (MSB) is stored at the lowest address.
Visualizing Byte Order
Here’s how 0x12345678 looks in memory under each scheme, assuming it starts at address 0x1000:
| Address | Little-Endian | Big-Endian |
|---|---|---|
| 0x1000 | 0x78 | 0x12 |
| 0x1001 | 0x56 | 0x34 |
| 0x1002 | 0x34 | 0x56 |
| 0x1003 | 0x12 | 0x78 |
Little-endian effectively stores the value “backwards” byte-wise, while big-endian stores it in the same order you’d naturally read it left-to-right, matching how we write numbers on paper.
flowchart LR
subgraph LE["Little-Endian Memory Layout"]
direction LR
A1["0x1000: 78"] --> A2["0x1001: 56"] --> A3["0x1002: 34"] --> A4["0x1003: 12"]
end
subgraph BE["Big-Endian Memory Layout"]
direction LR
B1["0x1000: 12"] --> B2["0x1001: 34"] --> B3["0x1002: 56"] --> B4["0x1003: 78"]
end
Which Architectures Use Which?
| Architecture | Default Endianness | Notes |
|---|---|---|
| x86 / x86-64 | Little-endian | Always little-endian, no exceptions |
| ARM (32-bit and AArch64) | Bi-endian | Little-endian by default (LE), but can run in big-endian mode (BE8/BE32) |
| MIPS | Bi-endian | Configurable at boot |
| PowerPC | Bi-endian, historically big-endian by default | Many modern PowerPC systems run little-endian |
| SPARC | Big-endian | Classic big-endian architecture |
| Network protocols (TCP/IP) | Big-endian | Called “network byte order” |
This is exactly why I got bitten early on: my program running on x86 (little-endian) was sending raw integer bytes over a socket, and the receiving side expected “network byte order,” which is big-endian by convention. The fix was calling byte-swapping functions like htonl() and ntohl() before transmission.
Why Does Little-Endian Even Exist?
It seems backwards at first, so why did anyone design it this way? A few practical reasons:
- Arithmetic convenience — When doing addition with carries, the CPU processes the least significant byte first. If that byte is already at the lowest address, the hardware can start crunching numbers without needing to first locate the “end” of a multi-byte value.
- Type reinterpretation — Casting a pointer from a larger type down to a smaller type (e.g., treating a 32-bit int’s address as an 8-bit char pointer) gives you the least significant byte directly, without any offset calculation, under little-endian.
- Historical inertia — Intel’s original 8086 design chose little-endian, and because x86 became so dominant, an enormous amount of software and tooling grew up around that assumption.
Big-endian, meanwhile, has the advantage of being more “human-readable” when examining raw memory or network dumps in a hex editor, since the bytes appear in the same order you’d read the number.
Seeing Endianness in x86-64 Assembly
Let’s prove this to ourselves with NASM. Here’s a simple program that stores a 32-bit value and reads it back byte by byte:
section .data
value dd 0x12345678
section .text
global _start
_start:
mov al, [value] ; al = 0x78 (lowest byte, at lowest address)
mov bl, [value + 1] ; bl = 0x56
mov cl, [value + 2] ; cl = 0x34
mov dl, [value + 3] ; dl = 0x12 (highest byte, at highest address)
; exit syscall omitted for brevity
Because x86-64 is little-endian, al ends up holding 0x78, the least significant byte, even though it’s stored at the first (lowest) memory address of the four-byte value.
Endianness on ARM Assembly
ARM cores are bi-endian, meaning they can operate in either mode, but they default to little-endian (this mode is often called LE or “EL” for “little-endian”). Here’s the equivalent AArch64 example using GNU syntax:
.data
value:
.word 0x12345678
.text
.global _start
_start:
ldr x0, =value
ldrb w1, [x0] ; w1 = 0x78 in little-endian mode
ldrb w2, [x0, #1] ; w2 = 0x56
ldrb w3, [x0, #2] ; w3 = 0x34
ldrb w4, [x0, #3] ; w4 = 0x12
If the same core were configured for big-endian mode (BE8, where instruction fetches stay little-endian but data accesses are big-endian), w1 would instead load 0x12. The REV instruction family on ARM (and BSWAP on x86) exists specifically to flip byte order manually when you need to convert between the two.
Byte-Swapping Instructions
| Instruction | Architecture | Purpose |
|---|---|---|
BSWAP | x86/x86-64 | Reverses byte order of a 32-bit or 64-bit register |
XCHG (with AH/AL) | x86 | Manual 16-bit byte swap trick using register halves |
REV | ARM | Reverses byte order of a full register (32-bit or 64-bit) |
REV16 | ARM | Reverses byte order within each 16-bit halfword |
REV32 | ARM (AArch64) | Reverses byte order within each 32-bit word of a 64-bit register |
A quick x86-64 example converting a little-endian value to big-endian form for network transmission:
mov eax, [value]
bswap eax ; eax now holds the big-endian representation
The Internal Conversion Process
Here’s the general workflow whenever software needs to move a multi-byte value between two systems (or interpret a network packet) with potentially mismatched endianness:
flowchart TD
A[Value in native register] --> B{Is target endianness\ndifferent from native?}
B -- No --> C[Store/transmit as-is]
B -- Yes --> D[Execute byte-swap instruction\nBSWAP / REV]
D --> E[Store/transmit swapped value]
E --> F[Receiving system reads bytes\nin its own native order]
C --> F
Practical Use Cases Where Endianness Bites You
- Network programming: TCP/IP headers use big-endian (“network byte order”). Sockets code on little-endian x86 machines must call
htons()/htonl()before sending, andntohs()/ntohl()after receiving. - File formats: Formats like BMP images are little-endian, while formats like JPEG (in certain header fields) and many older Mac/PowerPC-era formats use big-endian. Getting this wrong silently corrupts parsed values.
- Cross-platform binary data: Serializing a struct on an x86-64 machine and reading it back on a big-endian embedded system (some networking hardware still uses PowerPC or MIPS in big-endian mode) requires explicit byte-order handling.
- Debugging raw memory dumps: When I’m staring at a hex dump in GDB or a disassembler and a value looks “reversed,” the first thing I check is whether I’m misreading endianness rather than assuming the data itself is wrong.
Debugging Endianness Issues
In GDB, you can explicitly control how memory is interpreted:
x/4xb &value # examine 4 bytes individually, unaffected by endianness
x/1xw &value # examine as one word — GDB applies target endianness automatically
set endian big # force GDB to interpret bytes as big-endian
set endian little # force little-endian interpretation
This is genuinely useful when cross-debugging an embedded ARM target configured for big-endian mode from an x86-64 host.
Common Mistakes
- Assuming all architectures are little-endian because x86 dominates desktop and server computing — this assumption breaks the moment you touch networking code or certain embedded/legacy systems.
- Forgetting to convert before sending data over a socket, resulting in garbled integers on the receiving end even though the bytes transmitted correctly.
- Double-swapping — applying
ntohl()to a value that was never actually converted withhtonl()in the first place, silently reversing your data a second time. - Misreading raw hex dumps and assuming a byte sequence is “wrong” when it’s simply stored in the opposite endianness from what you expected.
Best Practices
- Always use standard conversion functions (
htons,htonl,ntohs,ntohl, or their 64-bit equivalents) rather than manual bit-shifting when working with network code — they compile down to efficientBSWAP/REVinstructions anyway. - Document the endianness assumption explicitly in any binary file format or protocol you design.
- When writing portable Assembly routines, use conditional assembly directives to select the correct byte-swap instruction (
BSWAPfor x86,REVfor ARM) rather than hardcoding one architecture’s approach. - Test cross-platform serialization code on an actual big-endian target (or an emulator like QEMU configured for big-endian ARM/MIPS) rather than assuming your little-endian tests are sufficient.
Little-Endian vs Big-Endian: Advantages and Disadvantages
| Little-Endian | Big-Endian | |
|---|---|---|
| Arithmetic operations | Slightly more efficient for carry propagation | No inherent hardware advantage |
| Human readability in hex dumps | Reversed, less intuitive | Matches natural reading order |
| Type casting (int to smaller types) | Direct, no offset needed | Requires offset calculation |
| Industry dominance | x86, most modern ARM deployments | Legacy SPARC, some network protocols |
| Networking convention | Requires conversion | Native (“network byte order”) |
A Third Option: Middle-Endian (and Why It’s Mostly History)
It’s worth knowing that little-endian and big-endian aren’t the only byte orderings that have existed in computing history, even though they’re the only two you’ll encounter in modern mainstream architectures. Some historical machines, most famously certain PDP-11 configurations, used a “middle-endian” (sometimes called “PDP-endian”) ordering for 32-bit values, where the two 16-bit halves were stored in one order but the bytes within each half-word were stored in the opposite order. This produced the notorious 0x4321 byte-swap pattern that made porting numeric code between PDP-11 and other systems a nightmare. Modern architectures (x86, ARM, MIPS, PowerPC, SPARC) all standardized on strict little-endian or strict big-endian ordering specifically to avoid this exact headache, which is one reason middle-endian is now purely a historical curiosity rather than something you need to code defensively against today.
Worked Example: Parsing a Network Packet by Hand
Let’s make this concrete with something I actually had to do early in my networking-code days: manually parsing an IPv4 header’s Total Length field, which sits at a fixed offset and is stored in network byte order (big-endian), from a buffer captured on a little-endian x86-64 machine.
section .bss
packet resb 64 ; assume packet data has been read into here already
section .text
global _start
_start:
; Total Length field is at offset 2, 2 bytes, big-endian
movzx eax, byte [packet + 2] ; high byte first (big-endian layout)
shl eax, 8
movzx ebx, byte [packet + 3] ; low byte second
or eax, ebx ; eax now holds the correct value in native form
Here I manually reconstruct the value byte by byte rather than just doing a 16-bit load, precisely because a plain movzx ax, word [packet+2] would apply little-endian interpretation on my x86-64 machine and give the wrong number. This manual approach — or, more idiomatically, loading the 16-bit word and then calling a byte-swap routine — is exactly what ntohs() does under the hood in C.
Endianness and Bit Fields
A related and often overlooked issue: endianness doesn’t just affect whole-byte ordering, it also interacts with how compilers lay out bit fields within a struct. On some architectures and compilers, big-endian and little-endian targets order bits within a bit field from opposite ends, which is a completely separate headache from byte ordering and one of the reasons hand-rolled binary protocol parsers in Assembly often avoid bit fields entirely in favor of explicit shifting and masking, where you fully control the exact bits being extracted regardless of underlying endianness assumptions.
Testing Your Endianness Assumptions
A simple but genuinely useful sanity check I still run when porting code to a new target is a tiny Assembly snippet that writes a known 32-bit pattern and reads back the first byte:
section .data
test_val dd 0x01020304
section .text
global _start
_start:
mov al, [test_val]
; if al == 0x04, the system is little-endian
; if al == 0x01, the system is big-endian
mov rax, 60
xor rdi, rdi
syscall
Running this under a debugger and inspecting al immediately confirms the target’s native endianness without needing to consult documentation — useful when working with an unfamiliar embedded board.
Endianness in File Formats: A Closer Look
Beyond networking, file format design is where endianness choices get baked in permanently, and it’s genuinely instructive to see how different formats made different choices. The BMP image format, designed on and for x86 hardware, stores its header fields in little-endian order throughout. The TIFF image format is unusual in that it explicitly declares its own endianness at the very start of the file — the first two bytes are either II (Intel, little-endian) or MM (Motorola, big-endian) — meaning a single TIFF parser must be able to handle both orderings depending on what it finds in those initial bytes. This “self-describing” approach is a clever design pattern worth remembering if you ever design your own binary format: rather than assuming one endianness forever, declare it explicitly in the header and branch your parsing logic accordingly.
section .bss
file_header resb 8
section .text
global _start
_start:
; assume file_header has been read from disk already
mov al, [file_header]
cmp al, 'I' ; check for Intel (little-endian) marker
je parse_little_endian
cmp al, 'M' ; check for Motorola (big-endian) marker
je parse_big_endian
Why “Network Byte Order” Became Big-Endian
It’s a reasonable question why the original designers of TCP/IP chose big-endian as the standard, given that little-endian eventually became the dominant order for general-purpose computing. The historical answer is that many of the influential early networking systems (including various minicomputers of the era) were big-endian machines, and the convention was locked into RFC specifications well before x86’s little-endian architecture came to dominate the desktop and server landscape. Once a wire protocol standard is published and widely deployed, changing the byte order convention becomes essentially impossible without breaking every existing implementation, so “network byte order” has remained big-endian for decades purely due to that historical momentum, entirely independent of which endianness happens to be more common in end-user hardware today.
Cross-Compiling and Emulation Considerations
If you ever need to test big-endian-specific code without owning genuine big-endian hardware, QEMU is invaluable — it can emulate big-endian targets like qemu-system-ppc (PowerPC, historically big-endian by default) or ARM configured explicitly in BE8 mode. This lets you validate that your byte-swapping logic actually works correctly rather than just assuming it does because it compiles without errors. I’ve caught more than one embarrassing byte-order bug this way that would have otherwise only surfaced once code shipped to genuinely different hardware.
Frequently Asked Questions
Q: Does endianness affect instruction encoding too, or just data? Primarily data. Instruction fetches on most architectures, including ARM’s BE8 mode, remain in a consistent internal format regardless of data endianness, since the CPU’s instruction decoder is fixed by design.
Q: Can I detect a machine’s endianness at runtime without Assembly? Yes, in C you can write a union trick or check *(char*)&some_int, but at the Assembly level it’s even more direct: store a known value and inspect the raw bytes at its address.
Q: Is one endianness objectively “better”? Not really — it’s largely a historical and engineering trade-off. Little-endian won out due to x86’s dominance and some arithmetic conveniences, but big-endian persists in networking conventions and certain legacy systems.
Summary and Key Takeaways
Endianness is simply about which end of a multi-byte value gets stored first in memory. x86/x86-64 is strictly little-endian, ARM is bi-endian but defaults to little-endian, and network protocols traditionally use big-endian (“network byte order”). Getting this wrong causes silent, confusing bugs — values that look “corrupted” but are actually just misinterpreted byte order.
Key points to remember:
- Little-endian stores the least significant byte at the lowest address; big-endian stores the most significant byte at the lowest address.
BSWAP(x86) andREV(ARM) are the standard instructions for manually flipping byte order.- Always convert explicitly when crossing between different endianness domains, especially in networking and cross-platform file formats.
- Debuggers like GDB let you set and inspect endianness explicitly, which is invaluable for cross-platform debugging.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1 — Intel Corporation
- ARM Architecture Reference Manual for A-profile architecture, Section on Byte Order — Arm Ltd.
- RFC 1700 and related IETF documents on “network byte order”
- GNU Binutils and GDB Documentation — Free Software Foundation
