diff --git a/articles/20220623-riscv-syscall-part2-procedure.md b/articles/20220623-riscv-syscall-part2-procedure.md index 5b7db7249667909cd64f16cb25b526ce63270a80..67430c66fd5eb7243375d8588e046fa1242edefc 100644 --- a/articles/20220623-riscv-syscall-part2-procedure.md +++ b/articles/20220623-riscv-syscall-part2-procedure.md @@ -4,11 +4,9 @@ > Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
> Sponsor: PLCT Lab, ISCAS - # RISC-V Syscall 系列 2:Syscall 过程分析 - -## 概述 +## 前言 本文主要对 Linux 在 RISC-V 架构下的 Syscall 机制进行分析,探究计算机是如何一步一步从应用程序开始,到执行 Syscall,最后返回应用程序的全过程。 @@ -23,7 +21,6 @@ 首先,Syscall 是如何开始的呢?当应用程序需要使用操作系统提供的一系列功能时,一般会通过操作系统提供的 C 标准库来进行 Syscall 的调用。而实际上,C 标准库的内部则使用了 `ecall` 指令来触发了整个 Syscall 流程。 - ### ecall 先介绍一下 ecall 指令。ecall 指令以前叫做 scall,用于执行环境的变更,它会根据当前所处模式触发不同的执行环境切换异常: @@ -34,7 +31,9 @@ Syscall 场景下是在 U-mode(用户模式)下执行 ecall 指令,主要会触发如下变更: * 处理器特权级别由 User-mode(用户模式)提升为 Supervisor-mode(内核模式) * 当前指令地址保存到 `sepc` 特权寄存器 -* 设置 `scause` 特权寄存器 +* 修改 `scause` 特权寄存器来标记产生异常的类型 +* 修改 `stval` 特权寄存器,某些异常需要将异常相关信息写入 `stval` +* 修改 `sstatus` 特权寄存器,将异常发生前的 SIE(当前全局中断使能标记位)保存到 SPIE(异常发生前的中断使能标记位),将异常发生前的特权级别保存到 SPP 中,并将 SIE 设置为 0。这意味着在硬件上,RISC-V 是不支持嵌套中断的。若要实现嵌套中断,则只能通过软件的方式来实现。 * 跳转到 `stvec` 特权寄存器指向的指令地址 简单来说,ecall 指令将权限提升到内核模式并将程序跳转到指定的地址。操作系统内核和应用程序其实都是相同格式的文件,最关键的区别就是程序执行的特权级别不同。所以 Syscall 的本质其实就是提升特权权限到内核模式,并跳转到操作系统指定的用于处理 Syscall 的代码地址。 @@ -48,9 +47,8 @@ ecall 指令规范中没有其他的参数,Syscall 的调用参数和返回值 * 返回值 * `a0` 寄存器存放 Syscall 的返回值 +### Syscall 入口 -### Syscall 入口 - ecall 跳转的地址是哪儿呢?根据 ecall 指令描述,stvec 寄存器存放的就是跳转的目标地址。下面通过代码看看 stvec 寄存器设置的值,以及设置的时机: ```asm // arch/riscv/kernel/head.S @@ -59,7 +57,6 @@ setup_trap_vector: la a0, handle_exception csrw CSR_TVEC, a0 // 将异常处理函数地址设置到 stvec 寄存器 - // arch/riscv/include/asm/csr.h #define CSR_STVEC 0x105 #define CSR_TVEC CSR_STVEC @@ -67,7 +64,6 @@ setup_trap_vector: 从上述代码可以看出,stvec 寄存器被设置成了 `handle_exception` 的地址,故 ecall 指令执行后会跳转到 `handle_exception`。而且 `arch/riscv/kernel/head.S` 是操作系统初始化时运行的代码,所以操作系统在启动时就配置了好 ecall 的跳转地址。 - ## handle_exception `handle_exception` 不止是作为 Syscall 调用进入内核的入口,也是整个 trap(transfer of control to a trap handler) 机制的入口。从下面简单的描述中可以看出 Syscall 其实是 trap 机制的一个应用场景。 @@ -141,15 +137,7 @@ _save_context: bge s4, zero, 1f /* Handle interrupts */ ... -1: - ... - /* Handle syscalls */ - li t0, EXC_SYSCALL - beq s4, t0, handle_syscall - -// arch/riscv/include/asm/csr.h -#define EXC_SYSCALL 8 ``` 因为此时 s4 存储的是 scause 的值,表示了 trap 的原因。scause 寄存器最高位含义如下: @@ -158,9 +146,30 @@ _save_context: 所以 s4>=0 表示本次 trap 是由某个 exception 触发,反之是由某个 interrupt 触发。所以这里会继续跳转到 `1f`。 -接着判断 s4 如果和 EXC_SYSCALL 相等,则跳转到 `handle_syscall`。实际上这里是在根据 exception code 判断 trap 的原因是否是 Syscall。当 scause==8 时,就表示由 Syscall 触发的 trap(具体 scause 值的含义可以参考文章末尾 scause 寄存器介绍部分),故这里会跳转到 `handle_syscall`。 +```asm +// arch/riscv/kernel/entry.S +1: + andi t0, s1, SR_PIE + beqz t0, 1f + ... + csrs CSR_STATUS, SR_IE +``` +通过上文中在 ecall 部分的说明,当前中断使能的状态是关闭的。而实际上系统调用过程耗时不确定,一般来说这个过程需要打开中断。所以如果系统调用前中断(SPIE)是开启的,则将当前中断使能(SIE)打开。 +```asm +// arch/riscv/kernel/entry.S +1: + la ra, ret_from_exception + /* Handle syscalls */ + li t0, EXC_SYSCALL + beq s4, t0, handle_syscall + +// arch/riscv/include/asm/csr.h +#define EXC_SYSCALL 8 +``` + +接着判断 s4 如果和 EXC_SYSCALL 相等,则跳转到 `handle_syscall`。实际上这里是在根据 exception code 判断 trap 的原因是否是 Syscall。当 scause==8 时,就表示由 Syscall 触发的 trap(具体 scause 值的含义可以参考文章末尾 scause 寄存器介绍部分),故这里会跳转到 `handle_syscall`。 ## handle_syscall @@ -233,7 +242,6 @@ RISCV_LGPTR 宏的定义如下: /* The array of function pointers for syscalls. */ extern void * const sys_call_table[]; - // arch/riscv/include/asm/asm.h #if __SIZEOF_POINTER__ == 8 #define RISCV_LGPTR 3 @@ -329,6 +337,9 @@ ret_from_syscall: // 将系统调用的返回值 a0 更新到用户态线程的上下文中 REG_S a0, PT_A0(sp) ... + // 关闭中断 + csrc CSR_STATUS, SR_IE + ... // 释放内核栈内存 addi s0, sp, PT_SIZE_ON_STACK REG_S s0, TASK_TI_KERNEL_SP(tp) @@ -349,14 +360,16 @@ ret_from_syscall: 从以上代码可以看出,返回用户态程序过程中主要做以下几件事: 1. 将系统调用的返回值 a0 更新到用户态线程的上下文中的 a0 -2. 释放内核栈内存 -3. 恢复用户态线程栈上下文信息,包括通用寄存器以及 sstatus 和 sepc 寄存器。 -4. 执行 `sret` 指令返回到用户态 +2. 关闭中断 +3. 释放内核栈内存 +4. 恢复用户态线程栈上下文信息,包括通用寄存器以及 sstatus 和 sepc 寄存器。 +5. 执行 `sret` 指令返回到用户态 `sret` 指令用来从 trap 机制中返回。sret 指令会执行如下操作: -* 将当前处理器特权级别设置为 sstatus.SPP; `sstatus.SPP = U` -* `sstatus.SIE = sstatus.SPIE; sstatus.SPIE = 1` -* `pc = sepc` +* 恢复到异常发生前的程序流执行:`pc = sepc` +* 更新 sstatus,将异常发生前的状态恢复 + * 将当前处理器特权级别设置为 SPP,即回到用户模式 + * 中断状态恢复到系统调用之前:`sstatus.SIE = sstatus.SPIE` 也就是说 sret 指令将处理器从内核模式切换到用户模式,并恢复中断的状态,然后跳转到进入 Syscall 时用户线程的下一条指令地址。至此,整个 Syscall 的过程就完成了。 @@ -364,7 +377,6 @@ ret_from_syscall: ![syscall_procedure](images/riscv_syscall/syscall_procedure.excalidraw.png) - 本文详细描述了从应用程序触发 Syscall 开始,到 trap 机制执行,再到 Syscall 实际处理函数,最后返回到应用程序的全过程。整个过程没有复杂的数据结构和算法,关键是理解这个流程机制,其中主要涉及的关键有以下几点: * ecall:进入比当前级别更高的特权级,针对应用程序,就是进入内核模式 * trap:用户态和内核态切换时的处理逻辑 @@ -375,55 +387,58 @@ ret_from_syscall: ## Syscall 相关特权寄存器 -**stvec** (Supervisor Trap Vector Base Address Register) +**stvec** (Supervisor Trap Vector Base Address Register) 用户保存发送异常时处理器需要跳转到的地址 -![](./images/riscv_syscall/stvec.png) +![stvec.png](images/riscv_syscall/stvec.png) -![](./images/riscv_syscall/stvec_mode.png) +![stvec_mode.png](images/riscv_syscall/stvec_mode.png) 根据上图中 RISC-V 规范中的描述,stvec 寄存器分为两部分,低 2 位称为 MODE,其他位称为 BASE。BASE 用于 trap 入口函数的基地址,必须保证四字节对齐。MODE 用于控制入口函数的地址配置方式。 * MODE=0 时,表示使用 Direct 方式,exception 发生后 PC 都跳转到 BASE 指定的地址处。 * MODE=1 时,表示使用 Vectored 方式,exception 的处理方式同 Direct,但 interrupt 的入口地址以数组方式排列。 - -**sepc** (Supervisor Exception Program Counter) +**sepc** (Supervisor Exception Program Counter) 当发生 trap 时,处理器会将发生 trap 所对应的指令的地址(pc)保存在 sepc 中 -**scause** (Supervisor Cause Register) +**scause** (Supervisor Cause Register) 当 trap 发生时,处理器会设置该寄存器表示 trap 发生的原因 -![](./images/riscv_syscall/scause.png) +![scause.png](images/riscv_syscall/scause.png) 根据 RISC-V 规范,scause 寄存器由高 1 位的 Interrupt 和 其他位的 Exception Code 组成。 -![](./images/riscv_syscall/scause_code.png) +![scause_code.png](images/riscv_syscall/scause_code.png) Syscall 触发时设置的内容如上图中红框所示,Interrupt=0 表示是异常,Exception Code=8,表示是从用户态执行的 ecall。 - -**sstatus** (Supervisor Status Register) +**sstatus** (Supervisor Status Register) 用于跟踪和控制处理器当前操作状态(比如包括关闭和打开全局中断) -![](./images/riscv_syscall/sstatus.png) +![sstatus.png](images/riscv_syscall/sstatus.png) 根据 RISC-V 规范,UIE、SIE 分别用于打开(1)或者关闭(0)用户/内核模式下的全局中断。UPIE、SPIE 用于当 trap 发生时保存 trap 发生之前的 UIE、SIE 值。SPP 用于当 trap 发生时用于保存 trap 发生之前的权限级别值。 - ## 参考资料 -- [Trap 和 Exception](https://gitee.com/unicornx/riscv-operating-system-mooc/raw/main/slides/ch10-trap-exception.pdf) -- [Misunderstanding RISC-V ecalls and syscalls](https://jborza.com/emulation/2021/04/22/ecalls-and-syscalls.html) -- [系统调用](https://gitee.com/unicornx/riscv-operating-system-mooc/raw/main/slides/ch16-syscall.pdf) -- [riscv-privileged](https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf) -- [Adding a New System Call](https://www.kernel.org/doc/html/v5.17/process/adding-syscalls.html) -- [系统调用 SYSCALL_DEFINE 详解](https://blog.csdn.net/rikeyone/article/details/91047118) +- [Trap 和 Exception][004] +- [Misunderstanding RISC-V ecalls and syscalls][007] +- [系统调用][005] +- [riscv-privileged][006] +- [Adding a New System Call][008] +- [系统调用 SYSCALL_DEFINE 详解][003] - [Linux Kernel 代码艺术——系统调用宏定义][2] - [1]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-0029 -[2]: https://mp.weixin.qq.com/s/gbZ4trQOvR-29elt8VDWxA? \ No newline at end of file +[2]: https://mp.weixin.qq.com/s/gbZ4trQOvR-29elt8VDWxA? + +[003]: https://blog.csdn.net/rikeyone/article/details/91047118 +[004]: https://gitee.com/unicornx/riscv-operating-system-mooc/raw/main/slides/ch10-trap-exception.pdf +[005]: https://gitee.com/unicornx/riscv-operating-system-mooc/raw/main/slides/ch16-syscall.pdf +[006]: https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf +[007]: https://jborza.com/emulation/2021/04/22/ecalls-and-syscalls.html +[008]: https://www.kernel.org/doc/html/v5.17/process/adding-syscalls.html