Computer Memory Fundamentals

Learning Objectives

Distinguish data from instructions, an address from the value stored there, and reads from writes.

Use bits, nibbles, bytes, words and capacity units (KB versus KiB) correctly.

Explain byte addressing, endianness, two's complement, integer ranges and overflow.

Describe IEEE 754 floating point, ASCII/Unicode/UTF-8, alignment and padding at a practical level.

Place registers, caches, SRAM, DRAM, flash, EEPROM and ROM in the memory hierarchy and compare their latency and bandwidth.

Explain the idea of memory-mapped I/O.

Plain-Language Explanation

Memory does not know what it holds. The same four bytes 2A 00 00 00 are the number 42 if a program reads them as a little-endian integer, the start of a different number if read big-endian, a tiny floating-point value if read as a float, and the text * followed by three zero bytes if read as characters. Meaning comes from the type the program uses to read the bytes. This module is about those interpretations — and about the different physical kinds of memory, from tiny fast registers to huge slow flash.

Precise Technical Explanation

Data and instructions in a computer In the von Neumann model used by mainstream computers, instructions and data share one memory. The CPU fetches instruction bytes and data bytes from the same address space; only *how* the bytes are used differs. (Many microcontrollers use a Harvard-style design with separate instruction and data buses, but still map both into one address range.) Because code is just bytes, operating systems mark code pages read-and-execute and data pages read-and-write, never both — see Module 3.

Bit, nibble, byte, word, double word and machine word smallest addressable unit Word (x86 terminology) historical 8086 size Double word (DWORD) Quad word (QWORD)

64 bits on x86-64 and AArch64; 32 bits on Cortex-M ARM documentation defines a word as 32 bits and a halfword as 16 — on the same 64-bit CPU an x86 "word" and an ARM "word" differ by a factor of two.

Binary and hexadecimal addresses Addresses are written in hex: 0x7ffd5e2c3a1c. Each hex digit is four address bits, so aligned boundaries are easy to see — an address ending in 0 is a multiple of 16, one ending in 000 a multiple of 4096 (a typical page size).

Memory cells and addressable units Physically, DRAM stores each bit as charge in a tiny capacitor; SRAM uses a six-transistor latch. Logically, the processor addresses bytes. A 64-bit load reads eight consecutive bytes starting at the given address.

Address versus stored value The address says *where*; the value says *what*. &x in C is the address of x; x is its value. Confusing the two is the root of most pointer bugs (Module 7).

Read and write operations A read (load) copies bytes from memory into a register; a write (store) copies a register into memory. Memory itself never computes — arithmetic happens in registers.

Byte-addressable versus word-addressable memory All mainstream CPUs are *byte-addressable*: consecutive addresses name consecutive bytes. Some DSPs and old machines are *word-addressable*: consecutive addresses name consecutive words, so address + 1 skips 16 or 32 bits. C code that assumes byte addressing breaks on them.

Endianness: little-endian and big-endian A multi-byte value must be stored in some byte order:

Little-endian stores the least significant byte at the lowest address (x86, most ARM and RISC-V systems).

Big-endian stores the most significant byte first (network protocols, many older RISC CPUs, some file formats).

0x11223344 at address A:

Signed and unsigned integer representation An unsigned *n*-bit integer represents 0 … 2ⁿ−1. Signed integers need a representation for negative values; virtually all hardware uses two's complement, and C23 and C++20 require it.

Two's complement In two's complement the top bit has weight −2ⁿ⁻¹ instead of +2ⁿ⁻¹. For 8 bits: 1111 1111 = −128 + 127 = −1, and 1000 0000 = −128. To negate a number, invert every bit and add 1. The great advantage: addition and subtraction use exactly the same circuit for signed and unsigned values.

Integer ranges and overflow Type (typical) int8_t / uint8_t int16_t / uint16_t int32_t / uint32_t int64_t / uint64_t

In C, unsigned arithmetic wraps modulo 2ⁿ (well-defined). Signed overflow is undefined behavior — the compiler may assume it never happens (Module 9). Java and C# define signed overflow to wrap; Python integers never overflow; JavaScript numbers are 64-bit floats.

Floating-point representation overview (IEEE 754) A binary32 float has 1 sign bit, 8 exponent bits (bias 127) and 23 fraction bits; a binary64 double has 1, 11 (bias 1023) and 52. The value is (−1)^sign × 1.fraction × 2^(exponent−bias) for normal numbers. Consequences: only numbers of the form *m*×2ᵉ are exact, so 0.1 is stored approximately; there are special patterns for ±0, ±infinity, NaN and subnormal numbers.

