加入星计划,您可以享受以下权益:

  • 创作内容快速变现
  • 行业影响力扩散
  • 作品版权保护
  • 300W+ 专业用户
  • 1.5W+ 优质创作者
  • 5000+ 长期合作伙伴
立即加入
  • 正文
    • ISA
    • 名正才能言顺,RISC-V 指令集规范
    •  
    • ISA 简述
    • 伪汇编
    • 异常处理
    • SBI
  • 相关推荐
  • 电子产业图谱
申请入驻 产业图谱

RISC-V架构系列之1:指令集和特权模式

2021/02/01
397
阅读需 18 分钟
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

作者按:在上个月的 os2atc 会议 上,笔者作为 Linux 阅码场高级顾问分享了 RISC-V 对 Linux 对支持情况。会议后对分享内容再次做了迭代,期待和大家一起交流,进步。

从 2010 年开始的 RISC-V 项目,已经有 10 年的时间,RISC-V 基金会先后批准了 RISC-V Base ISA, Privileged Architecture,Processor Trace 等规范。RISC-V 对 Linux 的基本支持也已经完成。本文尝试通俗易懂的介绍 RISC-V 对于 Linux 的基本支持,包括指令集和异常处理。内存管理,迁移到 RISC-V,UEFI,KVM 等支持,欢迎继续关注本公众号。

ISA

眼见为实,下面就是 RISC-V 的汇编语言了。从笔者代码中反汇编得来,功能是把传入的字符 c,通过 RISC-V 提供的标准接口(此处指 OpenSBI,见 下文 )输出到终端。

名正才能言顺,RISC-V 指令集规范

想做好一个生态,需要大家对齐目标,RISC-V 的规范( Specifications,参考链接 1)就起了这样的作用,目前的规范分成两部分,第 1 卷是非特权指令,第 2 卷是特权指令。在第一卷中,RISC-V 已经定义了 RV32I 和 RV64I 两个基础整数运算,并有如下扩展。

现在问题来了,这么多规范,大家如果用的指令集不一致,岂不是没法互操作了?别急,RISC-V 还定义了下面指令集组合。

为了提高指令密度,更节省存储空间,RISC-V 还有上述的 C 扩展(压缩指令),例如 RV32GC 表示使用压缩指令的 RV32G 指令集,RV64GC 表示使用压缩指令的 RV64G 指令集。根据 Andrew Waterman 的测试,在 Spec2006(一个测试 cpu 性能的商用测试套)中,RV32GC 和 RV64GC 分别比 RV32G 和 RV64G 节省 30%+的空间,而性能变化不大,见 参考资料 2 。

除了非特权指令,RISC-V 的规范还包括特权指令。Privileged Spec 里面 Machine ISA 和 Supervisor ISA 已经 release 了 1.11 版本。而虚拟化 Virtualization ISA 目前是 0.6,还在讨论中。

 

ISA 简述

了解指令集有助于我们了解这个架构。RISC-V 是一个 RISC 架构。所有的运算都在寄存器之间进行,通过单独的 load 和 store 指令,把数据从内存中读出或写回。整体的指令集架构方面,包云岗老师带领团队已经做了很好的中文翻译(参考链接 3) ,我这边就不再详细的展开讲,仅仅举两个例子

“Addi    sp,sp,-32”是把 sp 寄存器的值减 32 并保存到 sp 寄存器中,这条指令在准备本函数自己的栈空间。

“Sd      ra,24(sp)”是把本返回地址(ra)保存到栈上,24(sp)表示相对+24 的位置,这是 RISC-V 二进制调用规范定义的。

伪汇编

平时读代码的时候,除了架构中定义的汇编指令还会遇到伪汇编。伪汇编是一些帮助我们平时手写汇编提高效率的东西。比如说寄存器的赋值,下面的一条 li 伪指令会被翻译为 lui 和 addiw 两条指令。

再举个例子,csrw 用于写入 csr 寄存器。其中 csr 的全称是 Control and Status Register,主要是和特权管理相关的寄存器。

