Foundations/Bits, Bytes, and Words

Lesson 1.12,037 words

Bits, Bytes, and Words

Everything a machine stores is a string of bits grouped into bytes. We set out binary and hexadecimal, the byte as the unit of addressing, the word as the machine's natural integer size, and byte ordering — why the same four bytes read as 0x01234567 on one machine and 0x67452301 on another.

╌╌╌╌

At the lowest level a computer stores only bits, each a single device that is either on or off, charged or not, conducting or not. Everything else is interpretation. The same eight bits are an integer to one instruction, a character to another, and part of a machine instruction to a third; what they mean is decided entirely by the code that reads them. This first lesson fixes the vocabulary the rest of the course is written in: how bits are grouped, how we write them down, and the order bytes sit in memory.

Bits and the two notations

A single bit carries one of two values, written and . Group bits and you can name distinct patterns, so the count of representable values doubles with every bit added. Eight bits, a byte, give patterns; thirty-two bits give about four billion; sixty-four give more than .

We read a byte as a base-2 numeral: the rightmost bit carries weight , the next , and bit carries weight . The value is the sum of the weights of the bits that are set.

A byte is eight bits. Bit i carries weight 2^i, so reading right-to-left the pattern 0110 1001 sums to 105, written 0x69 in hex.

Binary is hard for humans to read: a 32-bit value is thirty-two ones and zeros that no one can parse at a glance. To address this, we write values in hexadecimal: base 16, with digits 09 then af for ten through fifteen. Hex works because : every group of four bits (a nibble) is exactly one hex digit, so a byte is always two hex digits and the translation is a lookup, not arithmetic.

Four bits make a nibble, and a nibble is exactly one hex digit. Two nibbles pack into a byte, so any byte is precisely two hex digits.

By convention a hex literal is written with a 0x prefix (0x69), a binary one sometimes with 0b (0b01101001), and a plain numeral is decimal. The sixteen nibble patterns are worth memorizing once; they come up constantly when reading memory dumps.

HexBitsDecHexBitsDec
000000810008
100011910019
200102a101010
300113b101111
401004c110012
501015d110113
601106e111014
701117f111115

Converting in either direction is now mechanical. To read 0x3b8 in binary, expand each digit: 3 is 0011, b is 1011, 8 is 1000, so 0x3b8 = 0011 1011 1000. Going the other way, group the bits into nibbles from the right (padding the top with zeros if the count is not a multiple of four) and look up each group. To go from hex to decimal, weight each digit by its power of sixteen: . Small cases are worth doing by hand; anything larger belongs in a calculator. Hex exists to make the bits readable, not to ease decimal conversion.

A worked conversion ties the three notations together. The byte 0xB4 decodes as follows: split into nibbles b and 4, giving bits 1011 0100; sum the set positions ; and confirm against hex arithmetic . All three agree, because they are three ways of writing the same eight-bit pattern.

The byte 0xB4 in all three notations. The high nibble 1011 is hex b, the low nibble 0100 is hex 4; the set bits at weights 128, 32, 16, 4 sum to decimal 180.

Words, addresses, and the memory model

Programs see memory as one enormous array of bytes, virtual memory, and every byte has an integer address, its index into that array. The set of all possible addresses is the address space. A machine with 64-bit addresses can in principle name bytes; no machine has that much memory, but the address is that wide, and that width is what we mean when we call a processor

64-bit.

A multi-byte value occupies a run of consecutive addresses, and every object has a natural size. A char is one byte, so it sits at a single address; an int is four bytes and spans four consecutive addresses; a pointer on a 64-bit machine is eight bytes and spans eight. Placing three objects in memory shows how addresses grow with size.

Memory as a byte array. A char occupies one address, an int four consecutive addresses, and a pointer eight. Each object's address is the lowest byte it occupies; the next object starts where the last ended.

