Page Table Entry
A Page Table Entry (PTE) is the leaf record of a page table: a single fixed-size word holding a physical page number, the permission bits that govern read, write, and execute access, the user/supervisor flag, the global flag, accessed and dirty flags, a valid bit, and a small reserved-for-software field. Every virtual-to-physical translation a CPU performs ultimately resolves to one PTE; every memory access touches the PTE’s permission bits to decide whether to proceed. For RISC-V Sv32, the PTE is exactly four bytes and packs all of this into 32 bits (per the spec figure sv32pte.edn and the descriptive text in supervisor.adoc, Sv32 chapter). The leaf-versus-pointer distinction (R=W=X=0 means pointer to the next level) lets the same 4-byte format describe both branch nodes and leaves of the page-table trie. This note walks the RISC-V Sv32 PTE format bit by bit, the leaf-vs-pointer rule, the U bit’s role in user/supervisor separation, the G bit for global mappings that span ASIDs, the A and D bits and the Svadu/Svade choice between hardware and software updates, the RSW bits and how Linux uses them, and the failure modes that the bit patterns encode.
The PTE is the smallest concrete unit of the virtual memory system. The memory protection and virtual memory section organizes around it: the Sv32 walk traverses a tree of PTEs, the TLB caches them, the MMU uses them on every access. For definitely-not-esp32, the PTE format is something the Phase 7 stretch goal must implement faithfully if Sv32 is ever turned on; v1.0 uses PMP and has no PTEs.
Mental Model
A PTE answers two questions at once.
- Where is this page? The PTE holds a physical page number (PPN) that combines with the page offset to form the physical address.
- May this access proceed? The PTE holds permission bits (R, W, X) plus contextual flags (U for user-accessibility, G for global mapping, A and D for usage tracking) that the hardware checks before completing the access.
The same word also serves a structural role: if all permission bits are zero, the PTE is not a leaf at all but a pointer to the next-level page table. The PPN then means “physical address of the next-level table, divided by 4 KiB.” This dual interpretation lets one bit-format describe both interior nodes and leaves of the page-table trie.
flowchart TD PTE["32-bit PTE word"] --> CHK{"R=0, W=0, X=0<br/>and V=1?"} CHK -->|"yes"| PTR["POINTER: PPN x 4 KiB<br/>= next-level page table base.<br/>Walk descends one level."] CHK -->|"no, and V=1"| LEAF["LEAF: PPN x 4 KiB<br/>= physical base of mapped page.<br/>R/W/X = permissions.<br/>U, G, A, D = context flags."] CHK -->|"V=0"| INV["INVALID: any walk step touching<br/>this entry raises a page-fault.<br/>Other bits are don't-care."]
The three roles of a PTE. What it shows: a PTE is interpreted differently depending on its low bits; V=0 means invalid, R=W=X=0 with V=1 means pointer to the next level, otherwise the PTE is a leaf carrying a physical mapping and its permissions. The insight to take: the leaf-vs-pointer disambiguation costs zero hardware (it falls out of bits the walker already needs to check) and lets the page-table representation be uniform from root to leaf.
The Sv32 PTE Format
The spec’s bit-field figure (sv32pte.edn, reproduced from the textual description in supervisor.adoc, Sv32 chapter):
bit: 31 ......... 20 19 ......... 10 9 8 7 6 5 4 3 2 1 0
+----------------+----------------+---+---+---+---+---+---+---+---+---+---+
| PPN[1] | PPN[0] | RSW | D | A | G | U | X | W | R | V |
+----------------+----------------+---+---+---+---+---+---+---+---+---+---+
12 bits 10 bits 2 1 1 1 1 1 1 1 1
Total 32 bits. The fields, low to high:
- V (bit 0): Valid. If 0, “all other bits in the PTE are don’t-cares and may be used freely by software” (per supervisor.adoc). Any walk step touching a V=0 PTE raises a page-fault.
- R (bit 1): Read. Set if the page is readable.
- W (bit 2): Write. Set if the page is writable. The combination
R=0, W=1is reserved and triggers a page-fault on the walk (per supervisor.adoc: “Writable pages must also be marked readable; the contrary combinations are reserved for future use”). - X (bit 3): Execute. Set if the page is fetchable as instructions.
- U (bit 4): User-mode accessible. If 1, U-mode may access the page; if 0, only S-mode and M-mode.
- G (bit 5): Global. Set on mappings that exist in every address space (the kernel’s mappings, typically).
- A (bit 6): Accessed. Indicates the page has been read, written, or fetched from since A was last cleared.
- D (bit 7): Dirty. Indicates the page has been written since D was last cleared.
- RSW (bits 8-9, 2 bits): Reserved for Supervisor software. The hardware ignores them; the OS may use them for any private bookkeeping.
- PPN[0] (bits 10-19, 10 bits): low part of the physical page number.
- PPN[1] (bits 20-31, 12 bits): high part of the physical page number.
The total PPN is 22 bits. Multiplied by the 4 KiB page size, this yields the 34-bit physical address space that Sv32 supports.
The bit ordering is identical between leaf and pointer PTEs; the interpretation differs. In a pointer PTE, only V, the (zero) R/W/X, and the PPN matter; A, D, U, G, RSW are reserved-for-standard-use in non-leaf PTEs and “must be cleared by software for forward compatibility” (per supervisor.adoc).
Leaf Versus Pointer
The leaf-versus-pointer rule is the trick that lets one PTE format serve double duty:
- If
V=1andR=0, W=0, X=0, the PTE is a pointer. The walker descends to the level below, takingPPN * 4 KiBas the base of the next-level page table. - If
V=1and at least one of R, W, X is set, the PTE is a leaf. The walk terminates; the PPN is the physical page number of the mapped page. - If
V=0, the PTE is invalid. The walk terminates with a page-fault.
This is step 4 of the Sv32 walk algorithm (per supervisor.adoc, “Virtual Address Translation Process”): “if pte.r=1 or pte.x=1, go to step 5. Otherwise, this PTE is a pointer to the next level of the page table. Let i = i - 1. If i < 0, stop and raise a page-fault.”
The corollary is that a leaf PTE can appear at any level of the tree. A leaf at the root (level 1 in Sv32) describes a megapage (4 MiB on Sv32, 2 MiB or 1 GiB on Sv39). A leaf at the bottom describes a base page (4 KiB). The walker stops the moment it finds a leaf, regardless of depth. This is the structural mechanism by which superpages exist; no separate “is_superpage” bit is needed because the geometry of where the leaf appears is itself the encoding.
When a leaf appears above the bottom level, the PTE’s lower PPN bits must all be zero (the megapage must be naturally aligned in physical memory). The walker checks this at step 5 of the algorithm: “if i > 0 and pte.ppn[i-1:0] is nonzero, this is a misaligned superpage; raise a page-fault.” For Sv32, a level-1 leaf must have PPN[0] = 0; only PPN[1] carries the high 12 bits and the bottom 22 bits of the physical address come from the virtual address’s VPN[0] and page offset.
The U Bit and User/Supervisor Separation
The U bit’s role is to keep user-mode software out of kernel memory and (with the SUM bit, see below) to keep kernel software out of user memory by default. The rule (per supervisor.adoc):
- U-mode may access the page only if
U=1. - S-mode accessing a page with
U=1faults unlesssstatus.SUM = 1. - S-mode may never execute code on a
U=1page, regardless of SUM.
The SUM bit is the explicit “Supervisor User Memory access” override. When the kernel needs to copy data to or from user memory (the copy_from_user and copy_to_user paths in Linux), it sets SUM, performs the copy, and clears SUM. Outside of these well-defined windows, any S-mode dereference of a user pointer faults, catching a class of kernel bugs that would otherwise be silent.
The “S-mode never executes user code” rule is hardwired and has no override. The reason is security: a kernel that mistakenly jumped to user-controlled memory could be tricked into executing attacker-controlled instructions at supervisor privilege. The spec excludes this even when SUM is set.
Notably, RISC-V does not have separate permission bits for supervisor versus user. The spec discusses the design choice (supervisor.adoc): “an alternative PTE format would support different permissions for supervisor and user. We omitted this feature because it would be largely redundant with the SUM mechanism and would require more encoding space in the PTE.” This is part of why the Sv32 PTE fits in 32 bits.
The G Bit and Global Mappings
The G bit marks mappings that exist in every address space. The textbook user is the kernel’s identity map: the kernel needs the same virtual-to-physical mapping no matter which process is running, so its PTEs are marked G=1. The benefit is that the TLB entry for a global mapping survives an ASID-tagged sfence.vma: a remap of one process’s ASID does not flush kernel entries, saving cache pressure.
The spec (supervisor.adoc) makes a careful warning about correctness:
“Note that failing to mark a global mapping as global merely reduces performance, whereas marking a non-global mapping as global is a software bug that, after switching to an address space with a different non-global mapping for that address range, can unpredictably result in either mapping being used.”
The asymmetry is important. Forgetting G=1 on a kernel mapping costs TLB efficiency. Setting G=1 on a per-process mapping is a correctness bug that can let a process see another process’s memory through TLB aliasing.
For non-leaf PTEs, setting G implies that all mappings reachable through the subtree are global. This lets the walker prune ASID checks at the higher levels.
The A and D Bits
The A (accessed) bit indicates the page has been read, written, or fetched from since A was last cleared. The D (dirty) bit indicates the page has been written since D was last cleared. The OS uses these bits to drive page replacement (which pages have been used recently? evict the unused ones) and writeback (which pages have been modified? must be flushed before discarding).
The spec defines two schemes for keeping these bits up to date (per supervisor.adoc, section “Each leaf PTE contains an accessed (A) and dirty (D) bit”):
- Svade: when the hardware would need to set A (or D), it raises a page-fault instead. Software (the OS) sets the bit in the page-fault handler and re-executes. This is the simpler hardware path but costs a trap per first-access.
- Without Svade (the default mode): hardware sets A and D atomically as part of the page-table walk. The PTE update must be performed atomically with respect to other accesses to the same PTE, and PMA / PMP checks apply to the implicit store.
The newer Svadu extension (per svadu.adoc, “Extension for Hardware Updating of A/D Bits”) makes the choice runtime-controllable: it adds the menvcfg.ADUE bit; when set, the hardware updates the A and D bits; when clear, Svade semantics apply. The spec is explicit: “When hardware updating of A/D bits is disabled, the Svade extension, which mandates exceptions when A/D bits need be set, instead takes effect.”
The atomic update is non-trivial. Step 9 of the Sv32 walk algorithm (per supervisor.adoc) describes it as a compare-and-swap: re-read the PTE, verify it matches the cached value, set A (and D if storing), write back atomically. If the comparison fails, restart at step 2 (the walk may have raced with a software update).
A and D semantics also differ subtly. A may be set as a result of speculative execution; D updates from explicit stores must be exact and observed in program order by the local hart. The reasoning is that A is just a hint for the page-replacement policy (a spuriously-set A is at worst a performance defect), while D affects the correctness of page-replacement decisions (a missed D means the OS discards a dirty page without writing it back).
Uncertain
Verify: which RISC-V profile (RVA20, RVA22, RVA23) mandates Svadu (hardware A/D update) versus Svade (software-trap A/D update), and which version of the privileged spec made the Svade/Svadu naming canonical. Reason: the privileged spec defines both extensions and leaves which-is-implemented to the platform; the binding mandate lives in the profile documents, which were not fetched in this batch. Older privileged spec versions (1.10, 1.11) presented this as implementation-defined without the Svade/Svadu names. To resolve: read the riscv-profiles repository’s RVA20/22/23/24 specs for the per-profile mandate and consult the privileged spec changelog for when Svade/Svadu were named.
The RSW Bits
Bits 8 and 9 are Reserved for Supervisor software. The hardware ignores them; the OS may use them for private bookkeeping that needs to survive page-table updates. Linux’s arch/riscv/include/asm/pgtable-bits.h uses them for:
_PAGE_SPECIAL(bit 8): marks pages that have unusual lifetime rules (no struct page, no refcount)._PAGE_SOFT_DIRTY(where supported by extension, see below): tracks pages modified since the last clear, used by checkpointing and live migration.
A newer RISC-V extension, Svrsw60t59b (Supervisor RSW bits 59 and 60), allocates additional software-reserved bits in the high end of the PTE on 64-bit modes. Linux’s RISC-V code uses these for _PAGE_SOFT_DIRTY (bit 59) and _PAGE_UFFD_WP (bit 60, userfaultfd write protect) when present.
RSW bits are also where the kernel encodes swap entries. When a page has been swapped out, its PTE is marked invalid (V=0); the rest of the bits no longer carry a translation, so the kernel repurposes them to encode the swap-file index. The hardware sees V=0 and faults; the fault handler reads the bits, treats them as a swap entry, reads the page back in, and rewrites a valid PTE.
Walking with PMP and PMA Checks
The implicit loads that fetch PTEs themselves go through PMP and PMA checks (per the Sv32 walk algorithm, step 2). Failing those checks raises an access-fault rather than a page-fault. The distinction matters: an access-fault means the firmware (M-mode) said no; a page-fault means the page table said no. Different traps, different handlers.
This is why M-mode firmware can fence its own data from the supervisor even when the supervisor controls its own page tables: any attempt to walk a page table that points at firmware memory fails on the PMP check during the implicit load, regardless of what the PTE itself would have said.
Linux’s Use of the Sv32 PTE
Linux’s RISC-V port in arch/riscv/include/asm/pgtable-bits.h defines the PTE bits as a one-to-one map of the spec:
#define _PAGE_PRESENT (1 << 0) /* V */
#define _PAGE_READ (1 << 1) /* R */
#define _PAGE_WRITE (1 << 2) /* W */
#define _PAGE_EXEC (1 << 3) /* X */
#define _PAGE_USER (1 << 4) /* U */
#define _PAGE_GLOBAL (1 << 5) /* G */
#define _PAGE_ACCESSED (1 << 6) /* A */
#define _PAGE_DIRTY (1 << 7) /* D */
#define _PAGE_SOFT (3 << 8) /* RSW (2 bits) */
#define _PAGE_SPECIAL _PAGE_SOFTFor Sv32 specifically (arch/riscv/include/asm/pgtable-32.h):
#define PGDIR_SHIFT 22
#define PGDIR_SIZE (1 << 22) /* 4 MiB megapage */
#define MAX_POSSIBLE_PHYSMEM_BITS 34PGDIR_SHIFT = 22 aligns with the 10-bit VPN[1] partition; PGDIR_SIZE = 4 MiB aligns with the megapage size. MAX_POSSIBLE_PHYSMEM_BITS = 34 aligns with the Sv32 physical-address width.
Linux’s generic VM code expects a five-level page table hierarchy (PGD, P4D, PUD, PMD, PTE); for Sv32 it folds three levels (P4D, PUD, PMD) so only PGD and PTE remain. The Linux page-tables docs describe the technique: “if the architecture does not use all the page table levels, they can be folded which means skipped, and all operations performed on page tables will be compile-time augmented to just skip a level.”
The kernel sets _PAGE_GLOBAL on its own mappings (the upper half of the address space), letting ASID-tagged sfence.vma calls leave kernel TLB entries intact across context switches.
Worked Example: Constructing a PTE by Hand
Suppose you want a PTE that maps a virtual page to physical page 0x10003 (so physical address 0x10003000), with R, W, U set, A=1 and D=1 (so the hardware never has to update them), and G=0. Construct the 32-bit value:
- PPN =
0x10003=0001 0000 0000 0000 0011(20 bits, fits in PPN[1]:PPN[0] = 12:10).- PPN[1] =
0x100(high 12 bits of the 22-bit PPN, but our PPN only uses 20 bits, so PPN[1] = 0x040). - Wait. Let’s be careful. The PPN is 22 bits: PPN[1] is the high 12, PPN[0] is the low 10. For PPN = 0x10003 = 65539, the binary is
00 0000 0001 0000 0000 0000 0011. The low 10 bits are0000 0000 11= 0x003; the high 12 bits are0000 0000 0100 0= 0x040. So PPN[0] = 0x003 (placed at bits 10..19) and PPN[1] = 0x040 (placed at bits 20..31).
- PPN[1] =
- Flags: V=1 (bit 0), R=1 (bit 1), W=1 (bit 2), X=0, U=1 (bit 4), G=0, A=1 (bit 6), D=1 (bit 7) → low 10 bits =
0011010111reading high-to-low = 0xD7. - RSW = 0.
Concatenate: PPN[1] << 20 | PPN[0] << 10 | flags = (0x040 << 20) | (0x003 << 10) | 0xD7 = 0x04000000 | 0x00000C00 | 0x000000D7 = 0x04000CD7.
Write this PTE to the right slot in the right page table, and sfence.vma to publish it.
The shift arithmetic is easy to get wrong; many kernel bugs across architectures are off-by-one or off-by-shift in the PTE construction macros. Linux uses inline functions like pfn_pte() to do the bit-fiddling once and re-use it.
Failure Modes
Setting reserved bits in any non-leaf PTE. A and D and U at any non-leaf level are reserved for standard use; setting them is a software bug that may trap on hardware that uses them as part of a future extension (per supervisor.adoc: “For non-leaf PTEs, the D, A, and U bits are reserved for future standard use. Until their use is defined by a standard extension, they must be cleared by software for forward compatibility”).
The W=1, R=0 combination. Always a page-fault. Software that tries to make a page “write-only” cannot; RISC-V does not encode that permission.
Forgetting V=1. The PTE is invalid; the walk faults. Common bug when the kernel zeroes a fresh page table and forgets to install actual mappings.
Misaligned megapage PPN. A level-1 leaf with PPN[0] != 0 causes the walk step 5 to raise a page-fault. The kernel must align megapage physical bases to 4 MiB.
G mismarked on per-process mapping. The spec spells out the bug: “marking a non-global mapping as global is a software bug that … can unpredictably result in either mapping being used” (per supervisor.adoc).
Software-set A bit not exact. Under Svadu’s relaxed semantics, A can be set speculatively. Software that uses A as evidence “this page has been touched by this specific instruction” is wrong; A is only a coarse hint.
Not handling Svade page-faults. When Svade is in force, a page that has not yet been accessed faults on first read. A kernel that does not implement the trap handler will see an infinite loop (the instruction re-executes, faults again, re-executes, faults again).
Concurrent PTE update without atomicity. The spec mandates that PTE updates be atomic at the width PTESIZE. Software that performs partial writes (e.g., two halfword stores to update a 32-bit Sv32 PTE) violates this: the walker may observe a half-updated PTE. Always update PTEs with a single MXLEN-bit store or a CAS.
Comparison to Other Architectures
The Sv32 PTE format is similar but not identical to other architectures’ PTEs:
- x86 PAE / x86-64: 64-bit PTE, with present, R/W (read/write, single bit instead of two), U/S (user/supervisor), PWT (write-through), PCD (cache-disable), A, D, PAT, G bits, and a 40-bit physical frame number. The execute-disable (XD/NX) bit is in the high bits if 64-bit. The bit layout is older and more cluttered.
- ARMv8-A AArch64: 64-bit descriptor, with valid, table-vs-block, lower attributes (memory attribute index, NS, AP, SH, AF, NG), output address (PPN), upper attributes (contig, PXN, UXN). The “AF” (access flag) is ARM’s A bit; the dirty bit is encoded indirectly via “dirty bit modifier” and AP.
- MIPS: separate EntryHi and EntryLo registers loaded by the TLB-miss handler; the PTE format in memory is whatever the OS chooses (software-managed). Permission bits include V, D (dirty/write-allowed), G, plus cache coherency attributes.
RISC-V’s Sv32 PTE is unusually compact (just 32 bits) and orthogonal (R/W/X separately set, U as one bit, A and D as one bit each, no cache attribute fields at all because those live in the PMA tables). This compactness is one reason the Sv32 design is easy to teach and easy to implement.
Production Notes
Linux RISC-V. Linux uses the Sv32 PTE faithfully on 32-bit ports, folding the kernel’s generic five-level hierarchy to two levels. The kernel sets G on its identity-mapped upper half; user mappings carry G=0. The _PAGE_SOFT_DIRTY and _PAGE_UFFD_WP features are conditional on the Svrsw60t59b extension and the 64-bit modes; Sv32 only has the two low RSW bits, so checkpointing on RV32 Linux is more limited.
CVA6. The CVA6 implementation (CVA6 MMU documentation) decodes the PTE in hardware exactly per the spec. It supports both software (Svade) and (newer revisions) hardware (Svadu) A/D update.
ESP32-C3. No MMU, no PTEs. Memory protection is PMP-only.
definitely-not-esp32 roadmap. v1.0 has no PTEs. Phase 7 stretch adds Sv32, at which point the PTE format above must be implemented in the walker’s RTL and in the kernel’s page-table management code. The clean 32-bit format makes both reasonably tractable.