Introduction: The Many Layers of Memory (And Why They Matter)
If you’ve ever been confused by the difference between physical memory, virtual memory, and linear addresses, you’re not alone. Memory management is one of the most complex—and most important—topics in systems programming. It’s also one of the most poorly explained.
I remember the first time I tried to understand how paging works. I was reading the Intel Software Developer’s Manual (all 4,000+ pages of it), and I kept getting lost in the terminology. What’s a “linear address”? How does the page table walk actually work? What’s a TLB? Why do we need both segmentation AND paging? The manual explained what everything was, but not why it mattered.
So let me start with a story.
In the early days of computing (1970s), memory was expensive. A kilobyte of RAM cost more than a good meal. Computers had tiny address spaces—the 8080 processor (1974) had 16 address lines, so it could address only 64KB of memory. That was “enough for anyone” (famous last words).
Then came the 8086 (1978) with its 20-bit address bus. Suddenly, you could address 1MB of memory. But there was a problem: the 8086 had only 16-bit registers. How do you address 1MB with 16-bit registers?
Intel’s solution was segmentation. You combine a 16-bit segment with a 16-bit offset to get a 20-bit physical address. It was clever, it was necessary, and—as it turns out—it was the beginning of decades of confusion for assembly programmers.
Fast-forward to today. We have 64-bit processors with massive address spaces (256TB in x86-64). Segmentation is “mostly dead.” Paging has taken over as the primary mechanism for memory protection and virtualization. But segmentation isn’t completely gone—it’s still there, lurking in the background, and if you’re writing operating system kernels or bootloaders, you need to understand it.
In this article, we’re going to dissect memory management in the x86 architecture. We’ll understand:
- The three types of addresses (physical, linear, logical)
- How segmentation and paging translate between them
- How to actually set up page tables in assembly
- How to write a simple memory allocator
By the end of this article, you won’t just know the theory—you’ll have written working code that sets up page tables and implements a buddy allocator. Let’s dive in.
Memory Management from the CPU’s Perspective: Physical vs. Virtual vs. Linear
To understand memory management, we need to define our terms precisely. The x86 architecture has three types of addresses, and if you mix them up, you’ll be hopelessly confused.
The Three Types of Addresses
- Logical Address: This is the address you actually use in assembly. It consists of a segment selector (a 16-bit value that indexes into a segment descriptor table) and an offset (a 32-bit or 64-bit value). For example, when you write
MOV EAX, [EBX], the CPU interprets this as: “load from the address inEBX, using the data segment (DS) as the base.” The logical address isDS:EBX. - Linear Address (also called Virtual Address): This is the result after the segmentation unit translates the logical address. If segmentation is enabled (as it is in protected mode and long mode), the linear address = segment base + offset. If segmentation is disabled (flat memory model), the linear address = offset (the segment base is 0). Linear addresses are what we typically mean when we say “virtual address” in the context of x86.
- Physical Address: This is the actual address that goes out on the memory bus to the RAM chips. If paging is disabled, linear address = physical address. If paging is enabled, the paging unit translates the linear address to a physical address using page tables.
Let me explain with a concrete example.
A Concrete Example: What Happens When You Access Memory
When the CPU executes MOV EAX, [EBX + 4], here’s what happens inside (simplified):
Step 1: Form the logical address
- Segment selector =
DS(the data segment register) - Offset =
EBX + 4 - Logical address =
DS:EBX+4
Step 2: Segmentation unit translates logical → linear
- The CPU looks up the segment descriptor for
DS(in the GDT or LDT). - The descriptor contains a base address (32-bit) and a limit (20-bit).
- Linear address = segment base + offset.
- If segmentation is in flat mode (base = 0), linear address = offset.
Step 3: Paging unit translates linear → physical (if paging enabled)
- The CPU uses
CR3(control register 3) to find the page directory. - It walks the page tables:
CR3→ page directory → page table → physical page frame. - If the page is present in memory: physical address = page frame base + page offset.
- If the page is not present: raise
#PF(page fault exception).
Step 4: Send physical address to memory bus
- The physical address goes out on the address bus.
- RAM returns the data at that address.
- The CPU loads the data into
EAX.
Visualizing the Translation
This is worth a diagram. The translation from logical to physical is a two-stage process: segmentation, then paging.
Why Three Types of Addresses? (Historical Context)
You might wonder: why does the x86 have both segmentation AND paging? Why not just one or the other?
The answer is historical. Each was added to solve a problem that existed at the time.
- Segmentation (introduced in 8086, enhanced in 80286): The 8086 had 16-bit registers but needed to address 1MB of memory. Segmentation was the solution. It also provided a protection mechanism: each segment has access rights (readable, writable, executable, user vs. supervisor). This prevents programs from stomping on each other’s memory.
- Paging (introduced in 80386): By the mid-1980s, operating systems needed virtual memory (each process thinks it has the entire address space to itself), demand paging (load pages from disk on demand), and copy-on-write (share pages between processes until one writes to them). Segmentation couldn’t do this efficiently, so Intel added paging.
Modern operating systems (Linux, Windows, macOS) use a flat memory model (segmentation does nothing—all segments have base=0, limit=4GB) and rely on paging for memory management and protection.
But segmentation is still there, lurking in the background. Even in x86-64 long mode, segmentation is mostly disabled, but not completely. The FS and GS segments can have non-zero base addresses (used for thread-local storage).