The word size is the machine's natural unit of integer and address arithmetic: the width of its general-purpose registers and of a pointer. On a 64-bit machine the word is 8 bytes, and that single number determines how much memory a pointer can reach, how wide the adder in the datapath must be, and the default size of a C long. Data still comes in several widths, and it helps to keep the names straight.

WidthBytesC type (x86-64)Assembly suffix
Byte1charb
Word2shortw
Double word4intl
Quad word8long, pointerq

The terminology has one historical inconsistency: in Intel's assembly a word is 16 bits, frozen at the size of the original 16-bit 8086, so a modern 64-bit value is a quad word. When this course says word without qualification it means the machine's natural width; when it means Intel's 16-bit unit it will say so.

The address space's width has a practical consequence worth making concrete with real numbers. A 32-bit machine has 32-bit addresses, so its address space is bytes, exactly 4 GiB. That is the ceiling on how much memory a single 32-bit process can name, and it is why 32-bit systems could never use more than 4 GiB per process no matter how much RAM the board held. A 64-bit machine lifts the ceiling to bytes — 16 exbibytes, so far beyond any real memory that current x86-64 hardware only implements 48 of the 64 address bits, naming a still-enormous 256 TiB. The width of the address, not the amount of installed RAM, is what 64-bit measures.

The same bits, three meanings

For example, take the claim that a bit pattern means nothing until code reads it, applied to the single byte 0x41. Read as an unsigned integer it is . Read as an ASCII character it is the capital letter A, because the ASCII table assigns code to A. Read as part of a machine instruction it might be an opcode byte, or the low byte of a larger immediate. Nothing about the byte itself picks among these; the choice is made entirely by the instruction that loads it.

One byte, 0x41 = 0100 0001, read three ways. The unsigned rule sums the weights to 65; the ASCII rule maps 65 to the letter A; an instruction decoder reads the same bits as an opcode or immediate byte.

The string "A" in C is therefore two bytes in memory: the code for A, 0x41, followed by a zero terminator 0x00. And the number stored in a char and the character 'A' are, at the level of bits, indistinguishable — C's char type is a small integer, and 'A' == 65 is true. This identity is why you can do arithmetic on letters: 'A' + 1 is 'B' because their ASCII codes are consecutive, and c - '0' converts a digit character to its numeric value by subtracting the code of '0' (which is ). The bits carry no type; the type lives in the program.

Byte ordering: the one real surprise

A byte has an address; a multi-byte value occupies several consecutive addresses. That raises a question with no logically forced answer: of the bytes making up a 4-byte integer, which one goes at the lowest address? Two camps settled it two opposite ways, and both are still in service.

  • Big-endian stores the most significant byte first, at the lowest address, the order we write numbers in with the most significant digit leftmost. Used by classic network protocols and older RISC machines.
  • Little-endian stores the least significant byte first. This is what x86-64 — and therefore almost every laptop and server — actually does.
The 4-byte value 0x01234567 laid out from address 0x100. Big-endian puts the most significant byte (01) at the lowest address; little-endian puts the least significant byte (67) first, which is x86-64's order.

Most of the time byte order is invisible: it cancels out as long as you only ever load a value the same width you stored it. It becomes visible the instant you cross that abstraction — casting a pointer to look at individual bytes, sending binary over a network, or reading a file written by a different machine. A short C routine makes the layout concrete by printing an object's bytes one at a time.

show_bytes.cc
#include <stdio.h>

/* Print the `len` bytes starting at `start`, low address first. */
void show_bytes(unsigned char *start, size_t len) {
  for (size_t i = 0; i < len; i++)
    printf(" %.2x", start[i]);   /* two hex digits per byte */
  printf("\n");
}

int main(void) {
  int x = 0x01234567;
  show_bytes((unsigned char *) &x, sizeof(int));
  return 0;   /* x86-64 prints: 67 45 23 01 */
}

The cast (unsigned char *) &x tells the compiler to stop treating those four bytes as one int and hand back a pointer to the first byte, which we then walk one address at a time. On any little-endian x86-64 box the output is 67 45 23 01 — the value's least significant byte sitting at the lowest address, exactly as the figure predicts.

