RISCV触发模块的应用——实现内存区域读写限制

一、前言

在计算机系统中,NULL指针(地址 0x00000000)往往被视为一个特殊的禁区。许多操作系统,软件和处理器都对其采用一些保护措施。

例如,软件返回空指针通常表示资源不足,而访问 NULL 指针并进行读取或修改通常会导致异常,比如常见的Segment FaultHard Fault,这有助于及早发现软件问题。

未初始化的指针通常表现为 0 值,随意访问 0 地址可能导致不可控的结果。

对于RISCV处理器,我们首先想到的是利用PMP对0地址读写进行限制。但由于0地址是ILM内存的起始地址,由于ILM的特殊性,CPU 内核访问 ILM 时可以绕过总线直接存取,这导致PMP无法限制访问ILM地址。

那么是否就没有办法实现读写0地址就触发异常了呢?

实际上还有一种特殊的方法,可能大家也想到了,当我们利用调试器打WATCH Point的时候,为什么可以实现当有指令修改内存的时候停下来?这就是利用Trigger Module实现的对特定地址进行读写的时候触发调试异常

二、测试芯片触发器数量和功能

通过查询RISCV内核手册 RISC-V External Debug Support Chapter 5 Trigger Module我们可以知晓Trigger Module的相关资料。

首先我们按照文档内容测试芯片具有几个触发器

以下是根据文档编写的测试代码

void detect_trigger(void)
{
    for (uint8_t i = 0; i < 16; i++)
    {
        write_csr(CSR_TSELECT, i);
        printf("trigger %d\n", i);

        if (read_csr(CSR_TSELECT) != i)
        {
            printf("  not supported\n\n");
            break;
        }

        if (CSR_TDATA1_TYPE_GET(read_csr(CSR_TDATA1)) == 0)
        {
            printf("  not supported\n\n");
            break;
        }
        else
        {
            printf("  type %d\n", CSR_TDATA1_TYPE_GET(read_csr(CSR_TDATA1)));
        }

        if (CSR_TINFO_INFO_GET(read_csr(CSR_TINFO)) == 1)
        {
            printf("  not supported\n\n");
            break;
        }
        else
        {
            printf("  info %d\n", CSR_TINFO_INFO_GET(read_csr(CSR_TINFO)));
        }

        printf("  dmode %d\n", CSR_TDATA1_DMODE_SET(read_csr(CSR_TDATA1)));

        printf("\n");
    }
}

对于HPM6750,我们可以得到以下结果

trigger 0
  type 2
  info 28
  dmode 0

trigger 1
  type 2
  info 44
  dmode 0

trigger 2
  not supported

再对手册进行查询,以下两段可以解释触发器的功能。

对于 trigger 0,支持 type2, type3和type4这三种模式。
对于 trigger 1,支持 type2, type3和type5这三种模式。
对于限制内存读写这个应用来说,只需要支持type2即可实现。

三、基本用法-实现读写0地址触发调试异常

代码主要参考手册的寄存器编写,具体寄存器定义可以查看手册,其中寄存器配置有一定顺序,且最后需要使能触发器。

void setup_trigger(uint32_t target_addr)
{
    uint32_t mcontrol;

    /* select trigger 0 */
    write_csr(CSR_TSELECT, 0);

    /* set type to 2 in mcontrol, type 2 (address/data match trigger.) */
    mcontrol = read_csr(CSR_TDATA1);
    mcontrol &= ~CSR_MCONTROL_TYPE_MASK;
    mcontrol |= CSR_MCONTROL_TYPE_SET(2);
    write_csr(CSR_MCONTROL, mcontrol);

    /* set action bit (bit12) in mcontrol, action = 0 on match (Raise a breakpoint exception)*/
    mcontrol = read_csr(CSR_MCONTROL);
    mcontrol &= ~CSR_MCONTROL_ACTION_MASK;
    write_csr(CSR_MCONTROL, mcontrol);

    /* set m mode bit (bit6), enable trigger in machine mode */
    mcontrol = read_csr(CSR_MCONTROL);
    mcontrol &= ~CSR_MCONTROL_M_MASK;
    mcontrol |= CSR_MCONTROL_M_SET(1);
    write_csr(CSR_MCONTROL, mcontrol);

    /* enable load & store address match */
    mcontrol = read_csr(CSR_MCONTROL);
    mcontrol &= ~(CSR_MCONTROL_EXECUTE_MASK | CSR_MCONTROL_STORE_MASK | CSR_MCONTROL_LOAD_MASK);
    mcontrol |= CSR_MCONTROL_LOAD_SET(1) | CSR_MCONTROL_STORE_SET(1);
    write_csr(CSR_MCONTROL, mcontrol);

    /* write match address */
    write_csr(CSR_TDATA2, target_addr);

    /* enable trigger in machine mode */
    write_csr(CSR_TCONTROL, CSR_TCONTROL_MTE_SET(1));
}

测试代码及结果如下,需要修改下exception_handler,然后分读写两次测试:

