本文最后更新于 2026年6月19日 08:17:00
一、PMP的背景原理
1. 组成
PMP分成pmpcfg和pmpaddr。根据csrfile的内容可知,本Lab要求实现的是单pmpcfg+单pmpaddr的组合。
2. pmpaddr
是地址右移2为后的值。实际处理过程中需要左移两位。
3. pmpcfg
单个PMP条目(entry)的pmpcfg一共8位,在本实现中只使用 pmpcfg0[7:0],也就是 entry 0。分别代表以下含义:
0
R
允许读
1
W
允许写
2
X
允许取指执行
4:3
A
地址匹配模式
7
L
锁定位
其中,A位最为重要,它决定了CPU应该如何处理pmpaddr中的值。根据pmpcfg[4:3]的值,一共分成4类:
1)A=0
表示不启用PMP表项,不匹配任何地址
2)A=1
表示Top of range:
若当前PMP不是第0个,匹配 \((pmpaddr_{i-1} << 2) \le y < (pmpaddr_i << 2)\)
若当前PMP是第0个,匹配 \(0 \le y < (pmpaddr_0 << 2)\)
3)A=2
表示Naturally aligned four-byte region,表示从 \((pmpaddr << 2)\) 开始的4字节(包含)
4)A=3
表示Naturally aligned power-of-two region,\(\ge\) 8 bytes 这时会从低位开始寻找连续1的个数,设找到n个,则表示从 (pmpaddr & ~((1 << x) - 1)) << 2 开始的\(2^{n+3}\) 个字节
二、PMP的具体实施
1. pmp_checker
这个模块用于判断地址是否匹配,输入物理地址、访问大小、访问类型、当前特权级和 PMP 配置,输出本次访问是否允许以及对应的 access fault cause。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 pmp_checker.sv module pmp_checker import common::*; import my_pkg::*; ( input u64 paddr, input msize_t access_size, input mmu_access_t access_type, input priv_t current_priv, input u64 pmpcfg0, pmpaddr0, output u1 allow, output u64 fault_cause );
pmpcfg0[7:0] 被拆分成权限位、地址匹配模式和锁定位。PMP 拒绝访问时按照原始访问类型产生 access fault:取指为 1,load 为 5,store 为 7。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pmp_checker.sv always_comb begin unique case (access_type) MMU_REQ_INST: access_fault_cause = 64'd1 ; MMU_REQ_LOAD: access_fault_cause = 64'd5 ; MMU_REQ_STORE: access_fault_cause = 64'd7 ; default : access_fault_cause = 64'd5 ; endcase end assign cfg0 = pmpcfg0[7 :0 ]; assign pmp_r = cfg0[0 ]; assign pmp_w = cfg0[1 ]; assign pmp_x = cfg0[2 ]; assign pmp_a = cfg0[4 :3 ]; assign pmp_l = cfg0[7 ];
定义辅助函数msize_to_bytes,将枚举类型对应回具体数值
1 2 3 4 5 6 7 8 9 10 11 function automatic u64 msize_to_bytes(input msize_t size); begin unique case (size) MSIZE1: msize_to_bytes = 64'd1 ; MSIZE2: msize_to_bytes = 64'd2 ; MSIZE4: msize_to_bytes = 64'd4 ; MSIZE8: msize_to_bytes = 64'd8 ; default : msize_to_bytes = 64'd8 ; endcase end endfunction
定义辅助函数count_low_ones,检查从最后一位开始的连续1的个数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function automatic u7 count_low_ones(input u64 value); u7 count; u1 done; begin count = 7'b0 ; done = 1'b0 ; for (int unsigned i = 0 ; i < 64 ; i++) begin if (!done) begin if (value[i]) begin count = count + 7'd1 ; end else begin done = 1'b1 ; end end end count_low_ones = count; end endfunction
根据A位的值具体对应规则。需要注意边界条件: - TOR时pmpaddr不能为0。当pmpaddr=0时,对应[0,0),区间为空。但由于数值溢出的原因,计算时反而会匹配[0,0xffff_ffff_ffff_ffff),导致错误 - 当pmpaddr全为1时,会左移64位,属于不规范操作。因此单独处理 - 连续1的个数>=61位时,左移位数>=64,同样单独处理,解释为匹配所有地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 pmp_checker.sv always_comb begin napot_ones = count_low_ones(pmpaddr0); napot_shift = napot_ones + 7'd3 ; napot_mask = 64'b0 ; napot_bytes = 64'b0 ; range_start = 64'b0 ; range_end = 64'b0 ; range_valid = 1'b0 ; unique case (pmp_a) 2'b00 : begin range_valid = 1'b0 ; end 2'b01 : begin range_start = 64'b0 ; range_end = (pmpaddr0 << 2 ) - 64'd1 ; range_valid = (pmpaddr0 != 64'b0 ); end 2'b10 : begin range_start = pmpaddr0 << 2 ; range_end = (pmpaddr0 << 2 ) + 64'd3 ; range_valid = 1'b1 ; end 2'b11 : begin if (napot_ones >= 7'd64 ) begin napot_mask = {64 {1'b1 }}; end else begin napot_mask = (64'b1 << napot_ones) - 64'd1 ; end if (napot_shift >= 7'd64 ) begin range_start = 64'b0 ; range_end = {64 {1'b1 }}; end else begin napot_bytes = 64'b1 << napot_shift; range_start = (pmpaddr0 & ~napot_mask) << 2 ; range_end = range_start + napot_bytes - 64'd1 ; end range_valid = 1'b1 ; end default : begin range_valid = 1'b0 ; end endcase end
权限判断遵循 M/S/U 的差异:L=0 时 M 模式不受限制;S/U 模式必须匹配 entry 并满足对应 R/W/X 权限;未匹配时 S/U 拒绝、M 模式允许。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pmp_checker.sv always_comb begin unique case (access_type) MMU_REQ_INST: perm_allow = pmp_x; MMU_REQ_LOAD: perm_allow = pmp_r; MMU_REQ_STORE: perm_allow = pmp_w; default : perm_allow = 1'b0 ; endcase end always_comb begin if ((current_priv == PRIV_M) && !pmp_l) begin allow = 1'b1 ; end else if (!addr_match) begin allow = (current_priv == PRIV_M); end else begin allow = perm_allow; end end assign fault_cause = allow ? 64'b0 : access_fault_cause;
为了避免只检查起始地址导致跨界访问漏判,checker 先根据访存大小计算 [paddr, access_last]。只有完整访问范围落入 PMP 区间,才认为地址匹配。
1 2 3 4 pmp_checker.sv assign access_bytes = msize_to_bytes(access_size); assign access_last = paddr + access_bytes - 64'd1 ; assign access_overflow = access_last < paddr;
最终判断式如下 1 2 3 4 assign addr_match = range_valid && !access_overflow && (paddr >= range_start) && (access_last <= range_end);
2. csr_file
CSR 文件把 pmpcfg0 和 pmpaddr0 作为输出暴露给顶层,并实现 L 位锁定语义。锁定后对 pmpcfg0 和 pmpaddr0 的后续写入都会被忽略,直到 reset。
1 2 3 csr_file.sv output u64 stvec, sscratch, sepc, scause, stval, medeleg, mideleg, output u64 pmpcfg0, pmpaddr0
由于只实现pmpcfg0,因此截取低8位,且5、6位保持为0 1 2 3 4 5 6 7 8 csr_file.sv function automatic u64 legalize_pmpcfg0(input u64 value); begin legalize_pmpcfg0 = {56'b0 , value[7 ], 2'b0 , value[4 :0 ]}; end endfunction pmpcfg0_reg <= (csr_write_enable && (csr_write_addr == CSR_PMPCFG0) && !pmpcfg0_reg[7 ]) ?legalize_pmpcfg0(csr_write_data) : pmpcfg0_reg; pmpaddr0_reg <= (csr_write_enable && (csr_write_addr == CSR_PMPADDR0) && !pmpcfg0_reg[7 ]) ?csr_write_data : pmpaddr0_reg;
3. core
就是普通的连线:加入 pmp_checker include;声明从 CSR 文件接出的 PMP 配置线;把配置线分别接到 csr_file 和 MMU。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 core.sv `include "src/utils/pmp_checker.sv" u64 csr_pmpcfg0, csr_pmpaddr0; csr_file u_csr_file( .mideleg (csr_mideleg), .pmpcfg0 (csr_pmpcfg0), .pmpaddr0 (csr_pmpaddr0) ); MMU u_mmu( .current_priv (current_priv), .satp (csr_satp), .pmpcfg0 (csr_pmpcfg0), .pmpaddr0 (csr_pmpaddr0), .core_req (mmu_core_req) );
4. MMU
每一次访存请求都需要经过pmp_checker的判断,如果判断地址不可用,直接报错。 具体实施如下:
mmu.sv 的接口新增 pmpcfg0/pmpaddr0 输入,并新增 PMP 检查结果与 access fault cause 信号。saved_access_fault_cause 用于页表 walk 被 PMP 拒绝时按原始访问类型报告 access fault。
1 2 3 4 5 6 7 8 mmu.sv input u64 pmpcfg0, pmpaddr0, u64 request_fault_cause, saved_fault_cause, saved_access_fault_cause; u64 req_pmp_fault_cause, pte_pmp_fault_cause, l2_pmp_fault_cause, l1_pmp_fault_cause, l0_pmp_fault_cause; u1 req_pmp_allow, pte_pmp_allow, l2_pmp_allow, l1_pmp_allow, l0_pmp_allow; `UNUSED_OK({pte_pmp_fault_cause})
mmu.sv 中一共例化五个 pmp_checker:原始请求、页表 PTE 读取、以及 L2/L1/L0 三级页表叶子命中后的最终物理地址检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 mmu.sv pmp_checker u_req_pmp_checker( .paddr (core_req.addr ), .access_size (core_req.size ), .access_type (req_access_type), .current_priv (current_priv), .pmpcfg0 (pmpcfg0), .pmpaddr0 (pmpaddr0), .allow (req_pmp_allow), .fault_cause (req_pmp_fault_cause) ); pmp_checker u_pte_pmp_checker( .paddr (pte_addr), .access_size (MSIZE8), .access_type (MMU_REQ_LOAD), .current_priv (saved_priv), .pmpcfg0 (pmpcfg0), .pmpaddr0 (pmpaddr0), .allow (pte_pmp_allow), .fault_cause (pte_pmp_fault_cause) ); pmp_checker u_l2_pmp_checker( .paddr (l2_paddr), .access_size (saved_req.size ), .access_type (saved_access_type), .current_priv (saved_priv), .pmpcfg0 (pmpcfg0), .pmpaddr0 (pmpaddr0), .allow (l2_pmp_allow), .fault_cause (l2_pmp_fault_cause) ); pmp_checker u_l1_pmp_checker( .paddr (l1_paddr), .access_size (saved_req.size ), .access_type (saved_access_type), .current_priv (saved_priv), .pmpcfg0 (pmpcfg0), .pmpaddr0 (pmpaddr0), .allow (l1_pmp_allow), .fault_cause (l1_pmp_fault_cause) ); pmp_checker u_l0_pmp_checker( .paddr (l0_paddr), .access_size (saved_req.size ), .access_type (saved_access_type), .current_priv (saved_priv), .pmpcfg0 (pmpcfg0), .pmpaddr0 (pmpaddr0), .allow (l0_pmp_allow), .fault_cause (l0_pmp_fault_cause) );
saved_access_fault_cause 用于把页表 walk 期间的 PMP 拒绝转换成原始取指、load、store 对应的 access fault。
1 2 3 4 5 6 7 8 9 mmu.sv always_comb begin unique case (saved_access_type) MMU_REQ_INST: saved_access_fault_cause = 64'd1 ; MMU_REQ_LOAD: saved_access_fault_cause = 64'd5 ; MMU_REQ_STORE: saved_access_fault_cause = 64'd7 ; default : saved_access_fault_cause = 64'd5 ; endcase end
页表 walk 发出 PTE 读取请求前增加 pte_pmp_allow 门控,避免在 PMP 已拒绝的情况下继续访问总线。
1 2 3 4 5 6 mmu.sv MMU_L2, MMU_L1, MMU_L0: begin if (pte_pmp_allow) begin dreq = pte_req; end end
未开启分页时,MMU_IDLE 在进入 MMU_DIRECT 前检查原始物理地址。PMP 拒绝时直接进入 MMU_FAULT。
1 2 3 4 5 6 7 8 mmu.sv end else if (!req_pmp_allow) begin state <= MMU_FAULT; fault_cause <= req_pmp_fault_cause; fault_tval <= core_req.addr ; end else begin state <= MMU_DIRECT; end
MMU_L2 状态先检查 PTE 读取地址是否被 PMP 允许,再在 L2 叶子 PTE 命中时检查 l2_paddr。允许后才更新 saved_req.addr。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 mmu.sv MMU_L2: begin if (!pte_pmp_allow) begin state <= MMU_FAULT; fault_cause <= saved_access_fault_cause; fault_tval <= saved_vaddr; end else if (dresp.data_ok ) begin if (pte_fault) begin state <= MMU_FAULT; fault_cause <= saved_fault_cause; fault_tval <= saved_vaddr; end else if (pte_is_leaf) begin if (!l2_pmp_allow) begin state <= MMU_FAULT; fault_cause <= l2_pmp_fault_cause; fault_tval <= saved_vaddr; end else begin state <= MMU_ACCESS; saved_req.addr <= l2_paddr; end end else begin state <= MMU_L1; page_base <= l1_page_base; end end end
MMU_L1 状态同样先检查 PTE 读取,再在 L1 叶子 PTE 命中时检查 l1_paddr。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 mmu.sv MMU_L1: begin if (!pte_pmp_allow) begin state <= MMU_FAULT; fault_cause <= saved_access_fault_cause; fault_tval <= saved_vaddr; end else if (dresp.data_ok ) begin if (pte_fault) begin state <= MMU_FAULT; fault_cause <= saved_fault_cause; fault_tval <= saved_vaddr; end else if (pte_is_leaf) begin if (!l1_pmp_allow) begin state <= MMU_FAULT; fault_cause <= l1_pmp_fault_cause; fault_tval <= saved_vaddr; end else begin state <= MMU_ACCESS; saved_req.addr <= l1_paddr; end end else begin state <= MMU_L0; page_base <= l0_page_base; end end end
MMU_L0 状态检查 PTE 读取后,对最终 4KB 页物理地址 l0_paddr 做 PMP 检查。通过后进入 MMU_ACCESS。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 mmu.sv MMU_L0: begin if (!pte_pmp_allow) begin state <= MMU_FAULT; fault_cause <= saved_access_fault_cause; fault_tval <= saved_vaddr; end else if (dresp.data_ok ) begin if (pte_fault) begin state <= MMU_FAULT; fault_cause <= saved_fault_cause; fault_tval <= saved_vaddr; end else begin if (!l0_pmp_allow) begin state <= MMU_FAULT; fault_cause <= l0_pmp_fault_cause; fault_tval <= saved_vaddr; end else begin state <= MMU_ACCESS; saved_req.addr <= l0_paddr; end end end end
三、最终输出
经过测试,无法通过make test-labplus-4,程序长时间无法退出。经过排查源码,发现需要实现U模式,但之前的lab没有这一要求(我只实现了S模式)
四、参考资料
知乎-RISC-V PMP物理内存保护机制详解
RISC-V官方手册: The RISC-V Instruction Set Manual Volume II: Privileged Architecture