关于RISC-V PMP机制,详见:juejin.cn/post/684490…
综述
QEMU中关于RISC-V PMP的部分在target\riscv\pmp.c中。
在这部分,目前设置了如下public interface:
bool pmp_hart_has_privs(CPURISCVState *env, target_ulong addr,
target_ulong size, pmp_priv_t privs, target_ulong mode)
void pmpcfg_csr_write(CPURISCVState *env, uint32_t reg_index,
target_ulong val)
target_ulong pmpcfg_csr_read(CPURISCVState *env, uint32_t reg_index)
void pmpaddr_csr_write(CPURISCVState *env, uint32_t addr_index,
target_ulong val)
target_ulong pmpaddr_csr_read(CPURISCVState *env, uint32_t addr_index)
第一个api用来判断对于某个物理地址的操作(Read, Write, Execute)是否有权限。之后的api分别用来对PMP entry的地址寄存器和配置寄存器进行修改。在target\riscv\pmp.c中,注释提到,PMP功能目前还没有完整经过测试:
PMP (Physical Memory Protection) is as-of-yet unused and needs testing.
目前上述api中,只有第一个pmp_hart_has_privs由外部调用过(因为PMP功能还没有公开给用户使用),故我们从这个api进入源码进行分析。
bool pmp_hart_has_privs()
先看完整代码:
bool pmp_hart_has_privs(CPURISCVState *env, target_ulong addr,
target_ulong size, pmp_priv_t privs, target_ulong mode)
{
int i = 0;
int ret = -1;
int pmp_size = 0;
target_ulong s = 0;
target_ulong e = 0;
pmp_priv_t allowed_privs = 0;
/* Short cut if no rules */
if (0 == pmp_get_num_rules(env)) {
return true;
}
/*
* if size is unknown (0), assume that all bytes
* from addr to the end of the page will be accessed.
*/
if (size == 0) {
pmp_size = -(addr | TARGET_PAGE_MASK);
} else {
pmp_size = size;
}
/* 1.10 draft priv spec states there is an implicit order
from low to high */
for (i = 0; i < MAX_RISCV_PMPS; i++) {
s = pmp_is_in_range(env, i, addr);
e = pmp_is_in_range(env, i, addr + pmp_size - 1);
/* partially inside */
if ((s + e) == 1) {
qemu_log_mask(LOG_GUEST_ERROR,
"pmp violation - access is partially inside\n");
ret = 0;
break;
}
/* fully inside */
const uint8_t a_field =
pmp_get_a_field(env->pmp_state.pmp[i].cfg_reg);
/*
* If the PMP entry is not off and the address is in range, do the priv
* check
*/
if (((s + e) == 2) && (PMP_AMATCH_OFF != a_field)) {
allowed_privs = PMP_READ | PMP_WRITE | PMP_EXEC;
if ((mode != PRV_M) || pmp_is_locked(env, i)) {
allowed_privs &= env->pmp_state.pmp[i].cfg_reg;
}
if ((privs & allowed_privs) == privs) {
ret = 1;
break;
} else {
ret = 0;
break;
}
}
}
/* No rule matched */
if (ret == -1) {
if (mode == PRV_M) {
ret = 1; /* Privileged spec v1.10 states if no PMP entry matches an
* M-Mode access, the access succeeds */
} else {
ret = 0; /* Other modes are not allowed to succeed if they don't
* match a rule, but there are rules. We've checked for
* no rule earlier in this function. */
}
}
return ret == 1 ? true : false;
}
函数参数
- env:保存RISC-V CPU全部信息
- addr:hart要访问的基地址
- size:从基地址开始,hart要访问多大的地址空间
- privs:hart想要使用的权限
- mode:cpu所处的特权模式(U/S/M)
函数过程
获取PMP规则数目
若没有设置PMP规则,即PMP未启用,那所有的访问都是允许的,直接返回True
下面我们看一下如何获取PMP规则数目:
static inline uint32_t pmp_get_num_rules(CPURISCVState *env)
{
return env->pmp_state.num_rules;
}
直接返回pmp_state.num_rules成员,那么一定有什么地方实时更新这个成员变量。我们在接下来其它函数中会看到。
处理未声明size大小的情况
目前的git版本中这部分内容如下:
/*
* if size is unknown (0), assume that all bytes
* from addr to the end of the page will be accessed.
*/
if (size == 0) {
pmp_size = -(addr | TARGET_PAGE_MASK);
} else {
pmp_size = size;
}
即,若是没有声明的话,默认需要访问整个页的大小。但是,在没有启用虚拟内存分页机制的情况下,这样的地址空间显然太大了。于是,在还没有被同步到主仓库的patch中,我们看到了Alistair将其做了以下修改:
Signed-off-by: Alistair Francis <address@hidden>
---
target/riscv/pmp.c | 19 ++++++++++++++-----
1 file changed, 14 insertions(+), 5 deletions(-)
diff --git a/target/riscv/pmp.c b/target/riscv/pmp.c
index 0e6b640fbd..5aba4d13ea 100644
--- a/target/riscv/pmp.c
+++ b/target/riscv/pmp.c
@@ -233,12 +233,21 @@ bool pmp_hart_has_privs(CPURISCVState *env, target_ulong
addr,
return true;
}
- /*
- * if size is unknown (0), assume that all bytes
- * from addr to the end of the page will be accessed.
- */
if (size == 0) {
- pmp_size = -(addr | TARGET_PAGE_MASK);
+ if (!riscv_feature(env, RISCV_FEATURE_MMU)) {
+ /*
+ * if size is unknown (0), assume that all bytes
+ * from addr to the end of the page will be accessed.
+ */
+ pmp_size = -(addr | TARGET_PAGE_MASK);
+ } else {
+ /*
+ * If size is unknown (0) and we don't have an MMU,
+ * just guess the size as the xlen as we don't want to
+ * access an entire page worth.
+ */
+ pmp_size = sizeof(target_ulong);
+ }
} else {
pmp_size = size;
}
--
也就是说,当没有启用分页的时候,猜一个访问空间大小,将其设置为sizeof(target_ulong),对于32位即4个字节,正好是PMP支持的最小颗粒度。
判断地址空间
接下来要将hart希望访问的地址空间与每一个PMP entry管理的地址空间相比较。即如下这个for循环:
for (i = 0; i < MAX_RISCV_PMPS; i++) {
s = pmp_is_in_range(env, i, addr);
e = pmp_is_in_range(env, i, addr + pmp_size - 1);
/* partially inside */
if ((s + e) == 1) {
qemu_log_mask(LOG_GUEST_ERROR,
"pmp violation - access is partially inside\n");
ret = 0;
break;
}
/* fully inside */
const uint8_t a_field =
pmp_get_a_field(env->pmp_state.pmp[i].cfg_reg);
/*
* If the PMP entry is not off and the address is in range, do the priv
* check
*/
if (((s + e) == 2) && (PMP_AMATCH_OFF != a_field)) {
allowed_privs = PMP_READ | PMP_WRITE | PMP_EXEC;
if ((mode != PRV_M) || pmp_is_locked(env, i)) {
allowed_privs &= env->pmp_state.pmp[i].cfg_reg;
}
if ((privs & allowed_privs) == privs) {
ret = 1;
break;
} else {
ret = 0;
break;
}
}
}
循环遍历所有的PMP entry。对于任一PMP entry,分别判断hart希望访问的地址空间的首尾是否在其管理的物理地址范围之内,若是,记为1,若否,记为0。这部分工作由pmp_is_in_range函数完成。
static int pmp_is_in_range()
函数内容如下:
static int pmp_is_in_range(CPURISCVState *env, int pmp_index, target_ulong addr)
{
int result = 0;
if ((addr >= env->pmp_state.addr[pmp_index].sa)
&& (addr <= env->pmp_state.addr[pmp_index].ea)) {
result = 1;
} else {
result = 0;
}
return result;
}
这里,只需要判断一个给定的地址是否属于某个PMP entry的管理范围。而在QEMU中对PMP entry做了优化,在设置规则的时候就对一个PMP entry负责的地址范围做了记录,记在sa与ea中(后面会看到实现方式),而不需要每次都通过PMP配置寄存器的A字段和对应的地址寄存器来判断。
若hart要访问的空间只有部分在某个PMP entry的管理范围,而不是全部嵌入或完全不相交,则结束循环,提示错误信息:
/* partially inside */
if ((s + e) == 1) {
qemu_log_mask(LOG_GUEST_ERROR,
"pmp violation - access is partially inside\n");
ret = 0;
break;
}
若不在这个PMP entry的管理范围,则继续循环。
若在当前PMP entry的地址管理范围,则获取当前PMP entry的配置寄存器的A字段:
const uint8_t a_field =
pmp_get_a_field(env->pmp_state.pmp[i].cfg_reg);
pmp_get_a_field函数实现也很简单:
static inline uint8_t pmp_get_a_field(uint8_t cfg)
{
uint8_t a = cfg >> 3;
return a & 0x3;
}
直接写成了内联函数。
之后就进入了匹配PMP entry情况下的核心部分了:
/*
* If the PMP entry is not off and the address is in range, do the priv
* check
*/
if (((s + e) == 2) && (PMP_AMATCH_OFF != a_field)) {
allowed_privs = PMP_READ | PMP_WRITE | PMP_EXEC;
if ((mode != PRV_M) || pmp_is_locked(env, i)) {
allowed_privs &= env->pmp_state.pmp[i].cfg_reg;
}
if ((privs & allowed_privs) == privs) {
ret = 1;
break;
} else {
ret = 0;
break;
}
}
判断条件是PMP entry匹配且当前PMP entry的配置寄存器的A字段不为OFF。可以看一下QEMU对于
A字段的枚举定义:
typedef enum {
PMP_AMATCH_OFF, /* Null (off) */
PMP_AMATCH_TOR, /* Top of Range */
PMP_AMATCH_NA4, /* Naturally aligned four-byte region */
PMP_AMATCH_NAPOT /* Naturally aligned power-of-two region */
} pmp_am_t;
这与PMP机制中对于A的取值是对应的。
在手册中提到过,当CPU处于M模式的时候,除非PMP的L位为1,否则允许所有操作。故允许所有操作的条件是CPU处于M模式且PMP的L位为0。对其取非即为需要判断权限的情况,即CPU不是M模式,或L位为1,即之后的判断条件:(mode != PRV_M) || pmp_is_locked(env, i)
之后导出PMP配置寄存器中的权限情况,和hart想要使用的权限进行比对即可。
有意思的是这里代码的实现方式,即将PMP配置寄存器声明了一个8位的空间:
uint8_t cfg_reg;
和实际中PMP配置寄存器的大小相同。而将权限枚举如下:
typedef enum {
PMP_READ = 1 << 0,
PMP_WRITE = 1 << 1,
PMP_EXEC = 1 << 2,
PMP_LOCK = 1 << 7
} pmp_priv_t;
那么之后只需要按位比较和操作allowed_privs即可。这种方式让我想起了linux的chmod对于权限的说明方式。
没有匹配的规则
若遍历了所有的PMP entry但是没有匹配的规则,需要判断当前是否是M模式,若是M模式,那么直接允许访问,若不是,则根据官方文档,拒绝访问(因为之前已经判断过是否启用了PMP了)
/* No rule matched */
if (ret == -1) {
if (mode == PRV_M) {
ret = 1; /* Privileged spec v1.10 states if no PMP entry matches an
* M-Mode access, the access succeeds */
} else {
ret = 0; /* Other modes are not allowed to succeed if they don't
* match a rule, but there are rules. We've checked for
* no rule earlier in this function. */
}
}
static void pmp_update_rule()
另一个重要的函数是对于规则的更新。在写配置寄存器的时候会调用这个函数。函数如下:
static void pmp_update_rule(CPURISCVState *env, uint32_t pmp_index)
{
int i;
env->pmp_state.num_rules = 0;
uint8_t this_cfg = env->pmp_state.pmp[pmp_index].cfg_reg;
target_ulong this_addr = env->pmp_state.pmp[pmp_index].addr_reg;
target_ulong prev_addr = 0u;
target_ulong sa = 0u;
target_ulong ea = 0u;
if (pmp_index >= 1u) {
prev_addr = env->pmp_state.pmp[pmp_index - 1].addr_reg;
}
switch (pmp_get_a_field(this_cfg)) {
case PMP_AMATCH_OFF:
sa = 0u;
ea = -1;
break;
case PMP_AMATCH_TOR:
sa = prev_addr << 2; /* shift up from [xx:0] to [xx+2:2] */
ea = (this_addr << 2) - 1u;
break;
case PMP_AMATCH_NA4:
sa = this_addr << 2; /* shift up from [xx:0] to [xx+2:2] */
ea = (this_addr + 4u) - 1u;
break;
case PMP_AMATCH_NAPOT:
pmp_decode_napot(this_addr, &sa, &ea);
break;
default:
sa = 0u;
ea = 0u;
break;
}
env->pmp_state.addr[pmp_index].sa = sa;
env->pmp_state.addr[pmp_index].ea = ea;
for (i = 0; i < MAX_RISCV_PMPS; i++) {
const uint8_t a_field =
pmp_get_a_field(env->pmp_state.pmp[i].cfg_reg);
if (PMP_AMATCH_OFF != a_field) {
env->pmp_state.num_rules++;
}
}
}
这部分完成的功能是将一个PMP entry控制的地址空间计算出来并且将其首尾地址放入QEMU设计的sa和ea中。这样的设计可以减少判断管理地址空间时的开销,相当于一种用空间换时间的优化方式。
值得注意的是这里对于NAPOT模式下地址空间的解析函数:pmp_decode_napot,代码如下:
static void pmp_decode_napot(target_ulong a, target_ulong *sa, target_ulong *ea)
{
/*
aaaa...aaa0 8-byte NAPOT range
aaaa...aa01 16-byte NAPOT range
aaaa...a011 32-byte NAPOT range
...
aa01...1111 2^XLEN-byte NAPOT range
a011...1111 2^(XLEN+1)-byte NAPOT range
0111...1111 2^(XLEN+2)-byte NAPOT range
1111...1111 Reserved
*/
if (a == -1) {
*sa = 0u;
*ea = -1;
return;
} else {
target_ulong t1 = ctz64(~a);
target_ulong base = (a & ~(((target_ulong)1 << t1) - 1)) << 2;
target_ulong range = ((target_ulong)1 << (t1 + 3)) - 1;
*sa = base;
*ea = base + range;
}
}
关键的是else中的部分。首先将a(PMP地址寄存器中的地址)取反,判断末尾连续0的个数,也就是a末尾连续1的个数t1。
之后将1左移t1位,减一获得一个末尾为t1个连续1的中间结果,取非,和a做与,再左移两位。这部分实际上就取出了基地址。
之后计算管理的地址空间大小,从而算出开始地址和结束地址。
这段代码可以说是非常精妙且容易理解。事实上,关于PMP配置寄存器中NAPOT模式下的地址空间计算方法,我也是先看了QEMU的代码再重读官方文档才看明白的。