RISC-V的PMP功能详解
1. PMP简介
PMP(Physical Memory Protection)是 RISC-V 架构中的一种硬件机制,用于控制不同内存区域的读、写和执行权限,主要提供对物理内存的保护和隔离,本文对PMP相关寄存器的说明以及PMP功能接口的实现与使用进行介绍。
2. PMP寄存器说明
PMP的使用是通过设置一对PMP寄存器操作的,包括一个是地址寄存器pmpaddr,用于设置受保护的地址区域,和一个配置寄存器pmpcfg,用于设置受保护区域的读、写、执行权限以及地址匹配模式,每一对PMP寄存器称之为一个PMP条目(entry),支持的条目数量和不同CPU硬件实现相关,通常entry数量是8、16、32和64不等,具体数量需要根据对应CPU的手册确定,软件不能使用超过CPU规定的entry数量,本文基于32位CPU的16组entry进行说明。
2.1 PMP配置寄存器(pmpcfg)
PMP每一个entry的配置主要有8bit组成,其定义如下:
bit width field_name desc note
0 1 R Read
1 1 W Write
2 1 X Execute
3-4 2 A Address matching 00: OFF(disable)
01:TOR (top of range)
10:NA4
11: NAPOT
5-6 2 Reserved Fix 0
7 1 L Lock
虽然每一个配置只有8bit,但是risc-v提供的pmpcfg寄存器在32bit CPU上是32bit的,为了充分利用每一个bit,一个32bit的pmpcfg寄存器包含4个pmpcfg配置,其结构如下:
pmpcfg0 = (pmp3cfg<<24)| (pmp2cfg<<16) | (pmp1cfg<<8) | pmp0cfg;
pmpcfg1 = (pmp7cfg<<24)| (pmp6cfg<<16) | (pmp5cfg<<8) | pmp4cfg;
pmpcfg2 = (pmp11cfg<<24)| (pmp10cfg<<16) | (pmp9cfg<<8) | pmp8cfg;
pmpcfg3 = (pmp15cfg<<24)| (pmp14cfg<<16) | (pmp13cfg<<8) | pmp12cfg;
1. R (Read)
当R==1时,表示该PMP entry对应的地址区域可读,反之不可读(如果读则触发exception);
2. W (Write)
当W==1时,表示该PMP entry对应的地址区域可写,反之不可写(如果写则触发exception);
3. X (Execute)
当X==1时,表示该PMP entry对应的地址区域可执行,反之不可执行(如果执行则触发exception);
4. A (Address matching)
00(OFF):说明这个条目是disable的,不会去匹配任何地址;
01:TOR (top of range):顶部匹配模式,需要使用2个PMP条目定义一块内存保护区域,比如entry[i-1]和entry[i],同时entry[i-1]的pmpaddr[i-1]必须小于entry[i]的pmpaddr[i],仅仅需要把entry[i]的pmpcfg[i]设置为TOR模式即可(不用考虑entry[i-1]中pmpcfg[i-1]的权限配置),如果i=0,则小于pmpaddr[0]的任何内存地址的访问权限都受entry[0]的配置影响,如果pmpaddr[i-1]>= pmpaddr[i],并且pmpcfg[i]设置TOR模式,则entry[i]匹配不到任何地址;
10:NA4(naturally aligned four-byte region):NA4是NAPOT的特殊情况,既受保护的内存大小刚好为4byte;
11: NAPOT(naturally aligned power-of-two region):表示内存大小必须是2的次幂大小,且最小值为8bytes;
5. L(Lock):如果Lock位被置1,则PMP的entry就会被锁定,写PMP的配置寄存器和地址寄存器都会被忽略,只有复位硬件才能解除锁定,当lock为0时,只有在用户模式和超级管理员模式下访问PMP配置的内存区域才会受影响,机器模式不受影响,此时机器模式可以访问任何内存区域,但是当lock为1时,所有模式下访问PMP配置的内存区域都会受影响,包括机器模式;
2.2 PMP地址寄存器(pmpaddr)
risc-v中pmpaddr定义的是34bit物理地址的高32bit,所以设置pmpaddr的时候需要将传入的实际物理内存地址左移2bit,即phy_addr << 2, 同时由于TOR模式可以通过2个entry之间的pmpaddr得到受保护的内存大小,而NA4是固定的4bytes大小,故不用在pmpaddr中传入内存大小,但是对于NAPOT模式,除了需要传入物理内存地址,还需要传入受保护的内存大小,公式如下:
pmpaddr = (phy_addr << 2) | ((phy_size>>3) - 1);//only for NAPOT mode
3. PMP接口代码实现
//PMP条目数量是根据具体CPU硬件实现来确定的,这里假设是16个entry #define ENTRY_NUM 16
3.1 pmpaddr接口函数实现
void _set_pmpaddrx(uint32_t index, uint32_t pmp_addr)
{
switch(index) {
case 0:
//基于编译器的risc-v汇编自己实现pmpaddr0 = pmp_addr
break;
case 1:
//基于编译器的risc-v汇编自己实现pmpaddr1 = pmp_addr
break;
case 2:
//基于编译器的risc-v汇编自己实现pmpaddr2 = pmp_addr
break;
case 3:
//基于编译器的risc-v汇编自己实现pmpaddr3 = pmp_addr
break;
case 4:
//基于编译器的risc-v汇编自己实现pmpaddr4 = pmp_addr
break;
case 5:
//基于编译器的risc-v汇编自己实现pmpaddr5 = pmp_addr
break;
case 6:
//基于编译器的risc-v汇编自己实现pmpaddr6 = pmp_addr
break;
case 7:
//基于编译器的risc-v汇编自己实现pmpaddr7 = pmp_addr
break;
...
case 15:
//基于编译器的risc-v汇编自己实现pmpaddr15 = pmp_addr
break;
default:
break;
}
}
//entry_index是PMP条目序号,phy_addr是物理内存地址,phy_size是需要保护的内存大小,mode是地址匹配模式
void set_pmpaddr(uint32_t entry_index, uint32_t phy_addr, uint32_t phy_size, uint8_t mode)
{
uint32_t index = 0;
uint32_t addr = 0;
uint32_t size = 0;
uint32_t pmp_addr = 0;
/* 1. set pmp entry index */
index = entry_index;
/* 2. set memory address */
addr = phy_addr >> 2;
/* 3. set memory size */
if(mode == NA4 || mode == TOR) {
pmp_addr = addr;
} else {
size = (phy_size >> 3) -1;
pmp_addr = addr | size;
}
/* 4. set pmpaddr register */
_set_pmpaddrx(index, pmp_addr);
}
注意:物理地址phy_addr低位必须足够放得下phy_size的空间,即phy_addr的低phy_size的位置必须是0,同时对于NAPOT模式下phy_size必须大于等于8byte且是2的次幂;
3.2 pmpcfg接口函数实现
uint32_t _get_pmpcfgx(uint32_t index)
{
uint32_t pmpcfg = 0;
switch(index) {
case 0:
//基于编译器的risc-v汇编自己实现pmpcfg = pmpcfg0
break;
case 1:
//基于编译器的risc-v汇编自己实现pmpcfg = pmpcfg1
break;
case 2:
//基于编译器的risc-v汇编自己实现pmpcfg = pmpcfg2
break;
case 3:
//基于编译器的risc-v汇编自己实现pmpcfg = pmpcfg3
break;
default:
break;
}
return pmpcfg;
}
void _set_pmpcfgx(uint32_t index, uint32_t pmpcfg)
{
switch(index) {
case 0:
//基于编译器的risc-v汇编自己实现pmpcfg0 = pmpcfg
break;
case 1:
//基于编译器的risc-v汇编自己实现pmpcfg1 = pmpcfg
break;
case 2:
//基于编译器的risc-v汇编自己实现pmpcfg2 = pmpcfg
break;
case 3:
//基于编译器的risc-v汇编自己实现pmpcfg3 = pmpcfg
break;
default:
break;
}
}
//entry_index是PMP条目序号,pmpcfg是pmp的配置
void set_pmpcfg(uint32_t entry_index, uint8_t pmpcfg)
{
uint32_t index = 0;
uint32_t pmp_cfgx = 0;
uint32_t offset= 0;
uint32_t pmpcfg_num = ENTRY_NUM/4;
uint32_t offset = 0;
uint32_t mask = 0;
/* 1. set pmp index */
index = entry_index >> 2;
/* 2. update pmpcfgx value */
offset= (entry_index & (pmpcfg_num - 1)) << 3;
mask = ~(0xff << offset);
pmp_cfgx = _get_pmpcfgx(index & (pmpcfg_num - 1));
pmp_cfgx |= ((pmpcfg << offset) & (~mask));
/* 3. set pmpcfg resiter */
_set_pmpcfgx(index, pmp_cfgx);
}
注意:由于一个pmpcfgx中包含4组pmpxcfg(pmp[i+3]cfg /pmp[i+2]cfg /pmp[i+1]cfg /pmp[i]cfg),所以传入的entry_index需要右移2bit,同时在设置pmpxcfg到对应的pmpcfgx寄存器的时候,需要先读取当前pmpcfgx寄存器的值,再将pmpxcfg的值改写到entry_index对应的pmpcfgx寄存器的8bit上;
4. PMP注意事项
1. [endif]PMP entry的寄存器只有在机器模式下才能设置;
2. [endif]默认情况下(Lock为0),PMP条目对内存区域的保护只针对用户模式和超级管理员模式,但是当Lock为1时,对机器模式也同样起作用;
3. [endif]在机器模式下,可以通过把mstatus.mprv寄存器设置为1,把mstatus.mpp的值设置为用户模式和超级管理员模式对应的值,从而让CPU按照用户模式和超级管理员模式来进行PMP控制;
4. [endif]当同时设置多个PMP条目时,序号越小,优先级越高,entry[0]> entry[1]> entry[2]>…entry[64];
5. Lock为1,机器模式下,没有匹配到任何PMP条目的内存区域,默认具有可读可写可执行权限;
6. 用户模式或者超级管理员模式下(包括通过mstatus.mprv和mstatus.mpp设置的),没有匹配到任何PMP条目的内存地址,默认不可访问,否则报exception;
7. PMP保护的区域不仅仅是RAM区域,还包括总线上支持的合法区域,如各个外设的地址区域,flash区域等;