Character encoding: ASCII, Unicode and UTF-8 ASCII assigns the codes 0–127 to English letters, digits, punctuation and control characters ('A' = 0x41). Unicode assigns a *code point* to every character in every script (é = U+00E9, € = U+20AC). UTF-8 stores code points in 1–4 bytes; ASCII text is unchanged, so strlen in C counts bytes, not characters.

Alignment and padding Most CPUs load an *n*-byte value fastest — and some only — when its address is a multiple of *n*. Compilers therefore give each type an alignment and insert padding bytes inside structures so every member is aligned, plus trailing padding so arrays of the structure stay aligned. Module 7 measures this with offsetof.

Memory capacity units: KB/KiB, MB/MiB, GB/GiB 1 kB = 1,000 bytes 1 KiB = 1,024 bytes 1 MB = 1,000,000 bytes 1 MiB = 1,048,576 bytes 1 GB = 10⁹ bytes 1 GiB = 1,073,741,824 bytes

RAM is built and addressed in powers of two, disks are marketed in powers of ten — which is why a "1 TB" drive shows as about 931 GiB.

Volatile versus non-volatile memory Volatile memory loses its contents without power (SRAM, DRAM, registers, caches). Non-volatile memory keeps them (ROM, flash, EEPROM, FRAM, disks). Do not confuse this hardware meaning with the C keyword volatile (Module 4), which is about compiler optimization.

RAM, ROM, SRAM, DRAM, flash, EEPROM, cache and registers operands of instructions tens of them, inside the CPU core CPU caches, MCU RAM fast, no refresh, ~6 transistors per bit dense, must be refreshed every few ms copies of recently used memory built from SRAM, managed by hardware ROM / mask ROM written at manufacture Flash (NOR/NAND) MCU program memory, SSDs erased in blocks, limited write cycles small configuration data byte-erasable, slower writes

Memory hierarchy: registers → caches → RAM → persistent storage

Each level is larger, slower and cheaper per byte than the one above. The numbers are order-of-magnitude figures for current desktop CPUs; they vary by product.

Latency, bandwidth, throughput and access time Latency — time from request to first data (≈ 100 ns for a DRAM miss).

Bandwidth — maximum data rate (tens of GB/s for DRAM).

Throughput — data rate actually achieved by a workload.

Access time — device-level time to read one location; for DRAM it includes row activation and column access.

Latency and bandwidth are independent: a highway can carry many cars (bandwidth) while each trip still takes an hour (latency).

Memory-mapped I/O introduction Devices such as timers, UARTs and GPUs expose control registers *at addresses*. Writing to such an address does not store data in RAM — it sends a command to the device. Microcontrollers do this for every peripheral; the C pattern uses volatile pointers (Modules 7 and 16).

flowchart TB
  R[Registers ~ 1 cycle, bytes] --> L1[L1 cache ~ 4 cycles, tens of KiB]
  L1 --> L2[L2 cache ~ 12 cycles, hundreds of KiB to MiB]
  L2 --> L3[L3 cache ~ 40 cycles, MiB]
  L3 --> M[DRAM ~ 80-100 ns, GiB]
  M --> D[SSD ~ 10-100 µs, TB]

Why It Matters in Real Systems

Network packets are big-endian, so every x86 server converts byte order on every packet header. A float that cannot represent 0.1 exactly changes financial results. A string that is "10 characters" can be 30 bytes in UTF-8. A struct written to a file on one compiler can be unreadable on another because of padding. All of these are memory-representation facts.

Key Terms

Byte order of multi-byte values in memory Two's complement Signed integer encoding where the top bit has negative weight

A result outside the representable range The floating-point standard: sign, exponent, fraction Unicode's number for a character

Variable-length (1–4 byte) Unicode encoding Required or preferred address multiple for a type

Unused bytes inserted to satisfy alignment Loses contents without power Latency / bandwidth Time to first data / maximum data rate

Memory-mapped I/O: device registers at addresses

Memory Layout and Execution Flow

The value 0x11223344 stored at address 0x1000 on a little-endian machine:

address   0x1000  0x1001  0x1002  0x1003
byte        44      33      22      11      <- least significant byte first

Syntax

Inspecting an object's bytes through unsigned char * is always allowed in C; reading them through other unrelated pointer types is not (strict aliasing, Module 7).

uint32_t v = 0x11223344u;                 /* hex literal, u = unsigned */
unsigned char *p = (unsigned char *)&v;   /* view the object's bytes */
printf("%02x", p[0]);                     /* first byte in memory */
int8_t  s = -1;                           /* bit pattern 0xFF */
float   f = 0.1f;                         /* nearest binary32 value to 0.1 */

Complete Runnable Example

