Sv32 Virtual Memory
Sv32 is the 32-bit RISC-V page-based virtual memory scheme: the only paging mode defined when SXLEN=32, used by every RV32 system that runs a Unix-like operating system. A 32-bit virtual address is split into two 10-bit virtual page numbers and a 12-bit page offset; address translation walks a two-level page table of 1024 four-byte page-table entries (PTEs) per level, rooted at the physical page held in the
satpCSR. Sv32 produces a 34-bit supervisor physical address, supports 4 KiB base pages and optional 4 MiB megapages, ASID-tagged translation caches, and thesfence.vmainstruction for cache management. The spec is direct: “when Sv32 virtual memory mode is selected in the MODE field of thesatpregister, supervisor virtual addresses are translated into supervisor physical addresses via a two-level page table” (per supervisor.adoc, “Sv32: Page-Based 32-bit Virtual-Memory Systems”). This note walks the bit-field layouts, the eleven-step translation algorithm reproduced from the spec, the megapage / superpage encoding, the satp register details, sfence.vma semantics for Sv32, the Svade / Svadu A and D bit schemes, and the way Linux configures Sv32 on RV32. It closes with a worked four-process microkernel example.
Sv32 is the concrete paging mode that the definitely-not-esp32 project would adopt if Phase 7 (an MMU) is ever reached; the v1.0 design uses PMP instead. Sv32 is also the simplest paging scheme in the RISC-V family and the one with the cleanest mental model for teaching, since both levels of the page table are identically structured and the address split is symmetric (10+10+12). The 64-bit siblings (Sv39, Sv48, Sv57) are direct generalizations.
Mental Model
Sv32 is a two-level radix-tree lookup keyed on a 32-bit virtual address. The 12 low bits never move; they index a byte within a 4 KiB page. The 20 upper bits are split into two 10-bit slices, each used as an index into a 1024-entry table; the first slice indexes the root page table, the second indexes a second-level page table reached through the first.
flowchart LR VA["32-bit virtual addr"] -->|"bits 31..22<br/>VPN[1] (10 bits)"| L1 VA -->|"bits 21..12<br/>VPN[0] (10 bits)"| L2 VA -->|"bits 11..0<br/>page offset (12 bits)"| OFF SATP["satp.PPN (22 bits)<br/>x 4 KiB = root table phys addr"] --> L1[/"Root page table<br/>1024 x 4-byte PTEs"/] L1 -->|"pointer PTE<br/>(R=W=X=0)"| L2[/"Second-level page table<br/>1024 x 4-byte PTEs"/] L2 -->|"leaf PTE<br/>(R=1 or X=1)<br/>PPN[1]·PPN[0]"| PA L1 -.->|"leaf PTE<br/>at level 1 = megapage<br/>(4 MiB)"| PA OFF -->|"appended unchanged"| PA["34-bit physical addr"]
The Sv32 translation tree. What it shows: every 32-bit virtual address resolves through one or two page-table indirections (one for a megapage, two for a 4 KiB page) into a 34-bit physical address; the 12-bit offset is never translated. The insight to take: the two-level structure means a TLB miss on a 4 KiB page costs two memory accesses to fetch the PTEs plus the final access to the data, before any TLB miss handler runs.
The bits are partitioned to fit cleanly into 4 KiB pages. A 1024-entry table of four-byte PTEs is exactly 4 KiB, so the root and every second-level table is itself one physical page. 4 KiB tables, 4 KiB pages, 1024 entries per slice. Everything aligns to powers of two; the walker is essentially three array indices.
The Bit Layouts
The bit-field figures in the spec (sv32va.edn, sv32pa.edn, sv32pte.edn, rv32satp.edn) are precise; they are reproduced below.
Sv32 virtual address (32 bits):
bit: 31 ............... 22 21 ............... 12 11 ........... 0
+---------------------+---------------------+----------------+
| VPN[1] | VPN[0] | page offset |
+---------------------+---------------------+----------------+
10 bits 10 bits 12 bits
Sv32 physical address (34 bits):
bit: 33 ............... 22 21 ............... 12 11 ........... 0
+---------------------+---------------------+----------------+
| PPN[1] | PPN[0] | page offset |
+---------------------+---------------------+----------------+
12 bits 10 bits 12 bits
Sv32 supports a larger physical address space than its virtual address space: 34 bits physical, 32 bits virtual. The extra two bits let an RV32 system address up to 16 GiB of physical RAM despite the 4 GiB virtual cap. The spec notes the point directly: “Storing a PPN in satp, rather than a physical address, supports a physical address space larger than 4 GiB for RV32” (per supervisor.adoc, “Supervisor Address Translation and Protection (satp) Register”).
Sv32 PTE (32 bits):
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
The bit ordering, low to high, is V, R, W, X, U, G, A, D, RSW (2 bits), PPN[0] (10 bits), PPN[1] (12 bits). Each bit’s meaning is detailed in Page Table Entry; the short version:
- V (bit 0): valid. If 0, the rest of the PTE is don’t-care, and any walk step touching it is a page-fault.
- R, W, X (bits 1, 2, 3): readable, writable, executable. If all three are 0, the PTE is a pointer to a second-level page table; otherwise it is a leaf. The combination
R=0, W=1is reserved. - U (bit 4): user-mode accessible. If 0, only S-mode (or M-mode) may access this page.
- G (bit 5): global mapping; not flushed by ASID-tagged sfences.
- A (bit 6): accessed since the bit was last cleared.
- D (bit 7): dirty (written) since the bit was last cleared.
- RSW (bits 8-9): reserved for supervisor software; Linux uses these for soft-dirty tracking and similar.
- PPN[0] (bits 10-19): low 10 bits of the physical page number.
- PPN[1] (bits 20-31): high 12 bits of the physical page number.
The total 22-bit PPN times the 4 KiB page size yields a 34-bit physical-address byte range. The 4-byte PTE is exactly the width of a 32-bit memory access; this is why the spec mandates PTE accesses be performed at width PTESIZE = 4 and atomically. (Per supervisor.adoc, Sv32 walk step 10: “All implicit accesses to the address-translation data structures in this algorithm are performed using width PTESIZE.” The note continues that an Sv48 implementation may not split an 8-byte PTE into two 4-byte reads, and PTE A/D updates atomically update the whole PTE.)
satp register on RV32 (32 bits):
bit: 31 30 ......... 22 21 .................. 0
+----+----------------+----------------------+
|MODE| ASID | PPN |
+----+----------------+----------------------+
1 9 bits 22 bits
- MODE (bit 31, 1 bit on RV32): 0 = Bare (no translation), 1 = Sv32.
- ASID (bits 30..22, up to 9 bits implemented): address-space identifier. The implementation may support 0 to 9 ASID bits; software determines ASIDLEN by writing all-ones into the ASID field and reading back. ASIDMAX for Sv32 is 9 (per supervisor.adoc, “Supervisor Address Translation and Protection (satp) Register”; “the maximal value of ASIDLEN, termed ASIDMAX, is 9 for Sv32”).
- PPN (bits 21..0, 22 bits): physical page number of the root page table. The root table’s byte address is
PPN * 4096.
A 22-bit root-table PPN encodes the same 34-bit physical address range as the rest of Sv32. Notice that the ASID and PPN share the CSR atomically; the spec is explicit that this is so the pair can be swapped in one CSR write on context switch: “we store the ASID and the page table base address in the same CSR to allow the pair to be changed atomically on a context switch” (per supervisor.adoc).
The Translation Algorithm
The Sv32 walk is specified as a precise eleven-step algorithm in supervisor.adoc, “Virtual Address Translation Process”. Reproduced and expanded:
- Let
a = satp.PPN * PAGESIZE(PAGESIZE = 2^12 = 4096) andi = LEVELS - 1(LEVELS = 2 for Sv32, soistarts at 1). Thesatpregister must be active, that is, the effective privilege mode is S or U. - Let
ptebe the value of the PTE at addressa + va.vpn[i] * PTESIZE, wherePTESIZE = 4for Sv32. The implicit load itself must pass PMA and PMP checks; failure raises an access-fault of the original access type (instruction, load, or store). - If
pte.V = 0, orpte.R = 0 and pte.W = 1, or any reserved bits in the PTE are set, raise a page-fault. - Otherwise, the PTE is valid. If
pte.R = 1 or pte.X = 1, it is a leaf; proceed to step 5. Otherwise, it is a pointer: seti = i - 1. Ifi < 0, raise a page-fault. Otherwise, seta = pte.PPN * PAGESIZEand return to step 2. - A leaf PTE has been reached. If
i > 0and any ofpte.PPN[i-1:0]are nonzero, this is a misaligned superpage; raise a page-fault. - Check the U bit. U-mode software may only access pages with
U = 1. S-mode software accessing aU=1page faults unlesssstatus.SUM = 1. S-mode may never execute code on aU=1page regardless of SUM. - Check shadow-stack-related rules against the leaf’s R/W/X (an extension; defaults to a no-op).
- Check the requested access type against the leaf’s R, W, X. Read needs R, write needs W (and R, since W=1, R=0 is reserved), fetch needs X. Mismatch raises a page-fault. If
sstatus.MXR = 1, executable pages are also readable. - If
pte.A = 0, or the access is a store andpte.D = 0:- Under the Svade extension, raise a page-fault and let software set the bits.
- Otherwise, atomically compare-and-update the PTE: re-read it, and if it still has the same value, set A (and D if a store), with PMP and PMA checks applying to the implicit store. If the CAS fails, restart at step 2.
- Translation succeeds. The physical address is composed as:
pa.pgoff = va.pgoff(untranslated).- If
i > 0(a superpage),pa.ppn[i-1:0] = va.vpn[i-1:0](the unused index bits carry through). pa.ppn[LEVELS-1 : i] = pte.PPN[LEVELS-1 : i].
- All implicit accesses use width
PTESIZE. The implementation may cache PTEs and walk speculatively, subject to thesfence.vmacache-invalidation semantics.
The algorithm is straightforward but step 9 is subtle. Two distinct schemes for the A and D bits exist (per supervisor.adoc):
- Svade: when an access needs to set A or D and the bit is clear, the hardware raises a page-fault and lets software perform the set.
- Svadu: the hardware does it atomically, gated by
menvcfg.ADUE(per svadu.adoc, “ext:svadu[] Extension for Hardware Updating of A/D Bits”).
Without either extension explicitly named, the older privileged-spec versions (1.10, 1.11) left the choice implementation-defined; the current spec mandates one or the other.
The Megapage Optimization
A leaf PTE at level 1 (the root) describes a 4 MiB region instead of a 4 KiB page. This is a megapage (also called a superpage). The walk stops one level earlier; the physical address takes the high 12 bits of the PTE’s PPN as pa.ppn[1], the original va.vpn[0] (10 bits) as pa.ppn[0], and the 12-bit offset.
Megapages exist for two reasons:
- They cut the page-table walk from two memory accesses to one when the working set fits.
- They consume one TLB entry per 4 MiB region instead of one per 4 KiB page. A small TLB covers a much larger working set.
The cost is alignment: a megapage’s physical base address must be 4 MiB aligned, that is, pte.PPN[0] == 0. The walk step 5 explicitly traps if this constraint is violated: “if i > 0 and pte.ppn[i-1:0] is nonzero, this is a misaligned superpage; raise a page-fault” (per supervisor.adoc). Software is responsible for installing only properly-aligned megapages. Linux’s arch/riscv/include/asm/pgtable-32.h configures Sv32 with PGDIR_SHIFT = 22 and PGDIR_SIZE = 1 << 22 = 4 MiB, reflecting the megapage size.
sfence.vma in Sv32
sfence.vma is the supervisor memory-management fence. It serves two purposes (per supervisor.adoc, “Supervisor Memory-Management Fence Instruction”):
- It orders previous stores to memory-management data structures (page tables, satp) before subsequent implicit references to them.
- It invalidates address-translation cache entries.
The instruction takes two register operands rs1 and rs2:
rs1names a virtual address;x0means “all addresses.”rs2names an ASID;x0means “all ASIDs.”
The four combinations:
sfence.vma x0, x0: fence and invalidate everything for the current hart. The big hammer; correct but expensive.sfence.vma x0, asid: invalidate all entries for the given ASID, except entries for global mappings. Used after recycling an ASID.sfence.vma va, x0: invalidate any leaf-PTE-cached entries forva, for all ASIDs. Used after modifying a leaf PTE shared across address spaces.sfence.vma va, asid: most surgical; invalidate cached leaf entries forvain the given ASID, except global mappings.
The spec is precise about what the fence does not do: writing satp does not implicitly invalidate the TLB, nor does it imply ordering. The kernel must execute sfence.vma after modifying page tables if it wants the new mappings to take effect on subsequent instructions. Writing satp itself takes effect immediately (per supervisor.adoc: “Changing satp.MODE from Bare to other modes and vice versa also takes effect immediately, without the need to execute an SFENCE.VMA instruction. Likewise, changes to satp.ASID take effect immediately.”). Only the page-table state requires the fence.
The fence is local to the issuing hart. Cross-hart consistency requires inter-processor interrupts. The classic RISC-V TLB shootdown sequence (per the spec’s commentary): the issuing hart does a local fence to make its own writes visible globally, sends an IPI to remote harts, each remote hart runs an sfence.vma in the IPI handler, and the originating hart waits for acknowledgment. Linux’s arch/riscv/mm/tlbflush.c implements exactly this, choosing between an SBI-mediated sbi_remote_sfence_vma_asid call and a direct IPI based on platform support.
ASIDs make the cost manageable. When a hart context-switches between two processes, each tagged with a different ASID, the TLB does not need to be flushed: ASID-tagged entries from the old process remain valid but are never consulted while the new ASID is loaded. The Sv32 spec allows ASIDLEN as low as zero (no ASIDs implemented), in which case every context switch must flush the entire TLB. Sv32’s ASIDMAX of 9 bits permits up to 512 simultaneously-distinguishable address spaces.
Why Two Levels and 4 KiB Pages
The (10, 10, 12) split is the only sensible choice given 32-bit addresses and 4 KiB pages. Each level’s index must produce a 4 KiB table of PTEs; a 4 KiB table of 4-byte PTEs holds 1024 entries, so each VPN slice is 10 bits. The remaining bits are the offset. Two slices fit; one would leave the offset 22 bits wide (4 MiB pages), three would shrink the offset below 4 KiB. So Sv32 has exactly two levels.
The spec’s commentary on the 4 KiB choice (supervisor.adoc): “After much deliberation, we have settled on a conventional page size of 4 KiB for both RV32 and RV64. We expect this decision to ease the porting of low-level runtime software and device drivers.” 4 KiB matches every other modern architecture (x86, ARM, MIPS), so OS kernels and drivers do not need to grow a separate code path for RISC-V page sizes.
The penalty is TLB reach: a 64-entry 4 KiB TLB covers 256 KiB of mappings, which is small compared to the working set of a modern web browser. Megapages mitigate; transparent huge pages in Linux explicitly try to coalesce 1024 base pages into a single megapage when possible. The spec calls this out: “the TLB reach problem is ameliorated by transparent superpage support in modern operating systems” (per supervisor.adoc).
Worked Example: Translating One Address
Suppose satp holds MODE=1, ASID=0, PPN=0x00100, so the root page table is at physical address 0x00100000 (1 MiB). A program executes lw a0, 0x40001008(zero), computing the virtual address 0x40001008.
Decompose the address:
- VPN[1] = bits 31..22 =
0x100(decimal 256). - VPN[0] = bits 21..12 =
0x001(decimal 1). - offset = bits 11..0 =
0x008.
Walk:
a = 0x00100000,i = 1. Fetchpte = mem[0x00100000 + 256*4] = mem[0x00100400]. Suppose this PTE is0x00200001. The low byte is0x01: V=1, R=0, W=0, X=0, U=0, G=0, A=0, D=0. R=W=X=0 means pointer. The PPN ispte >> 10 = 0x00080, so the next-level table is at0x00080000.i = 0. Fetchpte = mem[0x00080000 + 1*4] = mem[0x00080004]. Suppose this PTE is0x00400c5b. Bits: V=1 (bit 0), R=1 (bit 1), W=1 (bit 2), X=0 (bit 3), U=1 (bit 4), G=0, A=1, D=1, RSW=00, PPN =0x00400c5b >> 10 = 0x10003. R=1 means leaf. The access is a load; R is set, U=1 matches the U-mode of the issuing program, so permission passes.- The physical address is
(PPN << 12) | offset = (0x10003 << 12) | 0x008 = 0x10003008.
The CPU then loads four bytes from physical address 0x10003008. Two implicit memory accesses (0x00100400 and 0x00080004) plus one explicit (0x10003008); without TLB, three memory accesses per lw. With a TLB hit, just one.
Linux’s Sv32 Configuration
Linux’s RISC-V port supports Sv32 for 32-bit kernels. The bit definitions in arch/riscv/include/asm/pgtable-bits.h map the spec’s bit layout directly:
#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) */And in arch/riscv/include/asm/pgtable-32.h:
#define PGDIR_SHIFT 22 /* root table covers 4 MiB chunks */
#define PGDIR_SIZE (1 << 22) /* 4 MiB megapage size */
#define MAX_POSSIBLE_PHYSMEM_BITS 34The kernel folds three of its generic five page-table levels (P4D, PUD, PMD) into no-ops at compile time, leaving PGD (the root) and PTE (the second level). This is how Linux’s architecture-independent page-table walker handles Sv32 with no changes to the upper layers (per Linux’s page-tables documentation).
The vm-layout.html document (docs.kernel.org/arch/riscv/vm-layout.html) notes that the Sv32 section is sparse compared to Sv39/48/57; 32-bit RISC-V Linux is a less common configuration than 64-bit, but it works on the configurations that need it (small embedded systems, the LiteX/VexRiscv SoCs).
Microkernel Example: Four Processes on Sv32
Imagine a small Sv32 microkernel running four user processes P0..P3 on an RV32 hart with 32 MiB of RAM. Each process needs 4 MiB of address space. The kernel allocates:
- A kernel page table at physical
0x80000000(4 KiB), with megapage leaves identity-mapping0x80000000..0x80FFFFFFfor kernel R/W and0x80800000..0x80BFFFFFfor kernel X. - Per-process page tables: a root at
0x80100000 + (i * 0x1000), a second-level at0x80200000 + (i * 0x1000). Each process maps virtual0x40000000..0x403FFFFFto its own physical 4 MiB chunk. - ASID 1..4 assigned to P0..P3.
Process context switch from P0 to P1:
# entry: t0 holds P1's satp value
csrw satp, t0 # MODE=1, ASID=2, PPN=<root of P1's page table>
sfence.vma x0, x0 # invalidate cached translations (cheap if ASID-tagged)
mret # return to U-mode at P1's saved PCWith ASIDs, the sfence.vma could be sfence.vma x0, t1 where t1=2, fencing only entries for ASID 2; entries for other ASIDs survive. On a kernel write to one of P1’s PTEs:
# t0 = virtual address of the touched page
# t1 = ASID 2
sfence.vma t0, t1This invalidates exactly the affected entries, leaving the rest of the TLB warm.
Failure Modes
Misaligned megapage. Setting up a leaf PTE at level 1 with PPN[0] != 0 causes every access to that range to page-fault. The kernel must mask the physical base to 4 MiB alignment before installing.
Forgetting sfence.vma after writing a PTE. The new mapping silently fails to take effect; the CPU keeps using the stale TLB entry. Symptoms are intermittent and address-specific.
Writing satp MODE without zeroing other fields when MODE=Bare. The spec warns this has UNSPECIFIED effect on the remaining satp fields and on translation (per supervisor.adoc). Always write a clean value, never just toggle MODE.
Page tables in non-cacheable memory. The hardware walker may not see writes from the cached side. Page tables must live in normal cacheable RAM with the appropriate PMA.
Reusing ASIDs without sfence.vma x0, asid. Stale entries from the previous owner of the ASID survive in the TLB; the new owner sees the wrong translations.
Setting R=0, W=1. Reserved combination. Per supervisor.adoc, “Writable pages must also be marked readable; the contrary combinations are reserved for future use.” Walk step 3 raises a page-fault on this combination.
Assuming hardware A/D updates work everywhere. Under Svade, every first access to an unmarked page faults; the kernel handler must set the A bit and resume. Software that does not implement this trap path will see infinite-loop faults.
Why Sv32 Is the Right Choice for RV32
The spec mandates it: on SXLEN=32, MODE values other than Bare and Sv32 are reserved (per supervisor.adoc, “Encoding of satp MODE field”). There is exactly one paging mode on RV32, and Sv32 is it.
The deeper question is why no smaller mode (e.g. one-level Sv22 with 4 MiB pages only) was defined. The answer is that 4 MiB is too coarse for typical OS workloads (a small process wastes most of a megapage), and two levels is the smallest count that gives 4 KiB granularity over the full 32-bit space. Sv32 is the floor.
On the upper end, Sv32 has fewer levels and faster walks than Sv39/48/57, but the trade-off is invisible to RV32 systems because no other choice exists. RV64 systems get to pick; RV32 systems do not. This simplicity is partly why Sv32 is the right teaching mode for explaining how RISC-V paging works at all.
Production Notes
LiteX / VexRiscv Sv32 implementations. The VexRiscv core supports Sv32 as an option; LiteX SoCs that include it can run Linux on RV32. This is the realistic deployment context for Sv32 today: small open-source SoCs that need a Unix-like userland but can’t afford the area of a 64-bit core.
CVA6 (Ariane). The CVA6 RV32 configuration implements Sv32 with a hardware walker, fully-associative TLB, and ASID tagging (per the CVA6 MMU documentation). Worth reading as a reference RTL implementation.
SiFive U54 (RV64). The U54 does not implement Sv32 (it is an RV64 core supporting Sv39, per SiFive U54-MC manual). Sv32 is exclusively for RV32 cores.
definitely-not-esp32 roadmap. Phase 7 stretch goal: add Sv32 to the RV32IMC core. The hardware walker is small; the kernel changes are larger (process-private address spaces, demand paging hooks, mmap, page-fault dispatch). PMP suffices for v1.0.