Page Entry Format & Flags
Both page directory entries and page table entries use the same 32-bit format.
Entry Structure
Bit: 31 12 | 11 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
────────────── ────── ─ ─ ─ ─ ─ ─ ─ ─ ─
Physical Address Avail G PS D A PCD PWT U/S R/W P
(20 bits, page- (PT (PT
aligned) only) only)
The upper 20 bits hold the physical address of the next-level structure (or the page itself). Since all structures are 4KB-aligned, the lower 12 bits of the address are always zero — the CPU uses those bits for flags instead.
Flag Definitions
| Bit | Flag | Name | Description |
|---|---|---|---|
| 0 | P | Present | 1 = page/table exists in physical memory. If 0, accessing this address causes a page fault. |
| 1 | R/W | Read/Write | 0 = read-only, 1 = read/write |
| 2 | U/S | User/Supervisor | 0 = kernel-only (ring 0), 1 = accessible from user mode (ring 3) |
| 3 | PWT | Write-Through | Cache policy. 0 = write-back, 1 = write-through |
| 4 | PCD | Cache Disable | 1 = disable CPU cache for this page |
| 5 | A | Accessed | CPU sets this bit when the page is read or written |
| 6 | D | Dirty | CPU sets this when the page is written (page table entries only) |
| 7 | PS | Page Size | In PD entries: 0 = 4KB pages, 1 = 4MB pages |
| 8 | G | Global | Page is not flushed from TLB on CR3 write |
| 9-11 | Avail | Available | Reserved for OS use — CPU ignores these bits |
The Flags We Use
#![allow(unused)]
fn main() {
pub const PAGE_PRESENT: u32 = 1 << 0; // Page exists
pub const PAGE_WRITABLE: u32 = 1 << 1; // Allow writes
pub const PAGE_USER: u32 = 1 << 2; // User mode accessible
}
For kernel pages: PAGE_PRESENT | PAGE_WRITABLE (no user access)
For user pages: PAGE_PRESENT | PAGE_WRITABLE | PAGE_USER
Constructing an Entry
#![allow(unused)]
fn main() {
// Physical address: 0x00123000
// Flags: present + writable
let entry: u32 = 0x00123000 | PAGE_PRESENT | PAGE_WRITABLE;
// = 0x00123003
}
The physical address must be 4KB-aligned (lower 12 bits zero), so |-ing in the flags never corrupts the address.
Extracting the Address
#![allow(unused)]
fn main() {
let physical_address = entry & !0xFFF; // Mask off the flags
let flags = entry & 0xFFF; // Mask off the address
}
The PDEBit Enum
In our implementation, bits are represented as an enum for type-safe manipulation:
#![allow(unused)]
fn main() {
#[repr(u32)]
#[derive(Copy, Clone)]
pub enum PDEBit {
P = 0, // Present
RW = 1, // Read/Write
US = 2, // User/Supervisor
PWT = 3, // Write-Through
PCD = 4, // Cache Disable
A = 5, // Accessed
D = 6, // Dirty
PS = 7, // Page Size
G = 8, // Global
}
impl PDEBit {
const fn mask(self) -> u32 { 1 << (self as u32) }
}
fn is_set(entry: u32, bit: PDEBit) -> bool {
(entry & bit.mask()) != 0
}
}