Virtual Memory & Paging
RISC-V Sv39 · MP3 Checkpoint 2
ECE 391 Discussion — Hands-on GDB Walkthrough
1 / 18
Quick Concepts: Virtual Memory & Sv39
Isolation
Each process gets its own address space
Protection
R/W/X permissions enforced by hardware
Flexible Layout
OS maps VA freely to any PA
Sv39: 39-bit Virtual Address
VPN[2]
bits 38:30
9 bits
VPN[1]
bits 29:21
9 bits
VPN[0]
bits 20:12
9 bits
Offset
bits 11:0
12 bits
PTE Flags (each entry is 64 bits):
Bit 0: V (Valid) Bit 1: R (Read) Bit 2: W (Write) Bit 3: X (Execute)
Bit 4: U (User) Bit 5: G (Global) Bit 6: A (Accessed) Bit 7: D (Dirty)
Leaf PTE: R, W, or X set → maps a page/superpage
Non-leaf PTE: only V set → points to next-level page table
2 / 18
Example from lecture
The SATP Register
Supervisor Address Translation and Protection — tells hardware where the root page table is
MODE
4 bits (63:60)
ASID
16 bits (59:44)
PPN
44 bits (43:0)
MODE = 8 → Sv39 active. MODE = 0 → paging off (bare addressing).
PPN << 12 → physical address of root (Level-2) page table.
Helper Functions
/* From riscv.h */
csrr_satp() // read SATP
csrw_satp(val) // write SATP
sfence_vma() // flush TLB after page table changes
/* From memory.c */
ptab_to_mtag(root_pt, asid) // build SATP value from PT pointer + ASID
mtag_to_ptab(mtag) // extract PT pointer from SATP value
active_space_ptab() // get root PT of current address space
3 / 18
Starting QEMU and GDB
Terminal 1 — QEMU
$ make debug
qemu-system-riscv64 ... -m 16M \
-kernel kernel.elf -S -s
char device redirected to
/dev/pts/6 (label serial1)
char device redirected to
/dev/pts/7 (label serial2)
Terminal 2 — GDB
$ riscv64-unknown-elf-gdb kernel.elf
(gdb) target remote :1234
Remote debugging using :1234
0x0000000000001000 in ?? ()
(gdb) b memory.c:292
(gdb) c
-S freezes CPU at startup -s opens GDB server on port 1234
Essential GDB Commands
b <loc> breakpoint n / s step over/into info reg satp show SATP
c continue x/Ngx <addr> dump N 64-bit hex monitor info mem VM map
p/x <expr> print hex Ctrl-A X kill QEMU monitor system_reset reboot
4 / 18
Watching Paging Get Enabled — Before
Breakpoint at memory.c:292 — right before csrw_satp(main_mtag)
GDB Session — Before Paging
(gdb) b memory.c:292
(gdb) c
Breakpoint 1, memory_init () at memory.c:292
292 csrw_satp(main_mtag);
# ── Check SATP: should be zero (paging disabled) ──
(gdb) info reg satp
satp 0x0 0
# MODE = 0 means BARE ADDRESSING — VA == PA, no translation
# The kernel has built page tables in memory, but hardware isn't using them yet
# ── Look at the value about to be written ──
(gdb) p/x main_mtag
$1 = 0x800000000008007b
5 / 18
Watching Paging Get Enabled — After
GDB Session — After csrw_satp
(gdb) n # step over csrw_satp
(gdb) info reg satp
satp 0x800000000008007b -9223372036854251397
# ── Paging is now ACTIVE! Decode the value ──
(gdb) p/x (unsigned long)main_mtag >> 60
$2 = 0x8 # MODE = 8 = Sv39 ✓
(gdb) p/x ((unsigned long)main_mtag >> 44) & 0xFFFF
$3 = 0x0 # ASID = 0
(gdb) p/x (unsigned long)main_mtag & 0xFFFFFFFFFFF
$4 = 0x8007b # Root page table PPN
(gdb) p/x ((unsigned long)main_mtag & 0xFFFFFFFFFFF) << 12
$5 = 0x8007b000 # Root PT physical address
(gdb) p/x &main_pt2
$5 = 0x8007b000 # They match!
# Kernel still works because it set up IDENTITY MAPPINGS (VA == PA)
6 / 18
Virtual Memory Map After Paging
QEMU Monitor — Kernel VM Map
(gdb) monitor info mem
vaddr paddr size attr
---------------- ---------------- ---------------- -------
0000000000000000 0000000000000000 0000000080000000 rw--gad
0000000080000000 0000000080000000 0000000000060000 r---gad
0000000080060000 0000000080060000 0000000000015000 r-x-gad
0000000080075000 0000000080075000 0000000000002000 r---gad
0000000080077000 0000000080077000 0000000000189000 rw--gad
0000000080200000 0000000080200000 0000000000c00000 rw--gad
0000000080e00000 0000000080e00000 0000000000200000 r---gad
What this tells us:
0x00000000–0x7FFFFFFF → MMIO region (identity-mapped gigapages, RW, Global)
0x80060000–0x80074FFF → Kernel .text (R+X, no W — code is not writable!)
0x80077000–0x801FFFFF → Kernel .data + heap (R+W, no X — data is not executable!)
Everything is identity-mapped (vaddr == paddr). All have G flag, no U flag (kernel only).
7 / 18
Walking the Page Table: Level 2 (Root)
GDB — Dumping L2 Entries
# Root page table is main_pt2 at 0x8007b000
(gdb) x/2gx &main_pt2[0]
0x8007b000: 0x00000000000000e7 0x00000000100000e7
(gdb) x/1gx &main_pt2[2]
0x8007b010: 0x000000002037fc01
Decoding:
L2[0] = 0xe7 → flags: V,R,W,G,A,D → LEAF (gigapage!), PPN=0 → PA 0x0
Maps VA [0x00000000, 0x40000000) → PA [0x00000000, 0x40000000) (1st GB MMIO)
L2[1] = 0x100000e7 → same flags, PPN=0x400000 → PA 0x40000000
Maps VA [0x40000000, 0x80000000) → PA [0x40000000, 0x80000000) (2nd GB MMIO)
L2[2] = 0x2037fc01 → flags: 0x01 = V only → NON-LEAF → follow to L1 table
8 / 18
Walking the Page Table: L2[2] → Level 1
GDB — Following to Level 1
# Follow the non-leaf PTE to the next level
(gdb) set $l2_entry = *(unsigned long*)&main_pt2[2]
(gdb) set $l1_base = (($l2_entry >> 10) & 0xFFFFFFFFFFF) << 12
(gdb) p/x $l1_base
$6 = 0x80dff000 # L1 table is at 0x80dff000
(gdb) x/8gx $l1_base
0x80dff000: 0x000000002037f821 0x00000000200800e7
0x80dff010: 0x00000000201000e7 0x00000000201800e7
0x80dff020: 0x00000000202000e7 0x00000000202800e7
0x80dff030: 0x00000000203000e7 0x00000000203800e3
# L1[0] = 0x21 flags → V,G, no R/W/X → NON-LEAF → go to L0
# First 2MB has kernel image, needs per-page permissions
# L1[1] = 0xe7 flags → V,R,W,G,A,D → LEAF MEGAPAGE (2MB)
# PPN=0x80200 → maps VA 0x80200000 → PA 0x80200000 (RW)
# L1[2..6] same pattern — each maps next 2MB of RAM as megapage
# L1[7] = 0xe3 flags → V,R,G,A,D (no W!) → read-only DTB megapage
9 / 18
Walking the Page Table: L1[0] → Level 0
GDB — Following to Level 0 (4KB pages)
(gdb) set $l1_entry0 = *(unsigned long*)$l1_base
(gdb) set $l0_base = (($l1_entry0 >> 10) & 0xFFFFFFFFFFF) << 12
(gdb) p/x $l0_base
$7 = 0x80dfe000 # L0 table at 0x80dfe000
(gdb) x/8gx $l0_base # first 8 entries: SBI reserved pages
0x80dfe000: 0x00000000200000e3 0x00000000200004e3
0x80dfe010: 0x00000000200008e3 0x0000000020000ce3
0x80dfe020: 0x00000000200010e3 0x00000000200014e3
These are individual 4 KB page mappings for the first 2 MB of RAM (kernel image area).
Flags 0xe3 → V, R, G, A, D (read-only, global) = SBI reserved pages at 0x80000000+
Full hierarchy we just walked:
SATP → main_pt2 @ 0x8007b000 (L2) → L1 @ 0x80dff000 → L0 @ 0x80dfe000
[0],[1]: gigapages (MMIO) [0]: non-leaf→L0 [0x00-5F]: SBI (R,G)
[2]: non-leaf → L1 [1-6]: megapages (RW) [0x60-74]: .text (RX,G)
[3]: non-leaf → user L1 [7]: megapage DTB (R) [0x75-76]: .rodata (R,G)
[0x77-FF]: .data (RW,G)
10 / 18
Decoding a PTE Bit by Bit: Kernel .text
Kernel .text starts at 0x80060000 → L0 index 0x60
GDB — Decoding .text PTE Flags
(gdb) set $pte_text = *(unsigned long*)($l0_base + 0x60 * 8)
(gdb) p/x $pte_text
$8 = 0x200180eb
(gdb) p ($pte_text >> 0) & 1 # V (Valid) → 1
(gdb) p ($pte_text >> 1) & 1 # R (Read) → 1
(gdb) p ($pte_text >> 2) & 1 # W (Write) → 0 ← not writable!
(gdb) p ($pte_text >> 3) & 1 # X (Execute) → 1
(gdb) p ($pte_text >> 4) & 1 # U (User) → 0 ← kernel only
(gdb) p ($pte_text >> 5) & 1 # G (Global) → 1
(gdb) p ($pte_text >> 6) & 1 # A (Accessed) → 1
(gdb) p ($pte_text >> 7) & 1 # D (Dirty) → 1
(gdb) p/x ($pte_text >> 10) & 0xFFFFFFFFFFF
$18 = 0x80060 # PPN
(gdb) p/x (($pte_text >> 10) & 0xFFFFFFFFFFF) << 12
$19 = 0x80060000 # PA (identity mapped: VA == PA)
11 / 18
Physical Page Allocator: Free Chunk List
GDB — Inspecting Free Memory
# The data structure (from memory.c):
struct page_chunk {
struct page_chunk * next; // next chunk in list
unsigned long pagecnt; // number of pages in chunk
};
# Struct stored at the START of the free region itself (no overhead)
# Inspect after memory_init():
(gdb) p/x free_chunk_list
$26 = 0x8007d000
(gdb) p *free_chunk_list
$27 = {next = 0x0, pagecnt = 3457}
What this tells us:
One chunk at 0x8007d000, 3457 pages = 3457 × 4KB ≈ 13.2 MB free
next = NULL → only chunk in list (all remaining RAM after kernel)
16MB total - 384KB SBI - ~116KB kernel - 2MB DTB = ~13.2MB free ✓
13 / 18
User Process Paging: Shell Loaded
GDB — User Memory Map After Shell Load
(gdb) b trap_frame_jump
(gdb) c
Breakpoint 1, 0x00000000800663bc in trap_frame_jump ()
(gdb) monitor info mem
vaddr paddr size attr
---------------- ---------------- ---------------- -------
... (kernel mappings same as before)
# ── NEW: User-space mappings (note the 'u' flag!) ──
00000000c0000000 0000000080de8000 0000000000001000 r-xu-ad .text pg 1
00000000c0001000 0000000080de5000 0000000000001000 r-xu-ad .text pg 2
00000000c0002000 0000000080de4000 0000000000001000 r-xu-ad .text pg 3
00000000c0003000 0000000080de3000 0000000000001000 r-xu-ad .text pg 4
00000000c0004000 0000000080de2000 0000000000001000 rw-u-ad .data pg 1
00000000c0005000 0000000080de1000 0000000000001000 rw-u-ad .data pg 2
00000000fffff000 0000000080de9000 0000000000001000 rw-u-ad stack
U flag present (CPU only allows U-mode here) VA ≠ PA (0xC0000000→0x80DExxxx) Only 7 pages = 28KB!
14 / 18
Walking User-Space Page Tables
User VA 0xC0000000 → VPN[2] = 3 (since 0xC0000000 >> 30 = 3)
GDB — User Page Table Walk
# L2[3] — the user-space entry in the root table
(gdb) x/1gx &main_pt2[3]
0x8007b018: 0x0000000020379c01
# flags = 0x01 → V only → non-leaf → follow to user L1
(gdb) set $l2_3 = *(unsigned long*)(&main_pt2[3])
(gdb) set $user_l1 = (($l2_3 >> 10) & 0xFFFFFFFFFFF) << 12
(gdb) p/x $user_l1
$4 = 0x80de7000 # User L1 table
(gdb) x/4gx $user_l1
0x80de7000: 0x0000000020379801 0x0000000000000000
0x80de7010: 0x0000000000000000 0x0000000000000000
# Only L1[0] populated → covers VA [0xC0000000, 0xC01FFFFF)
# All other entries = 0 → unmapped → page fault if accessed!
15 / 18
Demand Paging: How Page Faults Work
Shell starts with only 7 pages. What happens when it needs more?
Page Fault Flow
# 1. Program accesses unmapped VA (e.g., stack grows beyond initial page)
# 2. Hardware triggers PAGE FAULT exception
# 3. CPU traps to kernel with:
# scause = 12 (instruction PF) / 13 (load PF) / 15 (store PF)
# stval = the faulting virtual address
# 4. Kernel calls handle_umode_page_fault(tfr, vma)
# 5. Handler: alloc physical page → create PTE → map it at faulting addr
# 6. Return from exception → CPU retries instruction → succeeds!
/* From riscv.h */
#define RISCV_SCAUSE_LOAD_PAGE_FAULT 13
#define RISCV_SCAUSE_STORE_PAGE_FAULT 15
#define RISCV_SCAUSE_INSTR_PAGE_FAULT 12
Your exception handler stub (excp.c):
excp.c
void handle_umode_exception(unsigned int cause, struct trap_frame * tfr) {
// YOUR CODE HERE — route page faults to handle_umode_page_fault()
return;
}
16 / 18
GDB Cheat Sheet: PTE Decode & Manual Translation
PTE Decode Recipe
# Given a raw PTE in $pte:
p/x $pte & 0xFF # flags
p ($pte >> 0) & 1 # V
p ($pte >> 1) & 1 # R
p ($pte >> 2) & 1 # W
p ($pte >> 3) & 1 # X
p ($pte >> 4) & 1 # U
p ($pte >> 5) & 1 # G
# Physical address:
p/x (($pte>>10)&0xFFFFFFFFFFF)<<12
Manual Address Translation
# Translate VA by hand:
set $vpn2 = ($VA >> 30) & 0x1FF
set $vpn1 = ($VA >> 21) & 0x1FF
set $vpn0 = ($VA >> 12) & 0x1FF
# Walk: L2 → L1 → L0
set $l2 = *(ulong*)(&main_pt2[$vpn2])
set $l1t = (($l2>>10)&0xFFF...F)<<12
set $l1 = *(ulong*)($l1t+$vpn1*8)
set $l0t = (($l1>>10)&0xFFF...F)<<12
set $l0 = *(ulong*)($l0t+$vpn0*8)
Useful Patterns
# Following a non-leaf PTE to next table:
set $entry = *(unsigned long*)<address_of_pte>
set $next_table = (($entry >> 10) & 0xFFFFFFFFFFF) << 12
x/8gx $next_table # dump next-level table
# Quick checks:
Is leaf? ($pte & 0xE) != 0 Is user? ($pte & 0x10) != 0 Is global? ($pte & 0x20) != 0
17 / 18
Checkpoint 2: What You Implement
Physical Page Allocator
alloc_phys_pages(cnt)
free_phys_pages(pp, cnt)
alloc_phys_page() / free_phys_page()
free_phys_page_count()
User Process Paging
map_page(vma, pp, flags)
alloc_and_map_range(vma, sz, flags)
unmap_and_free_range(vp, sz)
clone_active_mspace()
reset_active_mspace()
handle_umode_page_fault()
handle_umode_exception()
Where to Start
Look for // YOUR CODE HERE in memory.c and excp.c
18 / 18