Endianness in one minute
A 32-bit integer such as 0x12345678 is one number, but memory, flash, a file or a serial link can only hold it as four separate bytes. Endianness is the rule that decides which of those four bytes comes first.
- Big endian puts the most significant byte (
0x12) at the lowest address, or first on the wire. - Little endian puts the least significant byte (
0x78) at the lowest address, or first on the wire.
| Byte offset | +0 | +1 | +2 | +3 |
|---|---|---|---|---|
| Big endian | 12 | 34 | 56 | 78 |
| Little endian | 78 | 56 | 34 | 12 |
Three things the term does not cover are worth fixing in your head early. It says nothing about the order of the eight bits inside a byte; that is a property of a physical link, discussed below. It does not affect a single byte: 0xA5 is stored as 0xA5 everywhere. And it does not apply to a plain byte string such as an ASCII or UTF-8 message, which is already a sequence of single bytes. Endianness only matters when a value wider than one byte has to be turned into bytes, or rebuilt from them.
Big endian vs little endian at a glance
| Property | Big endian | Little endian |
|---|---|---|
| Byte at the lowest address | Most significant | Least significant |
| Byte at the highest address | Least significant | Most significant |
0x12345678 in memory |
12 34 56 78 |
78 56 34 12 |
| Hex dump readability | Multi-byte values read left to right as written | Multi-byte values appear reversed |
| Same value at different widths | Low-order byte moves as the width changes | Low-order byte stays at the start address |
| Typical contexts | IP, TCP and UDP headers; Modbus; PNG; SHA-2 message words; smart-card length fields | x86 and x86-64 memory; mainstream Arm operating systems; USB descriptor fields; ChaCha20 |
Neither order is faster in any general sense. Little endian lets a program read the low 8 or 16 bits of a wider integer from the same address, which suits multi-precision arithmetic that walks from the least significant end. Big endian keeps hex dumps and protocol traces readable, and it makes byte-wise comparison of unsigned keys agree with numeric comparison. Most of the trouble comes not from either choice but from two systems that disagree, or from code that never states which one it expects.
A third family, sometimes called middle- or mixed-endian, existed on older machines such as the PDP-11, which stored a 32-bit value as two 16-bit halves in an order that matched neither scheme. You will rarely meet it in new silicon, but GCC still defines __ORDER_PDP_ENDIAN__ for it, and the same “word swap” problem reappears in Modbus devices, covered later.
How to find out which byte order you have
There are three different questions hiding behind “what endianness is this?”, and each has a different answer.
- What does this CPU use for its own memory? This is fixed by the processor (or by its configuration at reset) and the toolchain knows it at compile time.
- What does this protocol or file format use? This is fixed by the specification of the data, and it does not change with the machine that happens to read it.
- What did this particular sender actually do? Only a format with a byte-order marker or a metadata field can tell you at run time.
For the first question, the compiler normally already has the answer. GCC and Clang predefine __BYTE_ORDER__, to be compared with __ORDER_LITTLE_ENDIAN__ or __ORDER_BIG_ENDIAN__. C23 standardizes the same idea in <stdbit.h> as __STDC_ENDIAN_NATIVE__, and C++20 offers std::endian::native in <bit>.
If you need a run-time check, for a self-test or a diagnostic log line, copy the first byte of a known value into an unsigned char. Reading an object’s bytes through a character type is always permitted in C, so this version has no undefined behavior and no alignment assumptions:
#include <stdint.h>
#include <string.h>
/* Returns 1 on a little-endian host, 0 on a big-endian host. */
static int host_is_little_endian(void)
{
const uint16_t probe = 0x0102;
unsigned char first;
memcpy(&first, &probe, 1); /* the byte at the lowest address */
return first == 0x02;
}
The more useful lesson is that well-written code rarely needs to ask. If every external field is decoded with explicit shifts, as shown in the parsing example below, the same source produces the right answer on any host, and the host’s byte order stops being a variable.
x86 and x86-64 byte order
x86 and x86-64 processors are little endian. Intel’s Software Developer’s Manual describes Intel 64 and IA-32 processors as little-endian machines: the bytes of a word are numbered from the least significant byte, so a 32-bit store of 0x12345678 to address A leaves 78 at A, 56 at A+1, 34 at A+2 and 12 at A+3.
What x86 does provide is fast conversion. BSWAP reverses the bytes of a 32- or 64-bit register, and MOVBE, on processors that report it through CPUID, loads or stores a value with its bytes reversed in a single instruction. You seldom need to write either by hand. When we compiled the portable load_be32() function shown later with Clang at -O2, the x86-64 output was one 32-bit load followed by bswap, and the 64-bit Arm output was one load followed by rev, Arm’s byte-reverse instruction.
The CPU’s byte order also does not decide the byte order of data it handles. An x86 server still has to put TCP port numbers on the wire in big-endian order, still has to read big-endian length fields from a smart card, and still has to honor whatever a sensor’s register map says. “My machine is little endian” answers question 1 above and none of the others.
Arm and endianness
Arm is the case where a one-word answer is wrong, because the architecture separates what a core can do from how it is configured.
- A-profile (Armv8-A, Armv9-A). In AArch64 the endianness of data accesses (and of translation-table walks) is set per exception level in the system control registers (
SCTLR_ELx.EE, plusSCTLR_EL1.E0Efor applications). Whether a core supports big-endian data at all, and whether the setting can be changed, is implementation defined and is reported inID_AA64MMFR0_EL1. A64 instructions themselves are always little endian. - M-profile (Cortex-M). The byte order is fixed for the device, either in the design or by a hardware input sampled at reset, and software cannot change it. It can be read from the
ENDIANNESSbit of the Application Interrupt and Reset Control Register (AIRCR). The core’s own system registers in the Private Peripheral Bus region are little endian even on a big-endian device. - What developers actually meet. The mainstream platforms run Arm in little-endian mode. Microsoft documents that Windows on ARM64 executes in little-endian mode, Android documents that it is always little endian, and Apple documents that Apple silicon Macs use the little-endian format. For a microcontroller, take the answer from the device reference manual or from
AIRCR, not from the word “Arm”.
So the accurate short answer to “is ARM big endian or little endian?” is: Arm cores can support either, the configuration is chosen by the system or the silicon vendor, and the systems most developers work with are configured little endian.
Network byte order
Two machines exchanging binary data need to agree on how a 16- or 32-bit field is laid out, otherwise a length of 300 sent by one is read as 11,265 by the other. Internet protocols settle this once for everyone. The IP specification, RFC 791, defines the order of transmission of its header fields with the most significant octet first, and the same convention runs through TCP, UDP and most IETF protocols that carry binary fields. That convention, big endian, is what “network byte order” means. The host’s own convention is called host byte order.
The sockets API gives you four conversion functions, declared in <arpa/inet.h>:
| Function | Converts |
|---|---|
htons() |
16-bit value, host to network order |
htonl() |
32-bit value, host to network order |
ntohs() |
16-bit value, network to host order |
ntohl() |
32-bit value, network to host order |
On a big-endian host there is nothing to convert, so they return their argument unchanged; on a little-endian host they reverse the bytes. Some C libraries implement them as macros. A familiar embedded example is opening a Modbus TCP socket on port 502: addr.sin_port = htons(502); places the bytes 01 F6 in the structure, which is the big-endian encoding the network stack expects. For 64-bit fields and for little-endian protocols, POSIX.1-2024 standardizes <endian.h> with htobe64(), be32toh(), htole16() and related functions, which glibc and the BSDs have provided for many years.
Network byte order is a convention of the IP family, not a law of networking. USB, for example, defines its multi-byte fields as little endian. The protocol specification is always the authority.
The names come from the egg-breaking factions in Gulliver’s Travels. Danny Cohen’s 1980 note On Holy Wars and a Plea for Peace (IEN 137) applied them to byte order and argued that agreeing on one order mattered more than which order was chosen.
Byte order, bit order and wire order are three different things
This is the distinction that causes the most confusion in embedded work, because all three show up in the same logic-analyzer trace.
- Byte order: how a multi-byte value is split into a sequence of bytes. This is endianness.
- Bit order: the order in which a serial link sends the bits inside each byte, least significant bit (LSB) first or most significant bit (MSB) first.
- Wire order: the order in which a protocol’s fields, and therefore bytes, appear in a frame.
A serial bus defines only the second item. It has no opinion about which half of a 16-bit sensor reading you send first.
| Interface | Bit order within a byte | Who defines multi-byte order |
|---|---|---|
| UART (asynchronous serial) | LSB first after the start bit | The protocol carried over it (Modbus RTU registers are big endian, for example) |
| I²C | MSB first | Each device’s register map |
| SPI | A controller setting: MSB first is the common choice, and many controllers can also shift LSB first | Each device’s command and data format |
| USB | LSB first (CRC fields excepted) | The USB specification: multi-byte fields are little endian |
So the questions “does SPI have an endianness?” and “does UART have an endianness?” both have the answer no. They have a bit order. The byte order belongs to whatever you put on top of them, and it has to be read from that device’s datasheet or that protocol’s specification.
Where endianness bites in embedded systems
Sensor registers
Two widely used I²C sensors make the point on their own. ST’s LIS3DH accelerometer places each axis reading low byte first, OUT_X_L at register 0x28 and OUT_X_H at 0x29, and offers a configuration bit that swaps the order in its high-resolution mode. Bosch’s BMP280 pressure sensor does the opposite for its temperature result, with the most significant byte at 0xFA followed by the less significant bytes at 0xFB and 0xFC. A burst read returns bytes in register-address order in both cases, so the same driver code cannot assemble both values correctly. The register map, not the bus and not the MCU, decides.
Modbus and other field protocols
The Modbus application protocol is explicitly big endian for addresses and data, and its registers are 16 bits wide. A 32-bit integer or an IEEE 754 float therefore has to span two registers, and the base specification does not say which register holds the high half. Meters and drives differ, which is why integration tools offer “word swap” and “byte swap” options. Treat the register order as part of each device’s documentation and test it with a known reading.
Firmware images, bootloaders and stored data
A bootloader that checks an image header reads a length, a version and a CRC that were written by a build tool, usually on a different machine. The ELF format solves this by recording the encoding in its identification bytes (EI_DATA is either ELFDATA2LSB or ELFDATA2MSB), and your own image headers should be just as explicit. The same applies to calibration data, configuration records and event logs kept in EEPROM or flash: once a field outlives the firmware that wrote it, its byte order must be written down.
Cryptography and security protocols
Algorithms specify their own byte order and ignore the CPU’s. SHA-256, as defined in NIST FIPS 180-4, treats its message words as big endian, while ChaCha20 and Poly1305, as defined in RFC 8439, read their words as little endian. A hash that is correct on a desktop and wrong on a microcontroller, or the reverse, is very often a missing or extra byte swap in exactly this step.
Smart cards and payment data
The multi-byte values that smart-card engineers meet most often are big endian. BER-TLV, the encoding used for card data objects, writes a long-form length as an unsigned integer whose first byte is the most significant (ITU-T X.690), so 82 01 2C announces a value of 300 bytes. An extended-length command APDU carries Lc as 00 followed by a two-byte length. On the card itself, Java Card’s Util.makeShort(b1, b2) takes b1 as the high-order byte. Ambimat’s notes on BER-TLV encoding in ISO/IEC 7816-4 and on APDU message structure show these fields in context.
Worked example: decoding a four-byte field
Suppose a device sends the four bytes 12 34 56 78 for a 32-bit counter.
| Interpreted as | Hex | Decimal |
|---|---|---|
| Big endian | 0x12345678 |
305,419,896 |
| Little endian | 0x78563412 |
2,018,915,346 |
Both are plausible counter values, which is why this class of bug survives casual testing. The robust way to decode is to build the value from individual bytes with shifts. The code below works on any host, never reads through a misaligned pointer, and states the byte order in the function name:
#include <stdint.h>
uint32_t load_be32(const uint8_t *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
uint32_t load_le32(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
void store_be32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)v;
}
Two details matter. The cast to uint32_t comes before the shift: a uint8_t is promoted to a signed int, and shifting 0x80 left by 24 overflows a 32-bit int, which is undefined behavior in C. And signed fields need an explicit step after assembly. For a 16-bit two’s-complement reading, (raw & 0x8000u) ? (int32_t)raw - 0x10000 : (int32_t)raw gives the right sign on every compiler; the bytes FF 38 big endian, or 38 FF little endian, both decode to −200.
Endianness in binary file formats
File formats handle byte order in one of three ways.
- One fixed order. PNG stores every multi-byte integer in network byte order, so a PNG decoder never has to guess.
- A byte-order marker. A TIFF file begins with
IIfor little endian orMMfor big endian. UTF-16 text may begin with the byte-order mark U+FEFF, which appears asFE FFin big-endian UTF-16 andFF FEin little-endian UTF-16. A reader that also accepts UTF-32 must look further, because the UTF-32LE mark isFF FE 00 00. - A metadata field. ELF’s
EI_DATAbyte records the encoding of everything that follows, which lets one toolchain produce images for targets of either order.
For a new format, a single fixed order is the simplest to implement and to test. A marker is only worth having if writers on different architectures must avoid a conversion cost.
Common endian bugs
- Casting a receive buffer to an integer pointer.
*(uint32_t *)&buf[3]silently uses host byte order, may fault on a core that rejects unaligned access, and breaks C’s aliasing rules. Use explicit loads instead. - Assuming both ends match. An MCU and a PC tool that are both little endian will agree by coincidence until one side changes, for example when the PC side is replaced by a big-endian protocol gateway or a Java tool.
- Writing C structs directly to the wire or to flash. Byte order, padding, field alignment and
enumsizes all depend on the compiler and target. - Confusing byte order with bit order. Setting an SPI controller to LSB-first does not swap the bytes of a 16-bit value, and swapping bytes does not fix a bit-order mismatch.
- Swapping twice. A value converted with
htonl()in one layer and again in another arrives in host order on the wire. Convert once, at the boundary. - Undocumented field order. A protocol document that lists field widths but not their byte order guarantees a future integration bug.
- Ignoring signedness. A negative temperature assembled into an unsigned variable becomes a reading near 65,000.
Designing an endian-safe embedded protocol
When you define the frame format for a new device, firmware update or host interface, these rules remove almost every byte-order defect before it is written:
- State the byte order once, in the first paragraph of the specification, and keep it the same for every field.
- Give every field an offset, a width, a signedness and a unit, so that nobody has to infer them from sample captures.
- Encode and decode with explicit load and store functions like the ones above, or with a code generator, never with raw structs.
- Publish known test vectors: a sample frame as bytes, with the decoded value of every field.
- Test on more than one architecture where practical, for example the MCU target, an x86 host and a host-side unit test built with an undefined-behavior sanitizer.
- Version the format, so that a later change of field layout is visible rather than silent.
A small example of rule 2:
| Field | Offset | Type |
|---|---|---|
| Frame type | 0 | uint8 |
| Sequence number | 1 | uint16, big endian |
| Temperature, 0.01 °C | 3 | int16, big endian, two’s complement |
| Uptime, seconds | 5 | uint32, big endian |
| CRC-16 over bytes 0–8 | 9 | uint16, big endian |
If you are planning a device of your own, our embedded firmware development page describes how we approach firmware projects.
Frequently asked questions
Is x86 little endian?
Yes. Intel 64 and IA-32 processors store multi-byte values with the least significant byte at the lowest address. Data formats that the x86 machine reads or writes can still be big endian.
Is ARM big endian or little endian?
Either, depending on the core and its configuration. AArch64 can support big-endian data per exception level where the implementation allows it, and a Cortex-M device has its order fixed in hardware. Windows, Android and macOS all run Arm little endian; for a microcontroller, check the reference manual or AIRCR.
What is network byte order?
Big endian: the most significant byte of a field is sent first. It is the convention defined for the IP protocol family, and the reason the sockets API provides htons(), htonl(), ntohs() and ntohl().
Why is network byte order big endian?
Because the early Internet specifications defined it that way, and the protocols built on them kept it. What matters is that every implementation agrees on one order; the choice of big endian is a convention rather than a technical advantage.
Does endianness affect one-byte values or text?
Not single bytes, and not byte-oriented text such as ASCII or UTF-8. It does affect UTF-16 and UTF-32, which is why those encodings can carry a byte-order mark.
Is byte order the same as bit order?
No. Byte order describes how a multi-byte value is laid out in memory or in a message. Bit order describes how a serial link transmits the bits within each byte. They are set independently.
How do I convert between big and little endian?
For data crossing a boundary, decode and encode with explicit shift-based functions such as load_be32() and store_be32(). For socket fields, use htons() and its relatives. Compilers also offer byte-swap built-ins such as GCC’s __builtin_bswap32(), and C++23 adds std::byteswap.
Does SPI or UART have an endianness?
No. They define bit order within a byte (UART sends LSB first; SPI bit order is a controller setting, commonly MSB first). The order of the bytes in a multi-byte value is set by the device or protocol that uses the link.
How can I tell my machine’s endianness?
At compile time, test __BYTE_ORDER__ (GCC and Clang), __STDC_ENDIAN_NATIVE__ (C23) or std::endian::native (C++20). At run time, copy the first byte of a known 16-bit value into an unsigned char, as in the function shown earlier.
Primary sources
- Intel, Intel 64 and IA-32 Architectures Software Developer’s Manuals (Volume 1, section 1.3.1 “Bit and Byte Order”; Volume 2, BSWAP and MOVBE)
- Arm, Arm Architecture Reference Manual for A-profile architecture (section B2.9, “Endian support”) and the Armv7-M Architecture Reference Manual
- Microsoft, Overview of ARM64 ABI conventions; Android, Android ABIs; Apple, Porting your macOS apps to Apple silicon
- IETF, RFC 791: Internet Protocol, Appendix B, “Data Transmission Order”
- Danny Cohen, IEN 137: On Holy Wars and a Plea for Peace (1980)
- The Open Group, htonl, htons, ntohl, ntohs and <endian.h> (POSIX.1-2024)
- GCC, Common Predefined Macros and Byte-Swapping Builtins; WG14, C working draft N3220 (
<stdbit.h>, aliasing and shift rules) - NXP, UM10204: I²C-bus specification and user manual; Texas Instruments, TL16C550C asynchronous communications element and KeyStone SPI user guide
- USB-IF, Universal Serial Bus 2.0 Specification, section 8.1
- Modbus Organization, Modbus Application Protocol Specification V1.1b3
- STMicroelectronics, LIS3DH datasheet; Bosch Sensortec, BMP280 datasheet (BST-BMP280-DS001)
- NIST, FIPS 180-4: Secure Hash Standard, section 3.1; IETF, RFC 8439: ChaCha20 and Poly1305 for IETF Protocols
- ITU-T, X.690: ASN.1 encoding rules (BER, CER, DER), section 8.1.3.5; Oracle, Java Card API: Util and Extended APDU format
- W3C, Portable Network Graphics (PNG) Specification, section 7.1; Adobe, TIFF Revision 6.0; Linux man-pages, elf(5); Unicode Consortium, FAQ: UTF-8, UTF-16, UTF-32 and BOM
This article is an independent engineering reference written by Ambimat. It describes the cited specifications in our own words and is not affiliated with or endorsed by their publishers.