C Basics · beginner · ~15 min

Endianness and byte order

Understand how multi-byte values are laid out in memory and on the wire.

Overview

Endianness is the byte order in which a multi-byte integer is stored in memory or transmitted on the wire. Little-endian means least-significant byte first; big-endian means most-significant first. Almost every networking, file-format, and reverse-engineering bug eventually involves an endianness mismatch.

Why it matters

C exposes raw bytes through pointers. Whenever your bytes leave the process — to a socket, a binary file, a memory-mapped device — endianness matters. Get it wrong and the receiver decodes garbage.

Core concepts

Host byte order vs network byte order. Most modern CPUs are little-endian (x86, ARM in default mode). Network byte order is big-endian. The POSIX helpers htons, htonl, ntohs, ntohl convert.

Manual byte assembly. (buf[0] << 8) | buf[1] reads a 16-bit big-endian value from a byte buffer. Equivalent for 32-bit needs 4 shifts.

Pentester mindset. Reverse engineering a captured binary or protocol always starts with: which endianness? Tools like xxd show the bytes; you map them to the integer interpretation yourself.

Defensive coding habit. Read multi-byte fields from untrusted buffers byte-by-byte with explicit shifts. Never cast (uint32_t*)buf and dereference — alignment UB.

Syntax notes

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);   /* host -> network (big endian) */
uint16_t htons(uint16_t hostshort);  /* same, 16-bit */
uint32_t ntohl(uint32_t netlong);    /* reverse */
uint16_t ntohs(uint16_t netshort);

Lesson

On x86 and ARM, the least significant byte of a 32-bit value comes first in memory (little-endian). On the network, the most significant byte comes first (big-endian). Whenever you cross that boundary — writing to a socket, reading a binary file format, parsing a packet — you have to convert.

Code examples

#include <arpa/inet.h>
uint32_t host = 0x12345678;
uint32_t net  = htonl(host);   /* swap on little-endian; no-op on big */

Line by line

uint32_t read_u32_be(const unsigned char *b){
    return ((uint32_t)b[0] << 24)
         | ((uint32_t)b[1] << 16)
         | ((uint32_t)b[2] << 8)
         |  (uint32_t)b[3];
}

Common mistakes

  • Memcpy'ing host bytes onto a wire format.
  • Forgetting to convert back on read.

Debugging tips

xxd file.bin | head shows the raw bytes; compare against your decoded integer to confirm byte order. printf("%08x", v) for hex inspection.

Memory safety

Always read multi-byte fields one byte at a time with shifts. Casting unaligned unsigned char * to uint32_t * and dereferencing is undefined on strict-alignment platforms.

Real-world uses

TCP/IP headers, PNG/PE/ELF file formats, USB descriptors, embedded device protocols, every byte that crosses a network or storage boundary.

Practice tasks

  1. Write bswap32. 2. Read a 16-bit big-endian length from a buffer. 3. Detect your host endianness at runtime by inspecting (uint32_t)1.

Summary

Host byte order is little-endian on almost every machine you'll touch; network byte order is big-endian. Use htons/htonl on the way out, ntohs/ntohl on the way in. For binary files and packets, prefer byte-by-byte reads with explicit shifts.

Practice with these exercises