Kernel中SBI代码分析
一、ecall指令详解
在RISC-V指令集架构中,ecall指令是用于请求特权级别切换的指令。当一个应用程序(位于用户态)需要访问操作系统或者进行其他特权级别的操作时,它会使用ecall指令触发一个系统调用,请求切换到更高特权级别(例如,从用户态切换到内核态)。类似于x86架构的syscall指令。
在处理器的特权级别切换时,ecall指令会触发一个异常,引发处理器从用户态切换到更高特权级别(比如机器态或超级用户态),并且控制转移到预定义的异常处理程序,通常是操作系统内核中的一个处理程序。在处理器处理完特权级别的操作后,通过特定的机制返回到用户态继续执行。
ecall指令用于向执行环境发出请求,在不同的特权等级中执行ecall指令有不同的效果:
- 在User模式下执行
ecall指令时,会引发environment-call-from-U-mode异常,进而切换到更高特权级别(如Supervisor-mode) - 在S模式下执行
ecall指令时,会引发environment-call-from-S-mode异常,进而切换到更高特权级别(如Machine-mode) - 在M模式下执行
ecall指令时,会引发environment-call-from-M-mode异常,这意味着在机器态中发出了请求,通常需要由硬件或者相关的特权级别的软件进行处理。
二、内核中SBI源码分析
ecall指令在Linux内核中用于SBI调用。sbi_ecall指令接受8个参数,分别是:
ext: SBI extension ID (EID)fid: SBI function ID (FID)arg0-arg5: SBI调用参数
Linux内核中的SBI调用实现
// linux/arch/riscv/kernel/sbi.cstructsbiretsbi_ecall(intext,intfid,unsignedlongarg0,unsignedlongarg1,unsignedlongarg2,unsignedlongarg3,unsignedlongarg4,unsignedlongarg5){structsbiretret;registeruintptr_ta0asm("a0")=(uintptr_t)(arg0);registeruintptr_ta1asm("a1")=(uintptr_t)(arg1);registeruintptr_ta2asm("a2")=(uintptr_t)(arg2);registeruintptr_ta3asm("a3")=(uintptr_t)(arg3);registeruintptr_ta4asm("a4")=(uintptr_t)(arg4);registeruintptr_ta5asm("a5")=(uintptr_t)(arg5);registeruintptr_ta6asm("a6")=(uintptr_t)(fid);registeruintptr_ta7asm("a7")=(uintptr_t)(ext);asmvolatile("ecall":"+r"(a0),"+r"(a1):"r"(a2),"r"(a3),"r"(a4),"r"(a5),"r"(a6),"r"(a7):"memory");ret.error=a0;ret.value=a1;returnret;}代码分析
对上述代码做简单分析:
- 使用ecall指令时,将异常类型写在a7寄存器,参数写在a0-a5寄存器,后面会根据异常类型的不同调用不同的异常处理函数
- register关键字表明后面的变量直接存储在寄存器中
- asm (“ax”)表明将后面的变量与ax寄存器进行绑定
- asm volatile表明嵌入汇编代码进入C代码中,并且将a0和a1寄存器既作为输入寄存器又作为输出寄存器传给ecall指令,而a2-a6寄存器作为输入寄存器传递给ecall
- ecall函数返回两个值a0和a1,sbi_ecall函数将这两个值作为错误和返回值传递给调用它的函数
三、SBI调用示例
比如实现一个putchar函数用于打印一个字符到系统控制台,就通过如下sbi_ecall调用来实现:
// linux/arch/riscv/kernel/sbi.cvoidsbi_console_putchar(intch){sbi_ecall(SBI_EXT_0_1_CONSOLE_PUTCHAR,0,ch,0,0,0,0,0);}SBI扩展ID定义
// linux/arch/riscv/include/asm/sbi.henumsbi_ext_id{#ifdefCONFIG_RISCV_SBI_V01SBI_EXT_0_1_SET_TIMER=0x0,SBI_EXT_0_1_CONSOLE_PUTCHAR=0x1,SBI_EXT_0_1_CONSOLE_GETCHAR=0x2,SBI_EXT_0_1_CLEAR_IPI=0x3,SBI_EXT_0_1_SEND_IPI=0x4,SBI_EXT_0_1_REMOTE_FENCE_I=0x5,SBI_EXT_0_1_REMOTE_SFENCE_VMA=0x6,SBI_EXT_0_1_REMOTE_SFENCE_VMA_ASID=0x7,SBI_EXT_0_1_SHUTDOWN=0x8,#endifSBI_EXT_BASE=0x10,SBI_EXT_TIME=0x54494D45,SBI_EXT_IPI=0x735049,SBI_EXT_RFENCE=0x52464543,SBI_EXT_HSM=0x48534D,};四、Linux与OpenSBI的交互
当以sbi_console_putchar为例:
交互流程分析
上层调用中相当于使用C语言的printf函数(①),自然而然我们陷入了内核态,然后 Linux Kernel 去调用 OpenSBI 提供的sbi_ecall()函数(②),并且在调用过程中将eid、fid以及之前提到的5个参数传递给 OpenSBI(③),之后由 OpenSBI 去真正的操作硬件。
最后操作完成之后,一级一级地向上返回执行结果(④ ⑤),完成整个向 console 进行输出的过程。
完整调用链
用户空间应用 │ ▼ (① printf) 内核态 printf 实现 │ ▼ (② sbi_console_putchar) sbi_ecall(SBI_EXT_0_1_CONSOLE_PUTCHAR, 0, ch, ...) │ ▼ (③ ecall 指令触发异常) OpenSBI 异常处理程序 │ ▼ 硬件操作:输出字符到控制台 │ ▼ (④ 返回结果) sbi_ecall 返回 │ ▼ (⑤ 返回用户空间) printf 返回交互层次结构
| 层次 | 组件 | 职责 |
|---|---|---|
| 用户态 | C库 printf | 格式化输出字符串 |
| 内核态 | Linux Kernel | 系统调用处理、缓冲区管理 |
| SBI层 | OpenSBI | 特权级切换、硬件抽象 |
| 硬件层 | UART/Console | 实际字符输出 |
这种分层设计体现了RISC-V架构的层次化特权模型,通过SBI层实现了操作系统与硬件之间的隔离,提高了系统的安全性和可移植性。