异常处理

了解了基本的汇编语言,我们就可以进一步的了解 RISC-V 的异常,这是操作系统的职责之一(另一个重要职责是虚拟内存的管理,在下一篇文章介绍)。

为了便于理解,我们与 ARM 和 X86 对比下。

大约 40 年前,x86 架构有了如上图的保护模式。其中 Level0 跑操作系统,Level3 跑应用。为了支持虚拟化,x86 引入了 VMX operation(如下图),Guest 操作系统和应用运行在 non-root 模式,Hypervisor 运行在 root 模式。在这样的设计下,支持 Type-1 和 Type-2 的虚拟机技术都比较方便,并且原有的操作系统不需要任何修改就可以作为 Guest 操作系统运行。不过早期的 x86 虚拟化也有缺点,例如不支持二级页表转换,需要用 shadow page table,这样效率很低,直到 EPT 的引入解决这一问题。

相比之下,ARM 架构采取了不同的方式。由于 ARM 架构下已经有了如下图的 Normal 和 Secure world 设计(这里指的是 Normal world 的操作系统,例如 Linux,可以不加修改的运行在 Secure world)。没有用类似 x86 添加 VMX root 和 non-root 的 operation 的形式。

而是如下图添加了新的一个异常级别 EL2(下图的 Hypervisor),很容易理解的是 EL2 比 EL1 有更多的级别。问题在于 EL2 并不是 EL1 的复制,也就是说 Linux kernel 没法直接运行在 EL2 上。对于 Xen 这种典型的 Type-1 虚拟化机制没问题,Xen hypervisor 可以很开心的运行在 EL2。但是对于 KVM,KVM 作为 Linux kernel 的一个模块,就比较尴尬:KVM 需要 EL2 的一些权限,但是 Linux 又只能运行在 EL1。于是原本在 x86 上完整的 KVM 被拆成了 high-visor 和 low-visor(需要 EL2 特权能力的部分)两部分。平时 KVM 的 high-visor 愉快和 Linux kernel 一起运行在 EL1,当需要虚拟化管理的特权操作时,KVM 从 high-visor 陷入到 low-visor 处理。

ARM 的虚拟化技术比 x86 的晚了很多年,有个好处是可以完成 x86 多次迭代得到的状态,例如前文提到的 x86 为了避免 shadow page table 引入的 EPT,在 ARM 虚拟化扩展时是原生支持的。同时,ARM 的虚拟化扩展在 32 位和 64 位架构下是完全一样的,早期的虚拟化工作,不论是 xen 还是 KVM 的工作都是在 32 位的 ARMv7a 架构的 Cortex-A15 和 Cortex-A7 上完成的。这样 ARM64 推出后,虚拟化这部分工作不需要重新做。至于 ARM 虚拟化上更多异常处理导致的性能问题,从 ARMv8.1 开始,有了 VHE 模式,支持把 EL1 下沉到 EL2 运行,这样 KVM ARM 就没有了前述的开销。

从上述历史可以看出,软硬件的协同,灵活可扩展的设计非常重要。RISC-V 的设计中也体现了这一点。在没有虚拟化特性情况下,RISC-V 最多支持三个特权级别。通常来说,为了支持 Linux 这样的 Rich OS,需要同时支持这三个模式。每一层有不同的权限。

Bootloader/BIOS/UEFI 运行系统的最高级别 machine mode,Linux kernel 运行在 supervisor mode,应用运行在 user mode。默认情况下,所有的异常都在 machine mode 处理。在有 Linux kernel 时,这样明显降低了效率:所有原本可以由 Linux kernel 处理的异常,例如应用的缺页异常,都需要先陷入到 machine mode 再转发给 kernel。为了允许软件系统更灵活的管理异常,RISC-V 引入了 delegation 机制,可以选择把一部分异常和中断由硬件直接交给 supervisor mode 的 kernel 处理。

 

