1 of 18

Virtual Memory & Paging

RISC-V Sv39 · MP3 Checkpoint 2

ECE 391 Discussion — Hands-on GDB Walkthrough

1 / 18

2 of 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

3 of 18

Example from lecture

4 of 18

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

5 of 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

6 of 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

7 of 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

8 of 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

9 of 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

10 of 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

11 of 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

12 of 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

13 of 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

14 of 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

15 of 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

16 of 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

17 of 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

18 of 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