long exception_handler(long cause, long epc)
{
    printf("cause %d,  epc 0x%08x\r\n", cause, epc);
    while (1);
    /* Unhandled Trap */
    return epc;
}

int main(void)
{
    board_init();
    board_init_led_pins();

    detect_trigger();

    setup_trigger(0x00000000);

    volatile uint32_t *addr = (uint32_t *)0x00000000;

    printf("try to write\r\n");
    *addr = 0xDEADBEEF;

    printf("try to read\r\n");
    printf("addr: %x\n", *addr);

    while (1)
    {
        printf("hello world\r\n");
        board_delay_ms(1000);
    }

    return 0;
}
try to write
cause 3,  epc 0x80006072

try to read
cause 3,  epc 0x8000606a

其中 cause 3 表示断点异常,而epc也就是读写指令执行的地址。

四、进阶用法-实现内存区域读写限制

在上一步中,我们实现了对0地址访问和修改的限制,但是当地址只要不为0,比如为1的时候,就不受此限制。这对于我们的实际应用来说是一个很大的限制,那么有什么方法能解决这个问题呢?

实际上根据手册,mcontrol寄存器还有很多其他的配置项,比如timing字段可以配置触发时机,而其中的match字段,则可以配置匹配规则。

其中match配置为1的时候,也就可以实现对一段内存进行保护,实现代码如下:

/**
 * @brief Restricts read and write access to specified memory areas
 * @param region_addr - region start address
 * @param region_size - region size, must be power-of-2
 */
void trigger_config(uint8_t trigger, uint32_t region_addr, uint32_t region_size)
{
    uint32_t mcontrol;

    /* select trigger */
    write_csr(CSR_TSELECT, trigger);

    /* set type to 2 in mcontrol, type 2 (address/data match trigger.) */
    mcontrol = read_csr(CSR_TDATA1);
    mcontrol &= ~CSR_MCONTROL_TYPE_MASK;
    mcontrol |= CSR_MCONTROL_TYPE_SET(2);
    write_csr(CSR_MCONTROL, mcontrol);

    /* set action bit (bit12) in mcontrol, action = 0 on match (Raise a breakpoint exception)*/
    mcontrol = read_csr(CSR_MCONTROL);
    mcontrol &= ~CSR_MCONTROL_ACTION_MASK;
    write_csr(CSR_MCONTROL, mcontrol);

    /* set match mode 1, Matches when the top M bits of the value match the top M bits of tdata2 */
    mcontrol = read_csr(CSR_MCONTROL);
    mcontrol &= ~CSR_MCONTROL_MATCH_MASK;
    mcontrol |= CSR_MCONTROL_MATCH_SET(1);
    write_csr(CSR_MCONTROL, mcontrol);

    /* set m mode bit (bit6), enable trigger in machine mode */
    mcontrol = read_csr(CSR_MCONTROL);
    mcontrol &= ~CSR_MCONTROL_M_MASK;
    mcontrol |= CSR_MCONTROL_M_SET(1);
    write_csr(CSR_MCONTROL, mcontrol);

    /* set load & store mode bit to 1 in mcontrol, enable load & store address match */
    mcontrol = read_csr(CSR_MCONTROL);
    mcontrol &= ~(CSR_MCONTROL_EXECUTE_MASK | CSR_MCONTROL_STORE_MASK | CSR_MCONTROL_LOAD_MASK);
    mcontrol |= CSR_MCONTROL_LOAD_SET(1) | CSR_MCONTROL_STORE_SET(1);
    write_csr(CSR_MCONTROL, mcontrol);

    write_csr(CSR_TDATA2, region_addr + (region_size >> 1) - 1);

    /* enable trigger in machine mode */
    write_csr(CSR_TCONTROL, CSR_TCONTROL_MTE_SET(1));
}

同时,我们还可以在异常里面对mtval增加打印,mepc指示访问或修改限制区域的指令的地址,mtval指示被访问或修改的地址。

long exception_handler(long cause, long epc)
{
    long mtval = read_csr(CSR_MTVAL);
    printf("mcause %d,  mepc 0x%08x, mtval 0x%08x\r\n", cause, epc, mtval);
    while (1)
        ;
    /* Unhandled Trap */
    return epc;
}

最后,我们写一段代码进行测试,分别对1023和1024地址进行测试

    trigger_config(0, 0x00000000, SIZE_1KB);

    volatile uint8_t *addr = (uint8_t *)1023;

    printf("try to write\r\n");
    *addr = 0xEF;

    printf("try to read\r\n");
    printf("addr: %x\n", *addr);

结果复合预期,1023禁止访问,1024允许访问:

try to write
mcause 3,  mepc 0x800060c0, mtval 0x000003ff

try to write
try to read
addr: ef

五、总结

最终,我们利用RISCV内核的Trigger Module实现了对内存区域的读写访问限制。
但需要注意的是,这种方式会占用触发器,对于调试会有影响,只建议不接入调试器且需要测试程序的情况下使用。
同时,Trigger Module还有很多其他功能,比如实现指令的计数,还有type4和type5对中断和异常进行触发的功能。

1
0
发表回复 0

Your email address will not be published. Required fields are marked *