RISC-V PMP 在 QEMU 中的实现

1,229 阅读8分钟

关于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负责的地址范围做了记录,记在saea中(后面会看到实现方式),而不需要每次都通过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的代码再重读官方文档才看明白的。