Little-Endian vs Big-Endian: The Byte Order Battle Every Assembly Programmer Must Understand

Explain the difference between little-endian and big-endian architectures in Assembly language

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:

AddressLittle-EndianBig-Endian
0x10000x780x12
0x10010x560x34
0x10020x340x56
0x10030x120x78

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?

ArchitectureDefault EndiannessNotes
x86 / x86-64Little-endianAlways little-endian, no exceptions
ARM (32-bit and AArch64)Bi-endianLittle-endian by default (LE), but can run in big-endian mode (BE8/BE32)
MIPSBi-endianConfigurable at boot
PowerPCBi-endian, historically big-endian by defaultMany modern PowerPC systems run little-endian
SPARCBig-endianClassic big-endian architecture
Network protocols (TCP/IP)Big-endianCalled “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:

  1. 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.
  2. 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.
  3. 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

InstructionArchitecturePurpose
BSWAPx86/x86-64Reverses byte order of a 32-bit or 64-bit register
XCHG (with AH/AL)x86Manual 16-bit byte swap trick using register halves
REVARMReverses byte order of a full register (32-bit or 64-bit)
REV16ARMReverses byte order within each 16-bit halfword
REV32ARM (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, and ntohs()/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

  1. 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.
  2. Forgetting to convert before sending data over a socket, resulting in garbled integers on the receiving end even though the bytes transmitted correctly.
  3. Double-swapping — applying ntohl() to a value that was never actually converted with htonl() in the first place, silently reversing your data a second time.
  4. 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 efficient BSWAP/REV instructions 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 (BSWAP for x86, REV for 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-EndianBig-Endian
Arithmetic operationsSlightly more efficient for carry propagationNo inherent hardware advantage
Human readability in hex dumpsReversed, less intuitiveMatches natural reading order
Type casting (int to smaller types)Direct, no offset neededRequires offset calculation
Industry dominancex86, most modern ARM deploymentsLegacy SPARC, some network protocols
Networking conventionRequires conversionNative (“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) and REV (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
Total
1
Shares

Leave a Reply

Previous Post
How are input and output operations performed in Assembly language

How Input and Output Operations Are Performed in Assembly Language

Next Post
Describe the purpose of the data segment in Assembly language programming

Describe the purpose of the data segment in Assembly language programming

Related Posts