One program that inspects byte order, two's complement, overflow, floating-point bits and UTF-8. GCC 11.2, x86-64 Linux.

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <inttypes.h>

static void dump(const char *label, const void *obj, size_t n)
{
    const unsigned char *b = obj;
    printf("%-22s", label);
    for (size_t i = 0; i < n; ++i)
        printf(" %02x", b[i]);
    printf("\n");
}

int main(void)
{
    uint32_t v = 0x11223344u;
    dump("0x11223344 in memory:", &v, sizeof v);

    int8_t minus_one = -1;
    int8_t min8 = INT8_MIN;
    dump("int8_t -1:", &minus_one, 1);
    dump("int8_t -128:", &min8, 1);

    uint8_t u = 250;
    u = (uint8_t)(u + 10);              /* unsigned arithmetic wraps modulo 256 */
    printf("uint8_t 250 + 10 = %" PRIu8 "\n", u);

    float f = 0.1f;
    uint32_t bits;
    memcpy(&bits, &f, sizeof bits);     /* well-defined way to read the bit pattern */
    printf("0.1f bits = 0x%08" PRIx32 " sign=%u exponent=%u fraction=0x%06" PRIx32 "\n",
           bits, (unsigned)(bits >> 31), (unsigned)((bits >> 23) & 0xFF), bits & 0x7FFFFF);
    printf("0.1f printed with 20 digits: %.20f\n", f);

    const char *text = "A\xC3\xA9\xE2\x82\xAC";   /* "Aé€" in UTF-8 */
    printf("strlen(\"A\\u00e9\\u20ac\") = %zu bytes for 3 characters\n", strlen(text));
…

Line-by-Line Explanation

dump walks any object byte by byte through const unsigned char * — the portable way to see memory contents.

0x11223344 prints as 44 33 22 11: x86-64 is little-endian.

-1 is ff and INT8_MIN (−128) is 80: two's complement.

(uint8_t)(u + 10): the addition happens in int (260), and converting to uint8_t keeps the value modulo 256, giving 4.

memcpy copies the float's bytes into an integer without breaking aliasing rules.

The exponent field 123 means 2^(123−127) = 2⁻⁴; the fraction holds the rest of 1.6 × 2⁻⁴ ≈ 0.1.

%.20f reveals the stored value is not exactly 0.1.

The UTF-8 string has three characters but six bytes.

Expected Output

0x11223344 in memory:  44 33 22 11
int8_t -1:             ff
int8_t -128:           80
uint8_t 250 + 10 = 4
0.1f bits = 0x3dcccccd sign=0 exponent=123 fraction=0x4ccccd
0.1f printed with 20 digits: 0.10000000149011611938
strlen("A\u00e9\u20ac") = 6 bytes for 3 characters
its bytes:             41 c3 a9 e2 82 ac

Step-by-Step Execution Trace

v is stored in four bytes; dump reads from the lowest address upward and sees 44 first.

minus_one holds bit pattern 1111 1111; min8 holds 1000 0000.

u + 10 is computed as int 260; the cast stores 260 mod 256 = 4.

The float's 32 bits are copied unchanged into bits and split into fields with shifts and masks.

strlen counts bytes until the zero terminator: 1 (A) + 2 (é) + 3 (€) = 6.

Interactive Visualization

Choose a width, signedness and byte order; enter a number (or a float) and inspect every byte and bit.

Common Mistakes and Failure Modes

Writing a struct or an int straight to a file or socket and reading it on a machine with different endianness or padding.

Expecting signed overflow to wrap in C. It is undefined behavior; use unsigned types or checked arithmetic.

Comparing floats with == after arithmetic (0.1f + 0.2f == 0.3f is false).

Treating strlen as a character count for UTF-8 text.

Using int for sizes; a 3 GiB buffer does not fit in a 32-bit int.

Confusing MB with MiB when sizing buffers or reporting memory.

Corrected / Safe Pattern

/* serialise explicitly, byte by byte, in a defined order */
void put_be32(unsigned char out[4], uint32_t v)
{
    out[0] = (unsigned char)(v >> 24);
    out[1] = (unsigned char)(v >> 16);
    out[2] = (unsigned char)(v >> 8);
    out[3] = (unsigned char)v;
}
/* compare floats with a tolerance appropriate to the problem */
int nearly_equal(double a, double b, double eps) { return fabs(a - b) <= eps * fmax(fabs(a), fabs(b)); }

Debugging and Diagnostic Techniques

A hex dump of the bytes (xxd, hexdump -C, a debugger's memory view) answers representation questions immediately.

printf("%a", x) prints a float in exact hexadecimal form.

-fsanitize=undefined reports signed overflow at run time (Module 9).

Performance Implications

Smaller types fit more values per cache line (Module 13), but arithmetic on narrow types may need extra extension instructions. Misaligned accesses can be slower or fault. Floating-point operations on subnormal numbers can be dramatically slower on some CPUs.

Security and Safety Implications

Integer overflow in a size calculation (count * size) can produce a small allocation for a large copy — a classic heap overflow (Modules 6 and 9). Signedness bugs turn a negative length into a huge unsigned one. Text-encoding confusion causes filter bypasses. Always validate ranges before using a value as a size or index.

Platform and Language Caveats

Endianness: x86 and most ARM/RISC-V systems are little-endian; network byte order is big-endian; some ARM and POWER systems can run either way.

C before C23 permitted ones' complement and sign-magnitude; GCC and every mainstream target use two's complement.

Java and C# define integer overflow to wrap; Rust panics on overflow in debug builds and wraps in release builds unless you use checked/wrapping operations explicitly.

Python integers are arbitrary precision; JavaScript number is an IEEE 754 double (exact integers up to 2⁵³).

Real-World Application

Packet parsers, image and audio codecs, file formats (PNG is big-endian, BMP little-endian), CAN signal decoding and sensor drivers all read raw bytes and must apply the correct width, signedness and byte order.

Best Practices Checklist

[ ] Use fixed-width types for data that crosses a boundary (file, network, hardware).

[ ] Serialise field by field in a documented byte order.

[ ] Never rely on signed overflow in C/C++.

[ ] Use tolerances for floating-point comparisons; use integers (cents) for money.

[ ] Treat strings as bytes plus an encoding.

[ ] Say KiB/MiB/GiB when you mean powers of two.

Exercises

E1 · Beginner. Write 0x0000ABCD as the four bytes stored on a little-endian and on a big-endian machine.

Answer: Little-endian: CD AB 00 00. Big-endian: 00 00 AB CD.

E2 · Intermediate. What is the 8-bit two's-complement pattern of −100? What unsigned value has the same pattern?

Answer: 100 = 0110 0100; invert → 1001 1011; add 1 → 1001 1100 = 0x9C = 156 unsigned.

E3 · Advanced challenge. Extend the example to print the bit fields of 1.0f, -2.5f and the smallest positive subnormal float (0x1p-149f). Explain the exponent field of the subnormal.

Answer: 1.0f = 0x3F800000 (exponent 127, fraction 0); −2.5f = 0xC0200000 (sign 1, exponent 128, fraction 0x200000 = .25 of 1.25×2¹); 0x1p-149f = 0x00000001 — exponent field 0 marks a subnormal: value = 0.fraction × 2⁻¹²⁶, so fraction 1 means 2⁻²³ × 2⁻¹²⁶ = 2⁻¹⁴⁹.

Quiz

? On a little-endian machine, which byte of 0x12345678 is stored at the lowest address?
- 0x12 :: That is big-endian order (most significant byte first).
+ 0x78 :: Little-endian stores the least significant byte first.
- 0x34 :: No common byte order starts with the second byte.
- It depends on the compiler. :: Byte order is a property of the platform's ABI, not of the compiler choice.

? In C, what is the result of INT_MAX + 1 computed in int?
- INT_MIN, always :: Many machines would produce that bit pattern, but the language does not guarantee it.
- 0 :: Nothing in the language produces 0 here.
+ Undefined behavior :: Signed overflow is undefined in C; optimizers rely on it never happening.
- A compile error, always :: Compilers may warn for constants, but variables overflow at run time.

? Which memories are volatile? (select all that apply)
+ DRAM :: It must be refreshed and loses data without power.
+ SRAM caches :: SRAM keeps data only while powered.
- NOR flash :: Flash is non-volatile.
- EEPROM :: EEPROM is non-volatile.

? strlen("€") in a UTF-8 C program returns:
- 1 :: strlen counts bytes, not characters.
+ 3 :: U+20AC is encoded as the three bytes E2 82 AC.
- 2 :: Two by
…

Summary

Bytes have no meaning until a type interprets them. Byte order, signed representation, float encoding and text encoding are all choices of interpretation. Physical memories differ in speed, size, cost and volatility, and are arranged in a hierarchy from registers to storage. Devices can appear as addresses (memory-mapped I/O).

References and Further Reading

IEEE 754-2019, *Standard for Floating-Point Arithmetic*.

David Goldberg, "What Every Computer Scientist Should Know About Floating-Point Arithmetic" (1991).

The Unicode Standard, chapter 3 (UTF-8 definition); RFC 3629.

Ulrich Drepper, "What Every Programmer Should Know About Memory" (2007), sections 2–3.