RISCV触发模块的应用——实现内存区域读写限制
一、前言
在计算机系统中,NULL指针(地址 0x00000000)往往被视为一个特殊的禁区。许多操作系统,软件和处理器都对其采用一些保护措施。
例如,软件返回空指针通常表示资源不足,而访问 NULL 指针并进行读取或修改通常会导致异常,比如常见的Segment Fault和Hard 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对中断和异常进行触发的功能。