当前位置: 首页 > 科技观察

RISC-V架构系列一:指令集与特权模式

时间:2023-03-22 15:07:30 科技观察

RISC-V项目始于2010年,RISC-V基金会批准RISC-VBaseISA、特权架构、处理器等规范至今已有10年痕迹。对Linux的基本RISC-V支持也已完成。本文试图通俗易懂地介绍RISC-V对Linux的基本支持,包括指令集和异常处理。内存管理、迁移到RISC-V、UEFI、KVM等支持,欢迎继续关注本公众号。ISA眼见为实,下面是RISC-V的汇编语言。从作者的代码反汇编出来,功能是通过RISC-V(这里指的是OpenSBI,见下)提供的标准接口,将传入的字符c输出到终端。只有名字对了,才可以说RISC-V指令集规范要成为一个生态,大家需要向目标看齐。RISC-V规范(Specifications,参考链接1)就起到了这样的作用。当前规范分为两部分。第1卷是非特权指令,第2卷是特权指令。在第一卷中,RISC-V定义了两个基本的整数运算RV32I和RV64I,并有如下扩展。现在的问题是,这么多规格,如果大家使用的指令集不一致,那岂不是无法互通?不用担心,RISC-V还定义了以下指令集组合。为了提高指令密度和节省存储空间,RISC-V还有上述的C扩展(压缩指令)。例如RV32GC表示使用压缩指令的RV32G指令集,RV64GC表示使用压缩指令的RV64G指令集。根据AndrewWaterman的测试,在Spec2006(测试cpu性能的商用测试套件)中,RV32GC和RV64GC分别比RV32G和RV64G节省了30%+的空间,性能变化不大,见参考文献2。除了非特权指令外,RISC-V规范还包括特权指令。PrivilegedSpec中的MachineISA和SupervisorISA已经发布了1.11版本。VirtualizationISA目前是0.6,仍在讨论中。ISABrief理解指令集有助于我们理解架构。RISC-V是一种RISC架构。所有操作都在寄存器之间执行,数据通过单独的加载和存储指令从内存读取或写回内存。在整体指令集架构方面,鲍云刚先生带领的团队已经做了很好的中文翻译(参考链接3)。32”是将sp寄存器的值减32,保存在sp寄存器中。这条指令是在准备这个函数本身的栈空间。“sdra,24(sp)”是保存返回地址(ra)在栈上,24(sp)表示相对于+24的位置,这是RISC-V二进制调用规范定义的伪汇编,平时看代码的时候,除了架构中定义的汇编指令,你也会遇到伪汇编,伪汇编是帮助我们提高手写汇编效率的东西,比如寄存器的赋值,下面的li伪指令会被翻译成两条指令,lui和addiw,再举个例子,csrw是用来写csr寄存器的,csr的全称是ControlandStatusRegister,主要是权限管理相关的寄存器。前任RISC-V的eption,这是操作系统的职责之一(另一个重要的职责是虚拟内存的管理,下篇文章会介绍)。为了便于理解,我们将其与ARM、X86进行对比。大约40年前,x86架构有一个保护模式,如上图。其中,Level0运行操作系统,Level3运行应用程序。为了支持虚拟化,x86引入了VMX操作(如下图),Guest操作系统和应用运行在非root模式下,Hypervisor运行在root模式下。在这样的设计下,更方便支持Type-1和Type-2虚拟机技术,原操作系统无需任何修改即可作为Guest操作系统运行。但是,早期的x86虚拟化也有缺点。比如不支持二级页表转换,需要使用shadow页表,效率很低,直到引入EPT解决了这个问题。相比之下,ARM体系结构采用了不同的方法。因为ARM架构已经有了下图的Normal和Secureworld设计(这里指的是Normalworld的操作系统,比如Linux,不需要修改就可以运行在Secureworld)。没有类似x86的增加VMXroot和non-root操作的形式。而是如下图添加了一个新的异常级别EL2(下图中的Hypervisor)。很容易理解,EL2比EL1多了一层。问题是EL2不是EL1的拷贝,也就是说Linux内核不能直接运行在EL2上。对于Xen这种典型的Type-1虚拟化机制,没有问题,Xenhypervisor可以愉快的运行在EL2上。但是对于KVM来说,作为Linux内核的一个模块,KVM比较尴尬:KVM需要EL2的一些权限,而Linux却只能运行在EL1上。所以原来完整的x86上的KVM被拆分成了high-visor和low-visor(需要EL2特权能力的部分)。通常情况下,KVM的high-visor是愉快地运行在带有Linux内核的EL1上。当需要虚拟化管理的特权操作时,KVM从high-visor降到low-visor处理。ARM的虚拟化技术比x86晚了很多年。一个优点是可以完成x86多次迭代得到的状态。比如为了避免上面提到的shadowpagetable引入的EPT,在扩展ARM虚拟化的时候,x86是native的。支持的。同时ARM的虚拟化扩展在32位和64位架构下是完全一样的。早期的虚拟化工作,无论是xen还是KVM工作,都是在32位ARMv7a架构的Cortex-A15和Cortex-A7上进行的。完全的。这样,ARM64推出后,虚拟化部分就不需要重做了。至于ARM虚拟化上更多的异常处理带来的性能问题,从ARMv8.1开始,有了VHE模式,支持下沉EL1到EL2运行,这样KVMARM就没有前面提到的开销了。从上面的历史可以看出,软硬件的协调,灵活可扩展的设计是非常重要的。这一点也体现在RISC-V的设计上。在没有虚拟化功能的情况下,RISC-V最多支持三个特权级别。一般来说,要支持Linux这样的RichOS,就必须同时支持这三种模式。每一层都有不同的权限。Bootloader/BIOS/UEFI运行系统最高层机器模式,Linux内核运行在管理员模式,应用程序运行在用户模式。默认情况下,所有异常都在机器模式下处理。当有Linux内核时,这显然降低了效率:所有Linux内核可以处理的异常,例如应用程序页面错误异常,都需要先落入机器模式,然后再转发给内核。为了让软件系统更灵活地管理异常,RISC-V引入了委托机制,可以选择将一些异常和中断直接从硬件上交给管理模式的内核。现在的问题是,RISC-V的虚拟化是如何设计的?显然,虚拟化的特权级别需要支持Linux内核的RichOS。所以RISC-V并没有像早期的ARM虚拟化那样,直接在supervisor模式和machine模式之间加入虚拟化例外,而是定义了一个独立的虚拟化模式,再与user模式和supervisor模式结合起来,所以就有了下面的形式。(表格来自TheRISC-VInstructionSetManual,VolumeII:PrivilegedArchitecture,DocumentVersion1.12-draftTable5.1)这个有点抽象,用作者之一AnupPatel画的图来表示RISC-VkVM(图片已获得作者授权,见参考链接4)为原图。备注:RISC-V虚拟化规范目前处于0.6草案状态,未来可能会有一些小改动。SBI了解RISC-V的特权模式。不同层次的软件调用遵循什么样的规范?在RISC-V的设计中,下层(硬件/软件)对上层是透明的,规范定义了二进制接口。没有具体的实现Require。例如Linux内核处于supervisor模式,通过SBI(SupervisorBinaryInterface)访问以下权限级别。SBI接入的软件称为SEE(SupervisorExecutionEnvironment)。SEE可以是引导加载程序、BIOS或管理程序。与SEE类似的还有AEE,一种支持应用程序的运行环境。(图片来自TheRISC-VInstructionSetManual,VolumeII:PrivilegedArchitecture,DocumentVersion1.12-draftFigure1.1)SBI规范见参考链接5.规范定义了SBI的能力,比如获取版本SBI规范,发送或接收A字符,远程栅栏,设置定时器,发送IPI中断,管理RISC-V处理器(RISC-V中称为hart)等,以及SBI的二进制调用规范。截至本文,SBI是0.3草案。该版本主要增加系统复位的SBI接口。由于SBI是一种规范,因此有多种实现方式。OpenSBI就是其中之一。本实现支持generic(用于支持qemu的RISC-V虚拟机)、sifive、k210等芯片。这个有点抽象,举个简单的例子吧。如果你想写一个简单的代码从supervisor模式调用SBI接口打印字符,你应该怎么做?首先,假设我们有一个c语言的运行环境,那么我们需要根据SBI定义的二进制调用规范,使用寄存器a7传递指定的扩展ID。(图片来自RISC-VSupervisorBinaryInterfaceSpecificationVersion0.3-rc0p6)从下图可以看出,扩展ID为1,同时我们看到函数原型是在字符ch中传递的通过第一个参数。(图片来自RISC-VSupervisorBinaryInterfaceSpecificationVersion0.3-rc0p6)RISC-V用哪个寄存器来保存第一个参数?根据RISC-VELFpsABI规范(参考链接6)的整数寄存器调用约定,我们可以看到寄存器a0用于传递第一个参数。发送字符的相应代码如下所示。写完SBI的调用接口,还不是万事大吉。如果我们想让bootloader直接加载我们的代码,还需要自己准备C语言运行环境。添加以下编译行。该字符串将打印在cpu_enter中。我们选择使用OpenSBI的fw_jump从固定的0x80200000加载我们的binary,启动效果如下。最后一行“HelloXUXiake”是上面的代码打印出来的。希望我们能像徐霞客一样,通过写代码的方式去探索RISC-V的各种特性。参考链接[1]RISC-V规范:https://riscv.org/technical/specifications/[2]DesignoftheRISC-VInstructionSetArchitecturehttps://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-1.pdf[3]RISC-V架构简述:http://riscvbook.com/chinese/[4]RISC-V虚拟化扩展:https://static.sched.com/hosted_files/osseu19/4e/Xvisor_Embedded_Hypervisor_for_RISCV_v5.pdf[5]SBI规范:https://github.com/riscv/riscv-sbi-doc[6]RISC-V整数寄存器约定https://github.com/riscv/riscv-elf-psabi-doc/blob/master/riscv-elf.md#integer-register-convention-[7]RISC-V软件状态https://github.com/riscv/riscv-software-list本文转载自微信公众号“Linux代码阅读领域”,可通过以下二维码关注。转载本文请联系Linux代码阅读领域公众号。

最新推荐
猜你喜欢