RISC-V特权模式与寄存器
1.特权模式
RISC-V架构定义了4种特权模式,目前实现了三种,剩下一种保留,便于后续扩展。实现的三种特权模式分别为User/Application(用户模式,U Mode)、Supervisor(监管者模式,S Mode)和Machine(机器模式,M Mode),特权模式级别由低到高。三种特权模式对应不同的操作权限,区别主要体现在对寄存器的访问、特权指令的使用、对内存空间的访问。处理器复位后在机器模式下执行程序。
在特权级架构实现中,机器模式为必选模式,另外两种为可选模式。通过不同的模式组合可以实现不同用途的系统。
- 机器模式:通常为简单的嵌入式系统
- 机器模式 + 用户模式:该系统可以实现用户和机器模式的区分,从而实现资源的保护
- 机器模式 + 用户模式 + 监管者模式:该系统可以实现类Unix操作系统
1.1.用户模式
用户模式是最低的特权模式,可以运行应用程序,只能访问用户级的指令和寄存器,不能直接操作硬件,需要通过监管者模式或机器模式的服务。
1.2.监管者模式
监管者模式是一种保护模式,可以运行操作系统内核(如Linux内核),管理虚拟内存和外设,不能访问机器模式的控制寄存器,受到机器模式的监督和控制。
1.3.机器模式
机器模式是最高的特权模式,可以访问所有的硬件资源和状态寄存器,负责处理异常和中断(异常和中断没有被委托到监管者模式处理),以及启动和管理其他的特权模式。
1.4.调试模式
在具体的架构实现中可能还会包含一种调试模式(Debug Mode,D mode),以支持片外调试和生产测试。调试模式可以被视为一种额外的特权模式,其权限甚至比机器模式更高。调试模式保留了一些仅在D mode下可访问的控制状态寄存器(CSR)地址,并且可能还会保留平台上物理地址空间的某些部分。
1.5.模式切换
当异常或者中断发生时,处理器会自动切换到对应的特权模式。其中异常同步产生,如软件使用特权指令(ECALL、SRET、MRET)主动切换特权模式;中断异步产生,如外设中断产生后,处理器自动切换到对应的特权模式。所有的异常和中断默认在机器模式处理,软件可以将部分异常和中断委托到监管者模式处理。
异常或者中断发生时,处理器将当时的特权模式自动保存到SSTATUS.SPP/MSTATUS.MPP中。下面只介绍特权指令引起的特权模式切换。
- 用户模式调用特权指令
软件使用ECALL指令产生环境调用异常,默认切换到机器模式处理(MEDELEG.bit[8]=0);若将环境调用异常委托到监管者模式(MEDELEG.bit[8]=1),则会切换到监管者模式处理。
若环境调用异常在监管者模式处理,则使用SRET指令返回到用户模式(SSTATUS.SPP = U)。若环境调用异常在机器模式处理,则使用MRET指令返回到用户模式(MSTATUS.MPP = U)。 - 监管者模式调用特权指令
软件使用ECALL指令产生环境调用异常,默认切换到机器模式处理(MEDELEG.bit[9]=0);若将环境调用异常委托到监管者模式(MEDELEG.bit[9]=1),则会保持在监管者模式处理。
若环境调用异常在监管者模式处理,则使用SRET指令返回,特权模式仍为监管者模式(SSTATUS.SPP = S)。若环境调用异常在机器模式处理,则使用MRET指令返回到监管者模式(MSTATUS.MPP = S)。 - 机器模式调用特权指令
软件使用ECALL指令产生环境调用异常,固定在机器模式处理。处理完成后使用MRET指令仍然返回到机器模式。
1.6.异常和中断委托
在机器模式中,软件可通过MEDELEG和MIDELEG寄存器,将部分异常和中断委托到监管者模式处理。
两个寄存器的位域如下图所示。Interrupt为1表示中断,对应于MIDELEG寄存器,Interrupt为0表示异常,对应于MEDELEG寄存器。
- 异常只能在大于或等于当前特权级别的特权模式中处理,不会降低特权级别。比如在机器模式中产生异常,只能在机器模式中处理,无论该异常是否被委托到监管者模式。
- 机器模式中发生了中断,且该中断被委托到监管者模式,此时机器模式不响应该中断(相当于在机器模式屏蔽了该中断),等返回到监管者模式才会响应并处理。
- 机器模式软件中断、机器模式定时器中断、机器模式外部中断无法被委托到监管者模式,只能在机器模式中处理。
2.寄存器
2.1.通用寄存器
RISC-V架构定义了32个通用整数寄存器,其中X0也称为zero寄存器,硬件保持为常量0。PC保存了执行指令的地址,软件无法直接修改。上述的寄存器在用户模式都可见。RV32(RISC-V32位架构)架构寄存器宽度为32位,RV64(RISC-V64位架构)架构寄存器宽度为64位。
RISC-V定义了一套ABI名称来描述通用寄存器,具体如下。RISC-V的汇编指令使用通用寄存器的ABI名称。
2.2.C920寄存器
下面是玄铁C920 CPU的寄存器视图,不仅包括RISC-V架构定义的寄存器,还包括C920扩展的寄存器。用户模式只能访问矢量寄存器、通用寄存器、浮点寄存器、性能事件计数器、浮点控制寄存器和矢量扩展寄存器。监管者模式只能访问异常配置寄存器、异常处理寄存器、地址转换寄存器、信息寄存器、异常配置寄存器、异常处理寄存器和内存保护寄存器。机器模式只能访问性能事件计数器(和用户模式性能事件计数器寄存器不同)和性能事件选择器。
3.汇编代码分析
3.1.C语言代码
下面使用内联汇编,执行ecall特权指令,陷入内核中调用系统调用write和exit,执行打印消息和退出进程的功能。
对于系统调用,RISC-V Linux ABI 规定:
- 系统调用号: 放在a7寄存器中。
- 系统调用的参数: 依次放在a0、a1、a2、a3、a4、a5寄存器中。
- 系统调用的返回值: 放在a0寄存器中。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define SYS_write 64
#define SYS_exit 93
// 使用内联汇编实现 write 系统调用
void riscv_write(int fd, const void *buf, size_t count) {
__asm__ volatile (
// li为伪指令,编译器会展开为
"li a7, %[syscall_num]\n" // 将系统调用号加载到 a7
"mv a0, %[fd]\n" // 将文件描述符加载到 a0
"mv a1, %[buf]\n" // 将缓冲区地址加载到 a1
"mv a2, %[count]\n" // 将缓冲区长度加载到 a2
"ecall\n" // 触发系统调用
:
: [syscall_num] "i" (SYS_write), [fd] "r" (fd), \
[buf] "r" (buf), [count] "r" (count)
: "a7", "a0", "a1", "a2"
);
}
// 使用内联汇编实现 exit 系统调用
void riscv_exit(int status) {
__asm__ volatile (
"li a7, %[syscall_num]\n" // 将系统调用号加载到 a7
"mv a0, %[status]\n" // 将退出状态码加载到 a0
"ecall\n" // 触发系统调用
:
: [syscall_num] "i" (SYS_exit), [status] "r" (status)
: "a7", "a0"
);
}
int main(int argc, char *argv[])
{
char msg[20];
int a = 1, b = 2;
snprintf(msg, sizeof(msg), "a + b = %d\n", a + b);
// 调用 riscv_write 函数,向标准输出写入消息
riscv_write(1, msg, strlen(msg));
// 调用 riscv_exit 函数,退出程序
riscv_exit(66);
return 0; // 这行代码不会被执行,因为 riscv_exit 会终止程序
}
使用下面的命令编译可执行程序。
riscv64-unknown-linux-gnu-gcc -o riscv riscv.c
上述程序的执行结果如下,打印了a和b的和,且程序的返回值为66,说明是调用了riscv_exit退出,而不是return 0。
# ./riscv
a + b = 3
# echo $?
66
3.2.汇编代码
使用下面的命令,将上述的C语言代码反汇编为RISC-V汇编代码。-j .text选项只反汇编代码段,-M no-aliases禁止别名,即禁止显示伪指令。这样反汇编出来的代码是真正的RISC-V指令。
riscv64-unknown-linux-gnu-objdump -D -j .text -M no-aliases riscv > riscv-Dj.S
main、riscv_write、riscv_exit三个函数的反汇编的代码如下。前缀c.是RISC-V的压缩指令集(C扩展),指令长度为16位,属于实际的机器指令,不是伪指令。
# a0 a1 a2保存了函数三个参数
0000000000010546 <riscv_write>:
10546: 7179 c.addi16sp sp,-48 # sp = sp - 48(非零符号扩展的6位立即数)
10548: f406 c.sdsp ra,40(sp) # 返回地址压栈
1054a: f022 c.sdsp s0,32(sp) # 上个函数的栈帧指针压栈
1054c: 1800 c.addi4spn s0,sp,48 # 记录当前函数的栈帧指针
1054e: 87aa c.mv a5,a0
10550: feb43023 sd a1,-32(s0) # 参数a1压栈
10554: fcc43c23 sd a2,-40(s0) # 参数a2压栈
10558: fef42623 sw a5,-20(s0) # 参数a0压栈
1055c: fec42783 lw a5,-20(s0) # 获取a0参数(lw:有符号扩展字加载指令)
10560: 86be c.mv a3,a5 # 保存a0参数到a3
10562: fe043783 ld a5,-32(s0) # 保存a1参数到a5
10566: fd843703 ld a4,-40(s0) # 保存a2参数到a4
1056a: 04000893 addi a7,zero,64 # a7保存write的系统调用编号,li伪指令被翻译为addi
1056e: 8536 c.mv a0,a3 # 保存系统调用write的函数的参数
10570: 85be c.mv a1,a5
10572: 863a c.mv a2,a4
10574: 00000073 ecall # 执行特权指令,陷入内核
10578: 0001 c.addi zero,0
1057a: 70a2 c.ldsp ra,40(sp) # 恢复返回地址
1057c: 7402 c.ldsp s0,32(sp) # 恢复上个函数的栈帧指针
1057e: 6145 c.addi16sp sp,48 # 释放当前函数栈占用的空间
10580: 8082 c.jr ra # 跳转到ra保存的地址处
0000000000010582 <riscv_exit>:
10582: 1101 c.addi sp,-32
10584: ec06 c.sdsp ra,24(sp)
10586: e822 c.sdsp s0,16(sp)
10588: 1000 c.addi4spn s0,sp,32
1058a: 87aa c.mv a5,a0
1058c: fef42623 sw a5,-20(s0)
10590: fec42783 lw a5,-20(s0)
10594: 05d00893 addi a7,zero,93
10598: 853e c.mv a0,a5
1059a: 00000073 ecall
1059e: 0001 c.addi zero,0
105a0: 60e2 c.ldsp ra,24(sp)
105a2: 6442 c.ldsp s0,16(sp)
105a4: 6105 c.addi16sp sp,32
105a6: 8082 c.jr ra
00000000000105a8 <main>:
105a8: 7139 c.addi16sp sp,-64
105aa: fc06 c.sdsp ra,56(sp)
105ac: f822 c.sdsp s0,48(sp)
105ae: 0080 c.addi4spn s0,sp,64
105b0: 87aa c.mv a5,a0
105b2: fcb43023 sd a1,-64(s0)
105b6: fcf42623 sw a5,-52(s0)
105ba: 4785 c.li a5,1
105bc: fef42623 sw a5,-20(s0)
105c0: 4789 c.li a5,2
105c2: fef42423 sw a5,-24(s0)
105c6: fec42783 lw a5,-20(s0)
105ca: 873e c.mv a4,a5
105cc: fe842783 lw a5,-24(s0)
105d0: 9fb9 c.addw a5,a4
105d2: 2781 c.addiw a5,0
105d4: fd040713 addi a4,s0,-48
105d8: 86be c.mv a3,a5
105da: 67c1 c.lui a5,0x10
105dc: 67078613 addi a2,a5,1648 # 10670 <__libc_csu_fini+0x2>
105e0: 45d1 c.li a1,20
105e2: 853a c.mv a0,a4
105e4: eadff0ef jal ra,10490 <snprintf@plt>
105e8: fd040793 addi a5,s0,-48
105ec: 853e c.mv a0,a5
105ee: e93ff0ef jal ra,10480 <strlen@plt>
105f2: 872a c.mv a4,a0
105f4: fd040793 addi a5,s0,-48
105f8: 863a c.mv a2,a4
105fa: 85be c.mv a1,a5
105fc: 4505 c.li a0,1
105fe: f49ff0ef jal ra,10546 <riscv_write>
10602: 04200513 addi a0,zero,66
10606: f7dff0ef jal ra,10582 <riscv_exit>
1060a: 4781 c.li a5,0
1060c: 853e c.mv a0,a5
1060e: 70e2 c.ldsp ra,56(sp)
10610: 7442 c.ldsp s0,48(sp)
10612: 6121 c.addi16sp sp,64
10614: 8082 c.jr ra