Memory Address and Addressing
When you declare a variable in your program, how does the computer "find" it? Why can a 32-bit system only support 4GB of RAM?
This lecture lifts the veil on memory addresses, helping you understand the address space—the fundamental limit that determines how much memory a computer can use.
Everyday analogy: mailboxes in an apartment building
Imagine an apartment building where every unit has its own mailbox, and each mailbox is marked with a unique unit number.
When a courier delivers mail, they only need to know the unit number (address) to accurately drop the letter into the correct mailbox. The way the CPU accesses memory is almost exactly the same as a courier delivering mail:
- Each memory unit (1 byte)= A Mailbox
- Memory address= the mailbox's unit number (numbered starting from 0)
- Address bus width= the maximum number of digits the unit number can have (determines how many numbers can be assigned at most)
If the unit number only has 2 digits (00~99), the building can have at most 100 mailboxes. Similarly, if the address bus is only 32 bits wide, the CPU can distinguish at most 2^32 distinct memory cells.
The Essence of Memory Addresses
An Address Is Just a Number
From a hardware perspective, memory is a long row of physical cells that can store 0s and 1s. Each cell stores 1 byte (8 bits), and each cell has a unique number—this number is theMemory address。
The CPU reads and writes memory through a three-bus system:
- The CPU places the target address onto theAddress busOn the address bus—"I want to find mailbox 103"
- CPU isControl busOn the control bus, it sends a "read" or "write" signal—"Please give me what's inside" or "Please put this in."
- Memory Places Data IntoData busOn the data bus, data is placed on it (read), or taken from the data bus (write).
Why does each address correspond to 1 byte?
This is a design choice. Some early computers used "word addressing" (each address corresponds to 2 or 4 bytes), but modern computers almost all useByte addressing—byte addressing, where each address corresponds to 1 byte.
The benefit: processing characters (each ASCII character takes up 1 byte) is very natural, with no need for extra alignment and splitting logic.
内存地址: 0 1 2 3 4 5 6 7
+------+------+------+------+------+------+------+------+
内容: | 0x48 | 0x65 | 0x6C | 0x6C | 0x6F | 0x00 | 0xFF | ... |
+------+------+------+------+------+------+------+------+
'H' 'e' 'l' 'l' 'o' '\0'
一个 32 位整数 0x12345678 在内存中的存储(小端序):
地址: 0 1 2 3
+------+------+------+------+
内容: | 0x78 | 0x56 | 0x34 | 0x12 |
+------+------+------+------+
低字节在低地址——这就是「小端序」(Little Endian)
Why a 32-bit system can only use at most 4GB of memory
Direct cause: the width of the address bus
A 32-bit address is a binary number made up of 32 0s or 1s.
The number of distinct values that 32 binary bits can represent is:
2^32 = 2 × 2 × 2 × ... (32 times) =4,294,967,296
That is approximately4GB(GigaBytes)。
If each address corresponds to 1 byte, then a 32-bit address space can address a total of 4,294,967,296 bytes = 4 GB.
Derivation
1 KB = 2^10 字节 = 1,024 字节
1 MB = 2^20 字节 = 1,048,576 字节
1 GB = 2^30 字节 = 1,073,741,824 字节
2^32 = 2^2 × 2^30
= 4 × 1 GB
= 4 GB
Therefore,32-bit = 4GBThis is not a coincidence, but a strict mathematical relationship between the number of address bits and the addressable space.
Analogy: the number of digits in a phone number
Suppose a city's phone numbers are 8 digits (00000000 ~ 99999999). What is the maximum number of different phone numbers this city can have?
10^8 = 100 million. If the population exceeds 100 million, 8 digits are no longer enough—it must be upgraded to 9 digits.
Same logic: a 32-bit address, pushed to its limit, is also 4GB. When memory requirements exceed 4GB, a 32-bit system is helpless—this is why the industry collectively migrated to 64-bit.
What does 64-bit mean?
The theoretical limit of a 64-bit address space is:
2^64 = 18,446,744,073,709,551,616 bytes ≈16 EB(ExaBytes)
How large is this number?
- 1 EB = 1 billion GB
- 16 EB is roughly several hundred times the amount of data traffic generated by the global internet in a single day in 2025.
- If each byte were a grain of sand, 16 EB of sand could fill about 2,000 standard swimming pools.
Currently, no consumer motherboard supports this much memory. Modern 64-bit CPUs typically implement only 48-bit physical address lines (addressable up to 256 TB), and Windows 10 Home is limited to 128 GB—but for the present and foreseeable future, this is already "good enough."
The true significance of 64-bit is not how much memory you can install, but rather:Software and operating systems no longer need to devise various workarounds for insufficient address space.(such as PAE—Physical Address Extension—from the 32-bit era).
Addressable space comparison across different bit widths
| Address bits | Number of Addressable Units | Addressable space | Vivid analogy | Represents device/system |
|---|---|---|---|---|
| 8 bits | 256 | 256 B | Length of a Text Message | Early Microcontrollers |
| 10 bits | 1,024 | 1 KB | One page of plain text | Early EEPROM |
| 16-bit | 65,536 | 64 KB | A Short Story | Intel 8086, 6502 |
| 20-bit | 1,048,576 | 1 MB | A book of medium thickness | Intel 8088 (IBM PC) |
| 24-bit | 16,777,216 | 16 MB | A Lossless Music Track | Early workstations |
| 32-bit | 4,294,967,296 | 4 GB | An HD Movie | Pentium, ARMv7 |
| 36 bits | 68,719,476,736 | 64 GB | All the data on a small NAS | x86 PAE mode |
| 40 bits | 1,099,511,627,776 | 1 TB | The full capacity of a consumer-grade SSD | Early ARMv8 |
| 48 bits | 281,474,976,710,656 | 256 TB | The amount of data in a mid-sized data center | Modern x86-64 |
| 64-bit | ~1.84 × 10^19 | 16 EB | An astronomical number, far exceeding current physical memory limits | x86-64 / ARMv8 architecture limits |
Note: the change from 32-bit to 64-bit is not "doubling", but a jump from 4GB to 16EB—an increase ofOver 4 billion times! The power of exponential growth is fully demonstrated here.
Interactive demo: address space calculator
The program below systematically shows the addressable space corresponding to different address bit widths, and uses visualization to give you an intuitive feel for exponential growth.
Example
Memory address space calculator (example demo)
Function:
1. Calculate the addressable space corresponding to any address bit count
2. Print the complete comparison table from 8-bit to 64-bit
3. Visualize exponential growth
"""
import math
def address_space_to_str(bytes_val):
"""
Convert the byte count to a human-readable format
Supported: B, KB, MB, GB, TB, PB, EB
"""
if bytes_val == 0:
return "0 B"
units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']
unit_idx = 0
value = float(bytes_val)
# Increase units level by level, keeping values within a reasonable range
while value >= 1024 and unit_idx < len(units) - 1:
value /= 1024
unit_idx += 1
if unit_idx == 0:
return f"{int(value):,} {units[unit_idx]}"
elif value >= 100:
return f"{value:,.0f} {units[unit_idx]}"
elif value >= 10:
return f"{value:,.1f} {units[unit_idx]}"
else:
return f"{value:,.2f} {units[unit_idx]}"
def print_address_space_table(start_bits=8, end_bits=64):
"""
Print address space comparison table
:param start_bits: starting bit position
:param end_bits: ending bit count
"""
print("=" * 85)
print(Memory Address Space Calculator - EXAMPLE Computer Organization Principles)
print("=" * 85)
print()
print(f"{'Address bits':<10} {'can寻址单元number':>20} {'Addressable space':>18} {'relative to 32-bit':>15}")
print("-" * 85)
# Based on 32-bit
baseline_32 = 2 ** 32
# Select significant digits for display
key_bits = [8, 10, 12, 14, 16, 18, 20, 24, 28, 30, 32, 36, 40, 44, 48, 52, 56, 60, 64]
for bits in key_bits:
if bits < start_bits or bits > end_bits:
continue
count = 2 ** bits
space_str = address_space_to_str(count)
# Calculate the ratio relative to 32 bits
if bits <= 32:
ratio = count / baseline_32
ratio_str = f"1/{int(baseline_32/count)}" if ratio < 1 else "1x"
else:
ratio = count / baseline_32
if ratio >= 1_000_000_000:
ratio_str = f{ratio/1_000_000_000:,.1f} hundred million times
elif ratio >= 1_000_000:
ratio_str = f{ratio/1_000_000:,.0f} million times
elif ratio >= 1_000:
ratio_str = f"{ratio/1_000:,.0f} thousand times"
else:
ratio_str = f"{ratio:,.0f}x"
print(f"{bits} bits{'':<5} {count:>20,} {space_str:>18} {ratio_str:>15}")
print("-" * 85)
print()
def analyze_why_4gb():
"""Detailed analysis of the derivation process for 32-bit = 4GB"""
print("=" * 85)
print("Deep Dive: Why is 2^32 = 4 GB?")
print("=" * 85)
print()
print("Derivation steps:")
print()
print(" 2^10 = 1,024 → 1 KB")
print(" 2^20 = 1,024 × 1,024 = 1,048,576 → 1 MB")
print(" 2^30 = 1,024 × 1,024 × 1,024 → 1 GB")
print(" = 1,073,741,824 bytes")
print()
print(" 2^32 = 2^2 × 2^30")
print(" = 4 × 1,073,741,824")
print(" = 4,294,967,296 bytes")
print(" = 4 GB (more precisely, 4 GiB)")
print()
print("Note: strictly speaking")
print(" 4 GB (Gigabyte) = 4 × 10^9 = 4,000,000,000 byte(decimal)")
print(4 GiB (Gibibyte) = 4 × 2^30 = 4,294,967,296 bytes (binary))
print(In the computer field, what is commonly called '4GB memory' actually refers to 4 GiB.)
print()
def visualize_growth():
"""Display exponential growth with a simple ASCII bar chart"""
print("=" * 85)
print("Exponential growth visualization (ASCII bar chart, logarithmic scale)")
print("=" * 85)
print()
bits_list = [8, 16, 20, 24, 28, 32, 36, 40, 48, 64]
# Use logarithm to compress the display range
for bits in bits_list:
count = 2 ** bits
log10 = math.log10(count)
bar = "=" * int(log10 * 3) # Each log10 unit = 3 equals signs
space_str = address_space_to_str(count)
print(f" {bits:>3} bits |{bar:<60} {space_str}")
print()
print(Observation: From 8-bit to 64-bit, the bar length grows steadily (because it is on a logarithmic scale))
print(" But the actual value grows exponentially—from 32-bit to 64-bit, it increased by more than 4 billion times!")
print()
def practical_questions():
"""Practical application problems"""
print("=" * 85)
print("Practical application: calculate how much memory your system can support.")
print("=" * 85)
print()
questions = [
("old-fashioned microcontroller with an 8-bit address bus", 8),
(6502 CPU with a 16-bit address bus, 16),
("laptop with a 32-bit operating system", 32),
(32-bit server with PAE enabled (36-bit physical address), 36),
(Modern 64-bit home computer (48-bit physical address), 48),
("Theoretical limit of 64-bit architecture", 64),
]
for desc, bits in questions:
space = 2 ** bits
print(f" {desc}:")
print(f" 2^{bits} = {space:,} byte = {address_space_to_str(space)}")
print()
# ========== Main Program ==========
if __name__ == "__main__":
print_address_space_table(8, 64)
analyze_why_4gb()
visualize_growth()
practical_questions()
# Extra: Interactive Query
print("=" * 85)
print("Custom query")
print("=" * 85)
print()
custom_bits = [1, 4, 8, 16, 32, 64, 128]
for bits in custom_bits:
space = 2 ** bits
print(f{bits:>3} bit addresses → {space:,} addresses)
if bits >= 128:
print(f" → {address_space_to_str(space)} (Note:Thisnumberquantityalreadyfarexceedcan观测宇宙inOriginal子totalnumber!)")
Interactive demo: address space calculator
Drag the slider to adjust the number of address bits (1~64), and use KaTeX to render 2nformula in real time, with CSS Grid drawing the memory cell grid (for 8 bits or fewer, all 256 cells are drawn).
Quick preset buttonPAE: the "life-extension" solution for 32-bit systems
Before 64-bit became widespread, faced with the 4GB limit, Intel designed a workaround—PAE (Physical Address Extension)。
PAE extends the physical address lines from 32 bits to 36 bits, allowing 32-bit CPUs to address up to 2^36 = 64 GB of physical memory.
But here there is a key limitation:Each process's virtual address space is still 32 bits (4GB)A single program is still limited to at most 4GB of memory. The benefit of PAE is that it lets servers run more programs concurrently, each program using its own 4GB.
PAE was just a transitional solution; the real solution was to fully switch to 64-bit.
Virtual address vs. physical address
The "address" we discussed earlier mainly refers toPhysical address—the real, hardware-level numbering on the memory modules.
But in modern operating systems, the address your program sees is not a physical address, but aVirtual address。
Why do we need virtual addresses?
- Isolation protectionvirtual address: Program A cannot read or write Program B's memory (otherwise a crashing program could corrupt the entire system).
- Simplify programmingEach program thinks it exclusively owns the entire address space (e.g., 0 to 4GB), without needing to care about which addresses other programs use.
- Flexible memory managementThe operating system can temporarily write infrequently used memory to disk (swap/paging) and read it back when needed.
Inside the CPU, there is a hardware unit calledMMU (Memory Management Unit), which is responsible for translating virtual addresses into physical addresses in real time.
程序看到的虚拟地址 → [MMU 翻译] → 物理地址 → [地址总线] → 内存条
程序写: *(0x08048000) = 42
↓
MMU 查表: 虚拟页 0x08048 → 物理页 0x1A3F0
↓
物理地址: 0x1A3F0000 的内存单元被写入 42
Virtual addresses are the cornerstone of modern operating system stability. Without virtual addresses and the MMU, a program reading or writing out of bounds could blue-screen the entire system—a nightmare that happened every day in the DOS era.
How memory addresses manifest in programming
In C/C++, you can directly manipulate memory addresses (pointers). Here is an intuitive example:
Example
Viewing memory addresses in Python (example demo)
Although Python abstracts away the underlying pointers, the id() function still gives us a glimpse into the concept of addresses.
"""
import sys
# Memory Addresses of Basic Types
a = 42
b = 42
c = 999
d = 999
print(EXAMPLE Memory Address Exploration: Python id() Demonstration)
print("=" * 60)
print()
# Python caches small integers (-5~256); variables with the same value point to the same address
print("Small integer caching phenomenon:")
print(fa = 42, id(a) = {id(a)} (hexadecimal: 0x{id(a):X}))
print(fb = 42, id(b) = {id(b)} (hexadecimal: 0x{id(b):X}))
print(f" a and b point to the same object: {a is b}")
print()
print("Large integers are not cached:")
print(fc = 999, id(c) = {id(c)} (hexadecimal: 0x{id(c):X}))
print(fd = 999, id(d) = {id(d)} (hexadecimal: 0x{id(d):X}))
print(fc and d refer to the same object: {c is d} (usually False, depending on Python implementation))
print()
# Address of the string
text = "Hello EXAMPLE"
print(fThe id of string 'Hello EXAMPLE': 0x{id(text):X})
# Address of the list and its element addresses
lst = [10, 20, 30]
print(f"\nlist lst = {lst}")
print(f" The id of the list itself: 0x{id(lst):X}")
print(flst[0]'s id: 0x{id(lst[0]):X})
print(flst[1]'s id: 0x{id(lst[1]):X})
print(flst[2]'s id: 0x{id(lst[2]):X})
print()
print(Description:)
print(Although Python does not expose physical memory addresses, id() returns the unique identifier of the object.)
print("In the CPython implementation, id() returns the address of the object in virtual memory.")
print(You can see that list elements are stored scattered (not contiguous like C arrays).)
print()
# The Embodiment of 32-bit vs 64-bit
print("=" * 60)
print("Python interpreter bitness information:")
print(f" sys.maxsize = {sys.maxsize}")
print(f" 2^31 - 1 = {2**31 - 1}")
print(f" 2^63 - 1 = {2**63 - 1}")
print()
if sys.maxsize > 2**32:
print(fYour Python is 64-bit (maxsize > 2^32))
else:
print(f" Your Python is 32-bit (maxsize <= 2^32)")