# Writing an OS Kernel from Scratch – Part 5: Enabling Paging — Building the Foundation of Virtual Memory
“Segmentation is just a transition; paging is the true moat of modern operating systems. Today, we manually enable x86 paging, giving the kernel virtual memory!”
In the previous article, we deeply understood protected mode and segmented memory management, and manually switched from real mode to protected mode. But you may have heard: modern operating systems almost never use segmentation, instead relying on Paging for memory isolation and virtualization.
Why? Because paging provides more flexible and efficient memory management: ✅ Supports mapping non-contiguous physical memory to contiguous virtual addresses ✅ Implements inter-process memory isolation (security!) ✅ Supports advanced features like demand paging, shared memory, copy-on-write
Today, we enable the most basic paging mechanism on 32-bit x86 protected mode, laying a solid foundation for subsequent kernel development.
🧱 1. Why Do We Need Paging? Isn’t Segmentation Enough?
Although segmentation can implement memory protection and privilege levels, it has fatal flaws:
| Problem | Explanation |
|---|---|
| Segments must be contiguous | A segment must be contiguous in physical memory, cannot utilize fragmented memory |
| Segment size is fixed | Once the segment limit is set, it’s hard to dynamically expand |
| High switching overhead | Switching tasks requires switching the entire segment descriptor table |
| Weak hardware support | Modern CPU optimization focuses on paging, not segmentation |
Paging divides memory into fixed-size Pages (typically 4KB on x86) and maps Virtual Address → Physical Address through Page Tables, perfectly solving the above problems.
💡 Modern OSes like Linux and Windows all use the “flat segment model + paging”: all segments are set to 0x00000000 ~ 0xFFFFFFFF, segmentation “steps back” and paging takes over memory management.
🗺️ 2. x86 Paging Principle (32-bit)
x86 32-bit paging uses a two-level page table structure:
1. Page Directory: 1, 1024 entries, each 4 bytes → 4KB
2. Page Table: up to 1024, each 1024 entries → each 4KB
🔍 How is a virtual address translated?
A 32-bit virtual address is divided into three parts:
Translation flow:
1. CPU reads the Page Directory Base Address (physical address) from the CR3 register
2. Uses the high 10 bits as an index to look up the page directory entry → gets the Page Table Base Address
3. Uses the middle 10 bits as an index to look up the page table entry → gets the Physical Page Frame Address
4. Appends the low 12-bit offset → final physical address
📌 All addresses (page directory, page table, page frame) are physical addresses! After paging is enabled, all “addresses” a programmer sees are virtual addresses; the hardware translates them automatically.
📦 3. Page Table Entry (PTE) and Page Directory Entry (PDE) Structure
Each page table entry (4 bytes) contains:
| Bit | Meaning |
|---|---|
| 0 (P) | Present bit: 1=page in memory, 0=page fault |
| 1 (R/W) | Read/Write bit: 1=writable, 0=read-only |
| 2 (U/S) | User/Supervisor bit: 1=user-mode accessible, 0=kernel only |
| 3 (PWT) | Page Write-Through (cache policy) |
| 4 (PCD) | Page Cache Disable |
| 5 (A) | Accessed bit (CPU sets to 1 automatically) |
| 6 (D) | Dirty bit (CPU sets to 1 on write) |
| 7 (PS) | Page Size (for PDE only, 0=4KB, 1=4MB) |
| 9-11 | Available for OS use |
| 12-31 | Physical Page Frame Address (high 20 bits) |
⚙️ 4. Implementing Identity Paging (Practical)
We implement the simplest identity paging: virtual address == physical address for the kernel region.
“`c
// Page Directory (aligned to 4KB boundary)
uint32_t page_directory[1024] __attribute__((aligned(4096)));
// Page Table (identity mapping for first 4MB)
uint32_t page_table[1024] __attribute__((aligned(4096)));
void init_paging() {
int i;
// Initialize page table: identity map first 4MB
for (i = 0; i < 1024; i++) {
page_table[i] = (i * 0x1000) | // Physical address = virtual address
0x3; // Present + Read/Write
}
// Set up page directory
// First entry points to our page table
page_directory[0] = ((uint32_t)page_table) | 0x3;
// Other entries: mark as not present (no entry)
for (i = 1; i < 1024; i++) {
page_directory[i] = 0x2; // Read/Write but not present
}
// Load page directory base address into CR3
asm volatile("mov %0, %%cr3" : : "r"(page_directory));
// Set PG bit in CR0 to enable paging
uint32_t cr0;
asm volatile("mov %%cr0, %0" : "=r"(cr0));
cr0 |= 0x80000000; // Set PG bit (bit 31)
asm volatile("mov %0, %%cr0" : : "r"(cr0));
// Paging enabled!
}
“`
✅ After execution: all addresses accessed by code are translated through the page table. The kernel is using virtual memory!
🧪 5. Testing and Verification
After enabling paging:
“`c
// Test: write to a virtual address
uint32_t *ptr = (uint32_t *)0x100000; // 1MB
*ptr = 0xDEADBEEF;
// Physical address 0x100000 also has this value (identity mapping)
“`
To verify paging is truly working:
✅ Paging successfully enabled!
⚠️ 6. Key Cautions
1. CR3 must hold a physical address: CR3 stores the physical address of the page directory. Before enabling paging, ensure the address is a physical address, not a virtual one.
2. Identity mapping for the enable code: The code that sets the PG bit must be identity-mapped because the CPU immediately uses virtual addresses after paging is enabled.
3. Page alignment: Page directories and page tables must be 4KB-aligned.
4. Enable paging at the right time: Enable after GDT is set up and segmentation works correctly.
💬 Final Thoughts
Paging is the cornerstone of modern operating systems. It gives every process the illusion of owning the entire address space, while the kernel safely manages physical memory behind the scenes.
Today you turned on the first switch to virtual memory — from here, the OS starts to gain its “deception” ability.
🌟 The operating system is built on a foundation of “illusions” — paging is the greatest one of them all.
📬 Hands-on challenge: Use paging to map a virtual address to a different physical address (non-identity mapping) and verify the translation. Share your paging debugging experience in the comments!
👇 Next article you’d like to see: Physical memory management (Buddy system), or kernel heap (Slab allocator)?
#OS #KernelDevelopment #Paging #VirtualMemory #PageTable #MMU #WritingFromScratch
Comments