Down at the machine level the byte width is not a comment but part of the instruction itself. Each move carries a size suffix, and writing a single byte touches exactly one address while leaving its neighbors untouched. In the assembly below, $ marks a constant, %rdi a register, and (%rdi) the memory at that address, syntax made precise in the machine-level module.

store.sassembly
movl    $0x01234567, %eax      # 32-bit value into the low 4 bytes of rax
movl    %eax, (%rdi)           # store all 4 bytes at address in rdi
movb    $0x67, (%rdi)          # overwrite just the byte at that address

Because x86-64 is little-endian, the movl lays 67 45 23 01 into memory from (%rdi) upward, and the following movb writes the same 0x67 the store had already placed at the lowest address, confirming that the size suffix and the byte order are consistent.

The endianness confusion is easiest to see when the same bytes cross a machine boundary. Suppose a big-endian server writes the 32-bit integer to a file: because it stores the most significant byte first, the four bytes on disk are 00 00 00 01. A little-endian laptop that reads those four bytes back into an int interprets the first byte, 00, as the least significant, and the last byte, 01, as the most significant — reconstructing the value , not . Nothing is corrupt; each machine followed its own rule faithfully. This is why binary network protocols declare a byte order (network byte order is big-endian) and why file formats either fix an endianness or store a marker so a reader can tell.

The integer 1 written big-endian (00 00 00 01) and read back on a little-endian machine. The reader assigns the first byte the lowest weight, so the same four bytes decode to 0x01000000 = 16777216.

Bi-endianness and the units question

CS:APP presents big- and little-endian as two fixed camps, which is how the world looked when the two conventions were tied to processor families. Modern practice is more fluid in two ways worth knowing.

First, several major architectures are now bi-endian: the byte order is a runtime or boot-time choice rather than a property of the silicon. ARM (from the ARMv6 generation) and Power and MIPS all support both orders and let the operating system pick, which is how the same instruction set runs a little-endian phone and a big-endian embedded controller.1 x86-64 remains resolutely little-endian, and because it dominates servers and desktops, most data you meet is little-endian in practice — but the code that parses a file format or a network packet cannot assume it, which is why portable code reaches for byte-swap intrinsics (bswap on x86, __builtin_bswap32 in GCC and Clang) or the classic htonl/ntohl network-order conversions instead of casting a pointer and hoping.

Second, the how big is a kilobyte ambiguity that has dogged the field was finally given a clean vocabulary. Memory sizes are powers of two, so bytes was long called a kilobyte, clashing with the SI meaning of . Since 1998 the IEC standard reserves the binary prefixes kibi (Ki), mebi (Mi), gibi (Gi), and tebi (Ti) for the powers of , leaving kilo, mega, giga for exact powers of .2 The distinction matters in practice: a 500 GB disk advertised in decimal gigabytes holds about GiB when the operating system counts in binary, and the roughly gap grows at each larger prefix. This course writes GiB and TiB when it means powers of two, as when a 64-bit process names bytes.

With the units in hand, the next lesson asks what those bit patterns mean as numbers — how a fixed-width byte string encodes both unsigned magnitudes and the negative integers, through two's complement.

Footnotes

  1. The bi-endian support in ARM, Power, and MIPS is documented in each architecture's reference manual; the choice is exposed to system software as a mode bit rather than fixed in the instruction encoding. Cocke and Markstein's history of the RISC line (IBM J. Res. Dev., 2000) traces why the flexibility mattered as one instruction set spread across product families with different legacy byte orders.
  2. IEC 60027-2 (1998, revised in IEC 80000-13, 2008) defines the binary prefixes kibi, mebi, gibi, and tebi for powers of , reserving the SI prefixes for powers of ; the standard was adopted to end the long-standing 1000-versus-1024 conflation in memory and storage sizes.

╌╌ END ╌╌