现在问题来了,RISC-V 的虚拟化是如何设计的呢?很明显,虚拟化的特权级别需要支持 Linux kernel 这种 Rich OS。所以 RISC-V 没有像早期的 ARM 虚拟化一样把虚拟化异常直接直接加到 supervisor mode 和 machine mode 之间,而是定义了独立的 virtualization mode,这个 mode 再与 user 和 supervisor mode 组合,于是有了下面的表格。

(表格来自 The RISC-V Instruction Set Manual, Volume II: Privileged Architecture, Document Version 1.12-draft Table 5.1)

这么说有点抽象,用 RISC-V kVM 作者之一的 Anup Patel 画的图表示(图片已获得作者授权, 原图见参考链接 4)。

备注:RISC-V 虚拟化规范目前处于 0.6 草稿状态,未来可能还会有些小的变化。

SBI

了解了 RISC-V 的特权模式,不同层次的软件调用遵循什么样的规范呢?RISC-V 的设计中,下层(硬件 / 软件)对上层透明,规范会定义二进制接口,对具体如何实现没有要求。例如 Linux kernel 在 supervisor mode,对下面的特权级别,通过 SBI(Supervisor Binary Interface)访问,SBI 访问的软件称为 SEE(Supervisor Execution Environment),SEE 可以是 bootloader,BIOS,也可以 Hypervisor。和 SEE 类似的还有支持应用的运行环境 AEE。

(图片来自 The RISC-V Instruction Set Manual, Volume II: Privileged Architecture, Document Version 1.12-draft Figure 1.1)

SBI 的规范见参考链接 5,规范定义了 SBI 的能力,例如获得 SBI 规范的版本,发送或接收一个字符,remote fence,设置 timer,发送 IPI 中断,管理 RISC-V 处理器(RISC-V 中称为 hart)等,以及 SBI 的二进制调用规范。截止这篇文章,SBI 是 0.3 draft,这个版本主要是增加了用于系统复位的 SBI 接口。既然 SBI 是个规范,那就有各种实现,OpenSBI 就是其中一个实现,这个实现支持 generic(用于支持 qemu 的 RISC-V virt machine),sifive 和 k210 等芯片

这么说有点抽象,咱们举个简单的例子。如果想写一个简单的从 supervisor mode 调用 SBI 接口打印字符的代码,要怎么做呢?

首先,假设,我们以及有了 c 语言的运行环境,那我们需要根据 SBI 定义的二进制调用规范,使用寄存器 a7 传递指定的 extension ID。

(图片来自 RISC-V Supervisor Binary Interface Specification Version 0.3-rc0 p6)

从下图可以看到,extension ID 是 1。同时我们看到函数原型是通过第一个参数传入字符 ch。

(图片来自 RISC-V Supervisor Binary Interface Specification Version 0.3-rc0 p6)

RISC-V 使用哪个寄存器保存第一个参数呢?根据 RISC-V ELF psABI

specification 的整数寄存器调用约定( 参考链接 6 ),我们可以看到寄存器 a0 用于传递第一个参数。发送一个字符的对应的代码是这个样子

写了 SBI 调用接口,还没有万事大吉,如果希望 bootloader 直接加载我们的代码,我们还需要自己准备 c 语言运行环境。加上下面几行汇编即可。

cpu_enter 里面会打印字符串。我们选择使用 OpenSBI 的 fw_jump 从固定的 0x80200000 加载我们的二进制,启动效果如下。最后一行“Hello XU Xiake“是上面代码打印的。希望我们像徐霞客一样,通过编写代码,游览 RISC-V 的各种特性。

参考链接

[1] RISC-V 规范: https://riscv.org/technical/specifications/

[2] Design of the RISC-V Instruction Set Architecture https://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 Integer Register Convention 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操作系统学习平台,内容涉及Linux内核,Linux内存管理,Linux进程管理,Linux文件系统和IO,Linux性能调优,Linux设备驱动以及Linux虚拟化和云计算等各方各面.