Linux内核分析之基础知识-05

韩乔落

6.1 RISC-V 架构概述:寄存器、特权级与 CSR

本节全面介绍 RISC-V 64 位架构的编程模型,包括寄存器组织、特权级别、控制状态寄存器、调用约定以及关键 ISA 扩展。这些知识是理解后续内核启动和运行机制的基石。

6.1.1 寄存器组织

通用寄存器(GPR)

RISC-V 定义了 32 个 64 位通用寄存器 x0-x31,其中 x0 始终为零:

寄存器 ABI 名称 用途 说明
x0 zero 硬连线为零 读取返回 0,写入无效
x1 ra 返回地址 函数调用返回地址
x2 sp 栈指针 必须 16 字节对齐
x3 gp 全局指针 指向全局数据区
x4 tp 线程指针 指向 thread_info / TLS
x5-x7 t0-t2 临时寄存器 调用者保存
x8 s0/fp 帧指针 / 被调用者保存 既可做帧指针也可做通用保存寄存器
x9 s1 被调用者保存 跨函数调用保持
x10-x17 a0-a7 参数 / 返回值 a0-a7 传参,a0-a1 返回值
x18-x27 s2-s11 被调用者保存 跨函数调用保持
x28-x31 t3-t6 临时寄存器 调用者保存

PC(程序计数器):RISC-V 中 PC 不是通用寄存器,不能直接读写。PC 通过跳转指令(jal/jalr)隐式修改,通过 csrr 读取(mepc/sepc 可间接获取)。

浮点寄存器(FPU)

如果处理器实现了 F(单精度)和 D(双精度)扩展,则提供 32 个浮点寄存器 f0-f31(每个 64 位宽)。内核在切换浮点上下文时,需要通过 sstatus.FS 位域来跟踪浮点寄存器的状态(Off/Initial/Clean/Dirty),以实现惰性上下文切换。

向量寄存器(Vector)

如果处理器实现了 V(向量)扩展,则提供 32 个向量寄存器 v0-v31。RISC-V 向量扩展的独特之处在于采用可变长度向量设计:向量寄存器的总宽度由 VLEN(128 到 2048 位甚至更宽)决定,软件通过 vsetvli/vsetvl 指令动态配置每条向量指令处理的元素宽度和数量。这种设计使得同一份二进制代码可以在不同向量长度的处理器上高效运行。

控制状态寄存器(CSR)

CSR 是 RISC-V 架构中独立于通用寄存器的地址空间,每个特权级都有对应的 CSR 集合。访问 CSR 使用专用指令:

  • csrr rd, csr:读取 CSR 到通用寄存器
  • csrw csr, rs:写入通用寄存器到 CSR
  • csrrw rd, csr, rs:原子读-写
  • csrrs/csrrc:原子读-置位 / 读-清零

CSR 地址编码中,高两位 [11:10] 表示最低访问特权级(00=Unprivileged, 01=Supervisor, 10=Hypervisor, 11=Machine),bit [9:8] 表示读写权限(11=只读)。

6.1.2 特权级别

RISC-V 定义了以下特权级别:

特权级 编码 典型用途 说明
U-mode 00 用户应用程序 受限访问,无法执行特权指令
S-mode 01 操作系统内核 管理虚拟内存、中断、设备
HS-mode 10 Hypervisor 宿主机内核(需 H 扩展)
M-mode 11 固件 / OpenSBI 最高权限,完全硬件访问

引入 H 扩展后,S-mode 进一步细分为 HS-mode(Host Supervisor)和 VS-mode(Virtual Supervisor)。VS-mode 运行客户机内核,VU-mode 运行客户机用户态程序。

与其他架构对比

  • x86:Ring 0(内核)到 Ring 3(用户),特权级通过 CS 段选择子低 2 位编码
  • ARM64:EL0(用户)到 EL3(安全监视器),异常级别通过 PSTATE.EL 编码
  • RISC-V:特权级更为简洁,仅 3-4 个级别,且 M-mode 始终为最高权限

6.1.3 关键 S-mode CSR

sstatus —— 管理者状态寄存器

sstatus 是 mstatus 的子集,内核最常操作的位域包括:

1
2
3
4
5
6
7
8
9
sstatus 关键位域:
[63:34] - SD, XS 等状态位
[33:32] - VS[1:0] 向量扩展状态(Off/Initial/Clean/Dirty)
[23:22] - XS[1:0] 用户扩展状态
[14:13] - FS[1:0] 浮点状态
[18] - SUM S-mode 允许访问 U-mode 页面
[8] - SPP 进入异常前的特权级(0=U, 1=S)
[5] - SPIE 进入异常前 SIE 的值
[1] - SIE S-mode 中断使能
  • SUM 位:置位后允许 S-mode 访问标记为 U 位的页面。内核在执行 copy_to_user/copy_from_user 等操作时需要临时设置此位。
  • FS/VS 位:跟踪浮点/向量寄存器脏状态,用于惰性上下文切换。内核在调度时若发现状态为 Dirty 则需要保存 FPU/向量上下文。

sie / sip —— 中断使能与中断等待

1
2
3
4
5
6
7
8
9
sie 位域:
[9] SEI - 外部中断使能(PLIC/APLIC)
[5] STI - 定时器中断使能
[1] SSI - 软件 IPI 中断使能

sip 位域(对应位,只读或可写取决于实现):
[9] SEI - 外部中断等待
[5] STI - 定时器中断等待
[1] SSI - 软件 IPI 等待

stvec —— Trap 向量基址

1
2
3
stvec 格式:
[MXLEN-1:2] BASE Trap 入口地址(4 字节对齐)
[1:0] MODE 00=Direct, 01=Vectored
  • Direct 模式:所有 Trap 跳转到 BASE 地址,内核默认使用此模式
  • Vectored 模式:中断根据 cause 值跳转到 BASE + 4*cause,异常仍跳转到 BASE

sscratch —— 暂存寄存器

内核巧妙利用 sscratch 实现快速的用户态/内核态检测:

  • 在内核态运行时,sscratch 保存当前 task 的 thread_info 指针(或 tp 值)
  • 切换到用户态前,将用户态的 tp 写入 sscratch
  • Trap 入口通过交换 tp 和 sscratch 来判断来源:如果交换后 tp 指向内核地址,说明来自用户态

sepc —— 异常程序计数器

发生 Trap 时,CPU 自动将当前 PC(或触发异常的指令地址)写入 sepc。sret 指令从 sepc 恢复执行。注意:如果异常由 ecall 指令触发,sepc 指向 ecall 本身而非下一条指令,因此内核在处理系统调用返回前需要手动将 sepc 加 4。

scause —— 异常原因

1
2
3
scause 格式:
[63] Interrupt 1=中断, 0=异常
[62:0] Code 异常/中断编号

通过 bit 63 区分中断和异常,低 63 位给出具体原因编码。例如 code=8 且 Interrupt=0 表示 ecall from U-mode(即系统调用),code=5 且 Interrupt=1 表示定时器中断。

stval —— Trap 值

对于访存异常,stval 保存触发异常的虚拟地址。对于非法指令异常,stval 保存触发异常的指令编码。内核利用此信息进行缺页处理和错误诊断。

satp —— 页表基址

satp 是 RISC-V 虚拟内存的核心寄存器,将在 6.2 节详细讨论。其格式为 Mode(4 位)+ ASID(16 位)+ PPN(44 位),通过写入 satp 并执行 sfence.vma 指令来切换地址空间。

senvcfg —— 环境配置

较新的 RISC-V 特权规范引入 senvcfg,控制多种微架构行为:

  • CBZE/CBCFE/CBIE:压缩指令行为控制(Cache Block Zero/Flush/Invalidate)
  • FIOM:Fence of I/O implies Memory
  • STCE:Sstc 定时器事件使能
  • PBMTE:基于页的内存类型使能(Svpbmt)

6.1.4 调用约定

RISC-V 函数调用约定(遵循标准 ABI):

寄存器类别 ABI 名称 约定
参数/返回值 a0-a7 (x10-x17) a0-a7 传递参数,a0-a1 返回结果
返回地址 ra (x1) 函数调用由调用者保存
栈指针 sp (x2) 16 字节对齐,被调用者保持
帧指针 fp/s0 (x8) 可选,用于栈帧回溯
被调用者保存 s0-s11 被调用函数必须保存并恢复
临时寄存器 t0-t6 调用者保存,被调用函数可自由使用
全局指针 gp (x3) 指向 .sdata / .sbss 段附近
线程指针 tp (x4) 指向线程局部存储或 thread_info

栈对齐要求:RISC-V 要求栈指针 sp 在函数调用时必须 16 字节对齐(而非 ARM64 的 16 字节或 x86 的 16 字节),这是硬件强制要求的。未对齐的 sp 会导致未定义行为。

6.1.5 关键 ISA 扩展

RISC-V 的模块化设计通过扩展实现功能递增。以下是 Linux 7.0 内核相关的重要扩展:

基础扩展

扩展 名称 说明
I Base Integer 47 条基础整数指令,RV64I 支持 32 位和 64 位操作
M Multiply/Divide 整数乘除法和取余指令
A Atomic 原子内存操作:LR/SC 和 AMO(Atomic Memory Operation)
F Single-Precision Float 单精度浮点运算,引入 f0-f31
D Double-Precision Float 双精度浮点运算
C Compressed 16 位压缩指令编码,约 30% 代码体积缩减

高级扩展

扩展 名称 说明
V Vector 可变长度向量,128-2048+ 位,支持多种元素类型
B Bit Manipulation Zba(地址生成)、Zbb(基础位操作)、Zbc(carryless 乘法)、Zbs(单比特操作)
H Hypervisor 二类虚拟化支持,引入 VS/VU 模式
Zicbom/Zicboz Cache Management 缓存块维护(清空、无效化、零化)
Zkr Entropy 硬件随机数源(种子 CSR)
Zk* Cryptography Zkn(NIST 算法)、Zks(ShangMi 算法)等密码学加速

特权扩展

扩展 名称 说明
Svpbmt Page-Based Memory Types 页表项中编码内存类型(NC/IO)
Svnapot NAPOT Huge Pages 支持自然对齐的连续大页
Svinval Efficient TLB Invalidation 高效 TLB 无效化操作序列
Sstc Supervisor Timer stimecmp CSR,无需 SBI 调用设置定时器
Zicfilp/Zicfiss CFI 前向/后向控制流完整性(Landing Pad / Shadow Stack)

6.1.6 指令编码格式

RISC-V 基础指令采用 32 位固定长度编码,分为以下格式:

1
2
3
4
5
6
R-type: [funct7(7)] [rs2(5)] [rs1(5)] [funct3(3)] [rd(5)]  [opcode(7)]  — 寄存器-寄存器操作
I-type: [imm(12)] [rs1(5)] [funct3(3)] [rd(5)] [opcode(7)] — 立即数操作
S-type: [imm(7)] [rs2(5)] [rs1(5)] [funct3(3)] [imm(5)] [opcode(7)] — 存储
B-type: [imm(1)][imm(6)] [rs2(5)] [rs1(5)] [funct3(3)] [imm(4)][imm(1)] [opcode(7)] — 分支
U-type: [imm(20)] [rd(5)] [opcode(7)] — 上层立即数
J-type: [imm(1)][imm(10)][imm(1)][imm(8)] [rd(5)] [opcode(7)] — 跳转

C 扩展(压缩指令):常见操作(如 load/store、寄存器-寄存器操作、跳转)可用 16 位编码表示。C 扩展不影响程序员模型,汇编器自动选择最短编码。

与其他架构对比

  • x86:变长编码(1-15 字节),复杂的前缀和 ModRM 字节,向后兼容 40 年历史
  • ARM64:固定 32 位编码(无压缩),通过 T32(ARMv7)提供 16/32 位混合编码
  • RISC-V:32 位基础 + 16 位压缩,指令格式规整,rs1/rd 字段位置在多种格式中保持一致

6.2 RISC-V 虚拟内存:Sv39/Sv48/Sv57 与 SATP

本节深入分析 RISC-V 的虚拟内存系统,包括 SATP 寄存器格式、多级页表结构、虚拟地址空间布局、页表项编码以及与其他架构的对比。

6.2.1 SATP 寄存器格式

SATP(Supervisor Address Translation and Protection)是 RISC-V 虚拟内存的核心控制寄存器。其 64 位格式如下:

1
2
3
4
5
6
SATP 寄存器格式(RV64):
+--------+----------------+------------------------------------------+
| 63-60 | 59-44 | 43-0 |
| Mode | ASID | PPN |
| (4位) | (16位) | (44位) |
+--------+----------------+------------------------------------------+
  • Mode(4 位):选择地址翻译模式
  • ASID(16 位):地址空间标识符,用于避免上下文切换时的 TLB 冲刷
  • PPN(44 位):根页表的物理页帧号,指向物理内存中的顶级页表

地址翻译模式

Mode 值 名称 级数 虚拟地址位数 用户空间大小 页表遍历次数
8 Sv39 3 级 39 位 512 GB 3 次
9 Sv48 4 级 48 位 128 TB 4 次
10 Sv57 5 级 57 位 64 PB 5 次
0 Bare - - 物理地址直通 无翻译

启动时的自动检测

Linux 内核在启动阶段通过 setup_vm() 自动检测处理器支持的最高地址翻译模式。检测逻辑(位于 arch/riscv/mm/init.c)按优先级依次尝试:

1
尝试 Sv57 → 如果硬件不支持则尝试 Sv48 → 兜底使用 Sv39

具体来说,内核检查 marchid 或通过 SBI 查询硬件能力。一旦确定模式,将其写入 satp 并刷新 TLB。大多数当前硬件仅支持 Sv39,但新一代处理器(如 SiFive Performance 系列)已支持 Sv48。

6.2.2 虚拟地址空间布局

以 Linux 7.0 在 Sv48 模式下的典型布局为例(64 位虚拟地址空间):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0xFFFFFFFFFFFFFFFF ─────────────────┐

0xFFFFE00000000000 Fixmap │
0xFFFFDFFFFFC00000 PCI I/O │ 内核空间
0xFFFFC00000000000 VMEMMAP │ (高半部分)
0xFFFF800000000000 MODULES │
0xFFFF000000000000 KERNEL_LINK │
(= end - 2GB) │
vmalloc region │
linear mapping │

0xFFFF000000000000 ─────────────────┤ PAGE_OFFSET

... 空洞 ... │ 非寻址区域

0x00007FFFFFFFFFFF ─────────────────┤

用户空间 │ 用户空间
(128 TB) │ (低半部分)

0x0000000000000000 ─────────────────┘

关键区域说明

  • 用户空间(0x0 - 0x00007FFFFFFFFFFF):128 TB,包含代码段、数据段、堆、栈、mmap 区域、vdso 等
  • 线性映射(Linear Mapping):从 PAGE_OFFSET 开始,将整个物理内存进行 1:1 的偏移映射。内核通过 __pa() / __va() 宏在线性映射区和物理地址之间快速转换
  • vmalloc 区域:用于 vmalloc()、ioremap() 等非连续物理页的虚拟地址映射
  • VMEMMAP:struct page 数组的虚拟映射,每个物理页对应一个 struct page 实例
  • MODULES 区域:内核模块的加载地址空间
  • KERNEL_LINK_ADDR:内核映像的链接地址,通常为 end - 2GB,确保内核代码位于虚拟地址空间顶部附近
  • Fixmap:固定映射的虚拟地址区域,用于早期启动阶段和特定硬件映射
  • PCI I/O:PCI 设备 I/O 端口映射

在 Sv39 模式下,用户空间缩减为 512 GB,内核空间同样有限,但布局结构类似。

6.2.3 页表项格式

RISC-V 的页表项(PTE)为 64 位,格式如下:

1
2
3
4
5
页表项格式(64 位):
+------+------+-------+-------+-------+----+----+----+----+----+----+----+----+
| 63 |62-61 |60-54 | 53-10 | 9-8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| NAPOT| MT | RSV | PFN | SW | D | A | G | U | X | W | R | V |
+------+------+-------+-------+-------+----+----+----+----+----+----+----+----+

各字段说明

位域 名称 说明
63 NAPOT 自然对齐连续页标记(需 Svnapot 扩展)
62-61 MT 内存类型(需 Svpbmt 扩展)
60-54 RSV 保留,必须为零(或由未来扩展定义)
53-10 PFN 物理页帧号,44 位(可寻址 2^56 字节物理内存)
9-8 SW 软件可用位,内核自行定义用途
7 D 脏位(Dirty),该页被写入时由硬件或软件置位
6 A 访问位(Accessed),该页被访问时由硬件或软件置位
5 G 全局映射,不随 ASID 切换而失效
4 U 用户态可访问
3 X 可执行
2 W 可写
1 R 可读
0 V 有效位

RWX 编码的特殊含义

RISC-V 页表项通过 V 位和 RWX 组合来区分指针项和叶节点项:

V R W X 含义
0 x x x 无效项(not present)
1 0 0 0 非叶节点,指向下一级页表(PFN 是下一级页表的物理地址)
1 ≠0 - - 叶节点项,映射一个物理页(权限由 R/W/X/U 编码)

这种设计非常优雅:不需要像 x86 那样额外的 PS(Page Size)位来区分大页,因为 RWX=000 且 V=1 天然表示”这不是最终映射,继续遍历”。大页通过在中间级别使用叶节点项实现。

与 x86 对比:x86 使用独立的 Present 位和 PS 位,其中 Present=1 + PS=1 表示大页。RISC-V 则通过 RWX 编码隐式表达,减少了专用控制位的数量。

6.2.4 Svpbmt 内存类型

Svpbmt 扩展在页表项的 bit[62:61] 中编码内存类型:

MT 值 名称 属性
00 PMA 平台默认(通常为缓存、强序)
01 NC 非缓存、幂等、弱序
10 IO 非缓存、非幂等、强序
11 - 保留

内核在映射设备寄存器(ioremap)时使用 IO 类型,确保每次访问都精确到达设备而非被缓存。对于帧缓冲等可缓存但不要求强序的区域使用 NC 类型。默认情况下(PMA),普通内存使用平台的缓存策略。

Linux 内核通过 pgprot_noncached()pgprot_writecombine() 等宏来设置对应的内存类型位。

6.2.5 ASID 与 TLB 管理

ASID(地址空间标识符)

SATP 中的 16 位 ASID 用于标识不同的地址空间。当内核切换进程时,如果新进程的 ASID 与当前 TLB 中的 ASID 匹配,则无需刷新 TLB。这显著减少了上下文切换的开销。

内核通过 asm-generic/tlb.harch/riscv/mm/context.c 中的 get_new_asid() 管理 ASID 分配。当 16 位 ASID 溢出(65536 个进程后)时,内核执行全局 TLB 冲刷并重新分配 ASID。

sfence.vma 指令

RISC-V 通过 sfence.vma 指令管理 TLB:

1
2
3
4
sfence.vma              # 刷新所有 TLB 项
sfence.vma rs1, x0 # 仅刷新虚拟地址 rs1 对应的 TLB 项
sfence.vma x0, rs2 # 仅刷新 ASID=rs2 的 TLB 项
sfence.vma rs1, rs2 # 刷新特定虚拟地址 + 特定 ASID 的 TLB 项

在 Linux 内核中,TLB 刷新的核心函数是 flush_tlb_page()flush_tlb_mm() 等,最终都会调用 sfence.vma。Svinval 扩展提供了更细粒度的 TLB 无效化机制,允许批量操作以减少 sfence.vma 的开销。

6.2.6 与 ARM64 页表机制对比

ARM64 和 RISC-V 在虚拟内存设计上采取了不同策略:

双页表 vs 单页表

ARM64(TTBR0/TTBR1 双页表)

  • 硬件根据虚拟地址的 bit[55] 自动选择使用 TTBR0(用户空间)或 TTBR1(内核空间)
  • 用户空间和内核空间各自拥有独立的页表基址寄存器
  • 切换进程时只需更新 TTBR0,TTBR1 保持不变
  • 内核空间和用户空间天然隔离

RISC-V(SATP 单页表)

  • 只有一个页表基址寄存器 satp
  • 内核空间映射在虚拟地址空间的顶部,用户空间在底部
  • 一次 satp 写入即切换整个地址空间(包含内核映射和用户映射)
  • 每个进程的页表都包含内核映射的副本(通过共享顶层页表页实现)

S-mode 访问用户页面

RISC-V 的 sstatus.SUM 位提供了一种独特机制:

  • SUM=0(默认):S-mode 代码访问 U=1 的页面会触发缺页异常
  • SUM=1:S-mode 代码可以访问 U=1 的页面

内核在执行 copy_from_user() / copy_to_user() 等操作时,通过临时设置 SUM 位来安全地访问用户空间页面。如果用户地址非法,触发的是缺页异常而非通用保护错误,内核可以优雅地返回 -EFAULT。

ARM64 则通过 PAN(Privileged Access Never)实现类似保护,但默认禁止特权访问用户页面,需要临时禁用 PAN。两种机制的出发点相同,但实现方式不同。

页表项权限编码

ARM64 的页表项使用独立的属性位(如 AP[2:1] 控制读写、nG 控制全局、PXN/UXN 控制执行),而 RISC-V 使用更紧凑的 R/W/X 三位组合。RISC-V 的编码方式更节省位空间,但也意味着权限组合更受限——例如不允许”只执行不可读”的页面(R=0, X=1 在标准规范中无效)。

6.3 RISC-V 异常与中断处理

本节深入分析 RISC-V 的 Trap 处理机制,包括异常和中断的分类、内核入口处理流程、系统调用路径、中断控制器架构以及异常返回机制。

6.3.1 Trap 处理机制

Trap 入口配置

RISC-V 的 Trap(陷阱)包括异常(同步事件)和中断(异步事件),它们共享同一套入口机制。stvec 寄存器配置 Trap 入口:

1
2
3
stvec 格式:
[MXLEN-1:2] BASE 入口地址(必须 4 字节对齐)
[1:0] MODE 00 = Direct 模式 / 01 = Vectored 模式

Direct 模式:所有 Trap(异常和中断)都跳转到 BASE 地址。Linux 内核使用此模式,在软件层面完成分发。

Vectored 模式:异常仍跳转到 BASE,但中断跳转到 BASE + 4 * cause。此模式在 Linux 中未使用,因为内核需要在中断入口保存更多上下文。

Trap 发生时的硬件行为

当 Trap 发生时,CPU 自动执行以下操作(不可分割):

  1. sepc <- PC:将当前 PC 保存到 sepc(如果是 ecall,保存 ecall 指令本身的地址)
  2. scause <- cause:写入异常/中断原因编码(bit 63 区分中断/异常)
  3. stval <- value:写入辅助信息(如缺页地址、非法指令编码)
  4. sstatus.SPP <- previous_privilege:记录进入前的特权级(0=U, 1=S)
  5. sstatus.SPIE <- sstatus.SIE:保存中断使能状态
  6. sstatus.SIE <- 0:关闭 S-mode 中断(进入异常处理期间禁止嵌套中断)
  7. PC <- stvec.BASE:跳转到 Trap 入口

sscratch 与快速来源检测

内核利用 sscratch 实现 Trap 来源的高效检测:

  • 内核态运行时:sscratch = 0(或指向当前 task 的 thread_info)
  • 用户态运行时:sscratch = 用户态 tp 值

在 Trap 入口(handle_exception),内核执行:

1
2
3
csrrw  tp, sscratch, tp    # 交换 tp 和 sscratch
bnez tp, .Ltrap_from_user # 如果 tp != 0,说明来自用户态
# 如果 tp == 0,说明来自内核态

这种设计避免了访问内存来判断 Trap 来源,仅需一条 CSR 交换指令和一条条件分支。

6.3.2 handle_exception 流程

内核的 Trap 入口位于 arch/riscv/kernel/entry.S,核心流程如下:

1
2
3
4
5
6
7
8
9
10
handle_exception:
1. 交换 tp 与 sscratch,检测用户态/内核态来源
2. 分配内核栈(用户态来源)或使用当前栈(内核态来源)
3. 保存所有 32 个 GPR 到 pt_regs 结构
4. 读取 sstatus, sepc, scause, stval 到通用寄存器
5. 将这些 CSR 值存入 pt_regs 对应字段
6. 根据 scause.bit[63] 分发:
- 1 (中断) → handle_interrupt()
- 0 (异常) → excp_vect_table[code]()
7. 跳转到 ret_from_exception

pt_regs 结构

内核在栈上构造 pt_regs 结构保存完整的异常上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct pt_regs {
unsigned long epc; // sepc,异常返回地址
unsigned long ra; // x1
unsigned long sp; // x2
unsigned long gp; // x3
unsigned long tp; // x4
unsigned long t0; // x5
// ... t1-t6, a0-a7, s0-s11 共 31 个 GPR ...
unsigned long status; // sstatus
unsigned long cause; // scause
unsigned long tval; // stval
// 注意:epc 位于栈底(高地址),保证栈顶对齐
};

pt_regs 的布局设计使得系统调用参数(a0-a7)位于固定偏移,方便快速访问。

6.3.3 异常原因编码

异常的 scause 编码中 bit[63]=0,低 63 位为异常代码:

Code 名称 说明
0 Instruction misaligned 取指地址未对齐
1 Instruction access fault 取指权限/物理错误
2 Illegal instruction 未实现或特权级不足的指令
3 Breakpoint ebreak 指令或调试断点
4 Load misaligned 加载地址未对齐
5 Load access fault 加载权限/物理错误
6 Store misaligned 存储地址未对齐
7 Store access fault 存储权限/物理错误
8 ecall from U-mode 系统调用入口
9 ecall from S-mode S-mode 的 ecall(调试用)
12 Instruction page fault 取指缺页
13 Load page fault 加载缺页(COW、按需分配等)
15 Store page fault 存储缺页(COW、写时复制)
18 Software check CFI 软件检查失败(Zicfilp/Zicfiss)
19 Hardware error 硬件错误

内核通过 excp_vect_table 数组分发异常,每个表项指向对应的处理函数。例如 code=13(Load page fault)对应 do_page_fault(),code=8(ecall from U-mode)对应 do_syscall()

6.3.4 中断原因编码

中断的 scause 编码中 bit[63]=1:

Code 名称 说明
1 SSI Supervisor Software Interrupt(软件 IPI)
5 STI Supervisor Timer Interrupt(定时器)
9 SEI Supervisor External Interrupt(外部设备)
13 PMU overflow 性能计数器溢出

中断控制器架构

RISC-V 的中断控制器经历了几代演进:

PLIC(Platform-Level Interrupt Controller)

  • 最早的外部中断控制器,管理来自设备的中断
  • 为每个 Hart 提供独立的优先级阈值和中断上下文(Context)
  • 内核通过 irq_chip 驱动(drivers/irqchip/irq-sifive-plic.c)管理 PLIC
  • 不支持消息信号中断(MSI),需要轮询或额外的软件支持

APLIC(Advanced PLIC)

  • 新一代外部中断控制器,配合 IMSIC 使用
  • 支持两种模式:Direct 模式(类似 PLIC)和 MSI 模式(通过 IMSIC 发送 MSI)
  • 更好的虚拟化支持和可扩展性

IMSIC(Incoming Message Signal Interrupt Controller)

  • 提供每个 Hart 独立的中断邮箱
  • 支持 MSI(Message Signaled Interrupt),设备直接写入 MSI 地址触发中断
  • 每个中断有独立标识,无需共享中断线

SBI IPI(软件中断)

  • 跨核通知(IPI)通过 SBI ecall 实现
  • SBI 固件(OpenSBI)通过触发 SSI(软件中断)通知目标 Hart
  • 常见用途:TLB 刷新广播、调度器踢核、停止 CPU

定时器中断

  • 传统方式:通过 SBI TIME 扩展设置定时器(SBI ecall)
  • Sstc 扩展:直接写入 stimecmp CSR 设置下次定时器中断,无需陷入 M-mode
  • Linux 内核优先使用 Sstc(如果可用),否则回退到 SBI 调用

6.3.5 系统调用

系统调用流程

RISC-V 系统调用使用 ecall(Environment Call)指令触发:

1
2
3
4
5
6
7
8
9
10
11
12
用户态:
a7 = 系统调用号
a0-a5 = 参数(最多 6 个)
ecall ← 触发异常 code=8

内核态(handle_exception 分发到 do_syscall):
1. 从 pt_regs 获取 a7(系统调用号)
2. 检查 a7 < NR_syscalls
3. 调用 sys_call_table[a7](a0, a1, a2, a3, a4, a5)
4. 将返回值写入 pt_regs->a0
5. sepc += 4(跳过 ecall 指令)
6. 跳转到 ret_from_exception

与 x86/ARM64 对比

  • x86:使用 SYSCALL 指令(快速系统调用),通过 MSR 寄存器配置入口,硬件自动保存 RSP 到 MSR
  • ARM64:使用 SVC #0 指令,异常级别从 EL0 切换到 EL1
  • RISC-V:使用 ecall 指令,没有专用的快速系统调用指令。ecall 在所有特权级都可用,通过 scause 区分来源特权级

RISC-V 的 ecall 机制简洁但稍显原始——没有像 x86 SYSCALL 那样的专用硬件加速。不过,由于 RISC-V 的 Trap 处理路径本身足够轻量(仅交换 sscratch、保存寄存器),实际性能差距并不显著。

6.3.6 异常返回(ret_from_exception)

异常返回流程负责恢复上下文并将控制权交还给被中断的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ret_from_exception:
1. 检查 sstatus.SPP:
- SPP=1 → 返回内核态(resume_kernel)
- SPP=0 → 返回用户态(resume_userspace)

resume_userspace:
2. 检查 _TIF_WORK_MASK 中的线程标志
- 需要调度 → 调用 schedule()
- 有信号 → 调用 do_signal() 处理挂起信号
- 需要通知 → 处理 user mode 返回前的工作
3. 擦除内核栈内容(安全考虑,防止信息泄漏)
4. 将当前 tp 值写入 sscratch(为下次 Trap 做准备)
5. 从 pt_regs 恢复所有 GPR
6. 执行 sret 指令

sret 硬件行为:
- PC <- sepc(恢复执行位置)
- 特权级 <- sstatus.SPP(恢复之前的特权级)
- sstatus.SIE <- sstatus.SPIE(恢复中断使能状态)
- sstatus.SPP <- 0(复位为 U-mode)
- sstatus.SPIE <- 1(复位)

内核态返回的特殊处理

如果 Trap 来自内核态(SPP=1),返回路径更简单:

  • 不需要处理信号
  • 不需要擦除栈内容
  • 不需要更新 sscratch
  • 直接恢复寄存器并执行 sret

但需要检查是否设置了 TIF_NEED_RESCHED 标志,如果设置了则需要调用 schedule() 进行调度。这是内核抢占(Preemption)的核心实现点。

sret vs eret vs iret

  • RISC-V sret:从 S-mode Trap 返回,恢复 sepc 到 PC,恢复特权级和中断使能
  • ARM64 eret:从异常返回,恢复 ELR_EL1 到 PC,恢复 SPSR_EL1 到 PSTATE
  • x86 iret:从中断返回,从栈上弹出 RIP、CS、RFLAGS、RSP、SS

三者语义相似:恢复 PC、恢复特权级/标志、继续执行。RISC-V sret 的硬件行为最为精简——仅修改 PC、特权级和中断使能三个状态。

6.4 RISC-V 内核启动:从 _start 到 start_kernel

RISC-V 架构的内核启动流程是理解整个系统初始化的关键。与 ARM64 类似,RISC-V Linux 内核在进入通用启动代码 start_kernel() 之前,需要完成硬件环境检测、早期页表建立、MMU 使能等一系列底层工作。本节基于 arch/riscv/kernel/head.S 的实际代码,详细分析这一过程。

6.4.1 启动入口条件

内核镜像被加载后,固件(通常是 OpenSBI)将控制权转交给内核的入口点 _start。此时系统必须满足以下条件:

  • a0 = hartid:当前硬件线程(hart)的 ID,由固件传入
  • a1 = DTB 物理地址:设备树 blob(Device Tree Blob)在内存中的物理地址
  • MMU 状态:可以开启也可以关闭,内核代码同时处理这两种情况
  • PMP(物理内存保护):必须允许内核镜像、DTB 和 initrd 所在内存区域的访问权限

与 ARM64 固定要求进入 EL1 或 EL2 不同,RISC-V 内核总是在 S 模式(Supervisor mode)下运行,M 模式的工作由 OpenSBI 负责。

6.4.2 _start:镜像头部与初始跳转

内核镜像的开头是一个标准化的头部结构,定义在 head.S 中:

1
2
3
4
5
6
7
8
/* 内核镜像头部 */
.word code0 /* MZ 魔数(EFI 兼容)或跳转指令 */
.word code1 /* 跳转到 _start_kernel */
.quad text_offset /* 内核加载偏移 */
.quad image_size /* 内核镜像大小 */
.quad flags /* 标志位 */
.quad 0 /* 保留 */
.ascii "RISCV\0\0\0" /* 魔数标识 */

code0 字段有两个用途:当通过 EFI 启动时,它包含 MZ 签名以兼容 PE/COFF 加载器;当直接启动时,它是一条跳转指令。code1 则无条件跳转到 _start_kernel,这是真正的启动逻辑入口。

6.4.3 _start_kernel:核心初始化序列

_start_kernel 是内核早期初始化的核心函数,按顺序完成以下工作:

1. 禁用中断和协处理器

1
2
3
4
5
6
csrw    CSR_IE, zero          /* 禁用所有中断 */
csrw CSR_IP, zero /* 清除所有挂起中断 */
/* 清除 sstatus 中的 FS 和 VS 位,禁用 FPU 和向量单元 */
li t0, ~(SR_FS | SR_VS)
and t0, t0, s_status
csrw CSR_SSTATUS, t0

在 MMU 开启之前,必须确保没有中断或异常会干扰页表设置过程。

2. 保存 hartid 和 DTB 指针

a0(hartid)和 a1(DTB 地址)保存到全局变量中,供后续代码使用:

1
2
3
4
mv      s0, a0                /* 保存 hartid */
mv s1, a1 /* 保存 DTB 指针 */
REG_S a0, CSVAR(tp, hartid)
REG_S a1, CSVAR(tp, dtb_pointer)

3. SMP 启动:抽签选择引导 hart

在 spinwait SMP 模式下(CONFIG_RISCV_BOOT_SPINWAIT),所有 hart 同时进入内核。通过原子操作进行”抽签”(lottery)来决定哪个 hart 成为引导 CPU:

1
2
3
li      t0, 1
amoadd.w t1, t0, (a2) /* 原子递增 hart_lottery */
bnez t1, .Lspin_wait /* 非零表示已有赢家,进入自旋等待 */

赢得抽签的 hart 继续执行引导流程,其余 hart 进入自旋等待,直到引导 CPU 为它们设置好栈和任务指针。

4. 清零 BSS 段

1
2
3
4
5
6
    la      t0, __bss_start
la t1, __bss_stop
.Lclear_bss:
sd zero, (t0)
addi t0, t0, 8
bne t0, t1, .Lclear_bss

BSS 段必须在使用任何全局变量之前清零。

5. 建立早期页表:setup_vm()

调用 C 函数 setup_vm() 创建两个早期映射:

  • 恒等映射(identity mapping):虚拟地址等于物理地址,确保 MMU 开启后当前代码仍可执行
  • 内核映射(kernel mapping):将内核镜像映射到其最终的虚拟地址(PAGE_OFFSET + 物理地址偏移)

这两个映射使用同一张页表 early_pg_dir,其物理地址在 MMU 关闭状态下可以直接使用。

6. 使能 MMU:relocate_enable_mmu()

这是启动过程中最关键的一步。详细流程将在下一小节分析。

7. SoC 早期初始化

调用 soc_early_init() 执行特定 SoC 的早期设置,例如时钟初始化、电源域配置等。这是一个弱函数(weak),各 SoC 可以提供自己的实现。

8. 跳转到 start_kernel()

所有架构特定的早期初始化完成后,跳转到通用的 Linux 内核启动入口:

1
call    start_kernel

从此开始,RISC-V 的启动流程与所有架构共享同一套通用初始化代码。

6.4.4 relocate_enable_mmu:使能内存管理单元

relocate_enable_mmu 负责从物理地址模式切换到虚拟地址模式,这是启动过程中最精细的操作之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
relocate_enable_mmu:
/* 计算 SATP 值:模式 | ASID | 页表物理地址 >> 12 */
li t0, SATP_MODE /* Sv39 或 Sv48 模式 */
or t0, t0, a1 /* a1 = 页表物理地址 >> 12 */

/* 刷新整个 TLB */
sfence.vma

/* 写入 SATP 寄存器,使能 MMU! */
csrw satp, t0

/* 再次刷新 TLB,确保旧映射不残留 */
sfence.vma

SATP(Supervisor Address Translation and Protection)寄存器是 RISC-V 页表的核心控制寄存器。写入 SATP 后,MMU 立即生效,所有地址访问都经过页表翻译。

关键点在于,MMU 开启的瞬间,PC(程序计数器)仍然指向物理地址空间的代码。通过 trampoline_pg_dir 中同时存在的恒等映射和内核映射,确保代码在 MMU 开启前后都能正确执行。随后,代码切换到最终的 swapper_pg_dir 页表。

6.4.5 完整启动流程

从上电到进入通用内核初始化,RISC-V 的完整启动链如下:

1
2
3
4
5
6
7
8
9
10
11
12
OpenSBI (M 模式固件)
→ SBI ecall 启动 S 模式
→ _start (内核入口)
→ _start_kernel
→ 禁用中断和 FPU/向量单元
→ 保存 hartid 和 DTB 指针
→ SMP 抽签(spinwait 模式)
→ 清零 BSS 段
→ setup_vm()(建立早期页表)
→ relocate_enable_mmu()(使能 MMU)
→ soc_early_init()(SoC 早期初始化)
→ start_kernel()(通用 Linux 初始化)

6.4.6 setup_arch():架构特定初始化

start_kernel() 在早期阶段调用 setup_arch(),这是 RISC-V 架构向通用内核注册自身信息的入口,定义在 arch/riscv/kernel/setup.c 中:

  • 解析 DTB:从保存的物理地址读取设备树,解析内存布局、CPU 信息、设备列表
  • sbi_init():探测 SBI 固件的可用扩展(详见第 6.5 节),设置定时器、IPI、TLB 刷新等功能指针
  • paging_init():建立完整的内核页表,包括 vmalloc 区域、fixmap、vmemmap 等
  • efi_init():如果通过 EFI 启动,初始化 EFI 运行时服务
  • 填充 hwcap:通过探测 CSR 寄存器和设备树中的 ISA 字符串,确定 CPU 支持的特性(如 FPU、向量扩展等),写入 elf_hwcap
  • SMP 初始化:从设备树的 CPU 节点中发现的每个 hart,注册为可能的 CPU

6.4.7 与 ARM64 启动流程的对比

RISC-V 和 ARM64 的内核启动流程有许多相似之处,也有显著差异:

方面 ARM64 RISC-V
启动模式 无实模式,直接进入异常级别 无实模式,直接进入特权级
硬件信息传递 x0 = DTB 物理地址 a1 = DTB 物理地址
特权级检测 init_kernel_el 检测 EL2 并降级到 EL1 内核始终在 S 模式运行,M 模式由 OpenSBI 管理
过渡页表 reserved_pg_dir trampoline_pg_dir
启动入口函数 primary_entry _start_kernel
MMU 使能 __cpu_setup + __enable_mmu relocate_enable_mmu
最终跳转 start_kernel() start_kernel()

两者最根本的架构差异在于特权级模型:ARM64 内核可以从 EL2 启动并自行降级到 EL1(以支持虚拟化扩展),而 RISC-V 内核始终运行在 S 模式,M 模式的功能通过 SBI 调用委托给 OpenSBI。这种设计使得 RISC-V 的启动代码更加简洁,但也意味着内核无法直接访问某些底层硬件功能。

另一个重要区别是 SMP 启动方式:ARM64 使用 PSCI(Power State Coordination Interface)通过 SMC 调用来启动辅助 CPU,而 RISC-V 使用 SBI HSM(Hart State Management)扩展通过 ecall 实现(详见第 6.6 节)。在旧固件不支持 HSM 的情况下,RISC-V 还支持 spinwait 模式,这是 ARM64 所没有的。

6.5 RISC-V SBI:Supervisor Binary Interface

SBI(Supervisor Binary Interface)是 RISC-V 架构中 S 模式(Supervisor)内核与 M 模式(Machine)固件之间的标准接口。在 RISC-V 的特权级架构中,内核运行在 S 模式,无法直接访问 M 模式的硬件资源。SBI 提供了一套标准化的调用机制,使内核能够通过受控的方式请求 M 模式固件的服务。目前最广泛使用的 SBI 实现是 OpenSBI。

6.5.1 SBI 的定位与作用

SBI 的角色类似于 ARM 架构中的 PSCI(Power State Coordination Interface),但其覆盖范围远不止 CPU 电源管理。SBI 涵盖了以下功能领域:

  • 定时器:设置下次定时器中断的时间
  • 核间中断(IPI):向其他 hart 发送中断
  • CPU 管理:启动、停止、挂起 hart
  • TLB 刷新:远程 TLB 无效化操作
  • 系统复位:关机、重启
  • 控制台:早期调试输出
  • 性能监控(PMU):硬件性能计数器管理
  • 固件特性:启用/配置安全特性(如影子栈)

SBI 使得内核不必关心 M 模式的实现细节,实现了固件与内核的解耦。OpenSBI 可以独立更新,而无需重新编译内核。

6.5.2 SBI 规范版本演进

SBI 规范经历了多个版本的迭代,功能逐步增强:

v0.1(legacy):最早的 SBI 实现。使用固定的功能 ID 进行调用,不支持扩展探测。每个功能(如定时器、IPI、控制台)都有预定义的调用号。内核无法在运行时查询固件支持哪些功能,只能硬编码假设所有功能都可用。

v0.2:引入了基于扩展(extension)的探测机制。内核可以通过 sbi_probe_extension() 查询固件是否支持某个特定扩展。这为固件和内核之间的兼容性提供了灵活的协商方式。v0.2 还定义了 BASE 扩展作为所有其他扩展的基础。

v0.3:在 v0.2 基础上新增了三个重要扩展:

  • SRST(System Reset):统一的系统复位接口,支持关机和不同类型的重启
  • PMU(Performance Monitoring Unit):硬件性能计数器的配置和读取
  • DBCN(Debug Console):标准化的调试控制台输出,取代了 legacy 的控制台调用

v2.0:最新版本,进一步扩展:

  • FWFT(Firmware Features):允许内核请求固件启用特定特性,如影子栈(Shadow Stack)和着陆点(Landing Pad)等控制流完整性保护
  • NACL(Nested Acceleration):为 KVM 虚拟化提供嵌套虚拟化加速,减少 VM exit 开销

6.5.3 核心 SBI 扩展

每个 SBI 扩展由一个 32 位 ID 标识,内部包含多个功能(function)。以下是 Linux 内核使用的主要扩展:

BASE(0x10):基础扩展,所有 v0.2+ 固件必须实现。提供:

  • sbi_get_spec_version():获取 SBI 规范版本
  • sbi_probe_extension(ext_id):探测指定扩展是否可用
  • sbi_get_firmware_id():获取固件标识

TIME(0x54494D45):定时器扩展。核心函数 sbi_set_timer(stime) 设置下一次定时器中断的触发时间。内核的时钟事件驱动通过此接口编程定时器。在旧固件上,退回到直接写入 mtimecmp CSR。

IPI(0x735049):核间中断扩展。sbi_send_ipi(hart_mask) 向指定的 hart 集合发送 IPI。内核的 SMP IPI 机制依赖此扩展。较新的硬件可能使用 ACLINT(Advanced Core Local Interruptor)或 IMSIC(Incoming Message Signaled Interrupt Controller)直接发送 IPI,绕过 SBI。

RFENCE(0x52464E43):远程栅栏扩展。提供以下 TLB 操作:

  • sbi_remote_sfence_vma():远程 sfence.vma
  • sbi_remote_hfence_gvma():远程 hfence.gvma(虚拟化场景)
  • sbi_remote_hfence_vvma():远程 hfence.vvma(虚拟化场景)

这些操作允许一个 hart 请求其他 hart 执行 TLB 刷新,是实现 SMP TLB 一致性的关键。

HSM(0x48534D):Hart 状态管理扩展。提供 CPU 生命周期管理:

  • sbi_hsm_hart_start(hartid, start_addr, opaque):启动指定 hart
  • sbi_hsm_hart_stop():停止当前 hart
  • sbi_hsm_hart_status(hartid):查询 hart 状态
  • sbi_hsm_hart_suspend(suspend_type, resume_addr, opaque):挂起 hart

HSM 是实现 CPU hotplug 和 SMP 启动的基础(详见第 6.6 节)。

SRST(0x53525354):系统复位扩展。sbi_system_reset(reset_type, reset_reason) 支持关机(shutdown)、冷重启(cold reboot)、热重启(warm reboot)等复位类型。

PMU(0x504D55):性能监控扩展。提供:

  • 计数器数量查询和事件到计数器的映射
  • 计数器的启动、停止和配置
  • 计数器值的读取

DBCN(0x4442434E):调试控制台扩展。sbi_debug_console_write(bytes)sbi_debug_console_read(bytes) 提供标准化的控制台 I/O,用于早期内核调试输出。

6.5.4 SBI 调用约定

SBI 使用 ecall(Environment Call)指令从 S 模式陷阱到 M 模式。调用约定如下:

1
2
3
4
5
6
7
8
9
10
11
12
ecall 指令执行后:
- 触发 S 模式 → M 模式的异常
- M 模式固件(OpenSBI)处理 ecall
- 根据 a7 中的扩展 ID 和 a6 中的功能 ID 分发到对应的处理函数
- 返回值通过 a0 和 a1 传回

寄存器使用:
a7 = 扩展 ID(Extension ID)
a6 = 扩展内功能 ID(Function ID)
a0 = 返回值 0(错误码,0 表示成功)
a1 = 返回值 1(输出参数)
a2-a5 = 输入参数

错误码定义:SBI_SUCCESS(0)表示成功,SBI_ERR_FAILED(-1)表示通用失败,SBI_ERR_NOT_SUPPORTED(-2)表示不支持,SBI_ERR_INVALID_PARAM(-3)表示参数无效。

6.5.5 与 ARM PSCI 的对比

SBI 和 PSCI 都是为操作系统提供固件服务的接口,但设计理念和覆盖范围有显著差异:

方面 ARM PSCI RISC-V SBI
覆盖范围 CPU 电源管理为主 全面(定时器、IPI、TLB、控制台、PMU、复位等)
调用机制 SMC(Secure Monitor Call) ecall(Environment Call)
可扩展性 通过 PSCI 版本号扩展 通过扩展探测机制(probe extension)灵活扩展
CPU 管理 cpu_on/cpu_off/cpu_suspend HSM:hart_start/stop/status/suspend
定时器 不涉及(使用硬件定时器) sbi_set_timer(SBI 管理)
IPI 不涉及(使用硬件中断控制器) sbi_send_ipi(SBI 管理或直接硬件)
TLB 操作 不涉及(硬件自动维护或内核直接操作) sbi_remote_sfence_vma

SBI 的设计更加全面和可扩展。PSCI 专注于 CPU 电源状态管理,定时器、IPI、TLB 操作在 ARM 上通常由内核直接操作硬件完成。而 RISC-V 的设计哲学是将更多功能下沉到 M 模式固件,使 S 模式内核更加精简和可移植。这种设计的代价是某些操作多了一次 ecall 的开销,但也带来了更好的隔离性和可维护性。

6.5.6 sbi_init():启动时的 SBI 初始化

内核在 setup_arch() 中调用 sbi_init(),完成 SBI 子系统的初始化:

  1. 获取规范版本:调用 BASE 扩展的 get_spec_version 确认固件支持的 SBI 版本
  2. 探测扩展:逐一探测所有已知的扩展(TIME、IPI、RFENCE、HSM、SRST、PMU 等),记录哪些扩展可用
  3. 设置函数指针:根据探测结果,将定时器、IPI、远程栅栏等操作绑定到具体的实现函数。优先使用 v0.2+ 的新接口,仅在固件不支持时才退回到 v0.1 的 legacy 调用
  4. 打印信息:输出 SBI 版本和可用扩展列表到内核日志

这种基于探测的初始化机制确保内核能够在不同版本的 OpenSBI 上正常工作,同时自动利用固件提供的最新功能。

6.6 RISC-V 多核启动与 HSM

RISC-V 架构的对称多处理(SMP)支持涉及多个 hart(硬件线程)的发现、启动和协调。与 ARM64 使用 PSCI 进行 CPU 管理不同,RISC-V 提供了两种 SMP 启动机制:基于 SBI HSM 扩展的现代方式和基于 spinwait 的传统方式。本节详细分析这两种机制以及相关的核间通信。

6.6.1 CPU 发现

内核通过 setup_smp() 函数发现系统中的所有 CPU。发现方式取决于固件接口:

设备树(DT)方式:遍历设备树中的 /cpus 节点,解析每个 cpu@N 子节点,提取 hartid 和 ISA 信息。每个有效的 CPU 节点对应一个 hart。引导 CPU 的 hartid 通过启动时传入的 a0 寄存器值确定。

ACPI 方式:通过 RINTC(RISC-V Interrupt Controller)表发现 CPU,每个 RINTC 条目对应一个 hart。

发现过程会跳过 hartid 为 -1(无效)的条目以及引导 CPU 自身,将所有有效的 hart 注册到内核的 CPU 掩码中。最大支持 CPU 数量受 NR_CPUS 配置项限制,也受 misa CSR 中 hart 数量字段的约束。

6.6.2 SBI HSM:Hart 状态管理

HSM(Hart State Management)是 SBI 的一个扩展(扩展 ID 0x48534D),专门用于管理 hart 的生命周期。它定义了 hart 的状态机:

1
2
3
4
5
STOPPED ←→ START_PENDING → STARTED
↑ ↓
←── STOP_PENDING ←───────
↑ ↓
←── SUSPENDED ←── SUSPEND_PENDING ← STARTED

核心状态说明:

  • STOPPED:hart 未运行,可以被启动
  • STARTED:hart 正常运行中
  • START_PENDING:hart 正在被启动的过程中(过渡状态)
  • STOP_PENDING:hart 正在被停止的过程中(过渡状态)
  • SUSPENDED:hart 处于低功耗挂起状态,可以通过 sbi_hsm_hart_start() 唤醒

HSM 提供四个核心操作:

  • sbi_hsm_hart_start(hartid, start_addr, opaque):启动一个处于 STOPPED 或 SUSPENDED 状态的 hart,使其从 start_addr 开始执行。opaque 参数传递给目标 hart 的 a0 寄存器
  • sbi_hsm_hart_stop():停止当前 hart,使其进入 STOPPED 状态。调用后不会返回
  • sbi_hsm_hart_status(hartid):查询指定 hart 的当前状态
  • sbi_hsm_hart_suspend(suspend_type, resume_addr, opaque):将当前 hart 挂起。suspend_type 区分保留上下文的挂起(可恢复)和不保留上下文的挂起(类似关机)

HSM 的功能与 ARM PSCI 的 cpu_on/cpu_off/cpu_suspend 直接对应,但 RISC-V 的状态机定义更加形式化。

6.6.3 辅助 CPU 启动流程

使用 SBI HSM 时,辅助 CPU 的启动流程如下:

引导 CPU 侧

1
2
3
__cpu_up(cpu)
→ sbi_cpu_start(hartid, start_addr)
→ sbi_hsm_hart_start(hartid, secondary_start_sbi, hartid)

__cpu_up() 为目标 CPU 准备栈和任务结构体,然后通过 SBI HSM 请求 OpenSBI 启动目标 hart。start_addr 指向 head.S 中的 secondary_start_sbi 标号。

辅助 CPU 侧

1
2
3
4
5
6
7
OpenSBI 启动目标 hart,跳转到 secondary_start_sbi
→ 保存 hartid
→ csrw CSR_IE, zero(禁用中断)
→ 清除 sstatus.FS/VS(禁用 FPU/向量)
→ 设置 SP(栈指针)
→ relocate_enable_mmu()(使能 MMU,切换到 swapper_pg_dir)
→ smp_callin()

smp_callin() 完成以下工作:

  1. 设置当前 CPU 的 per-CPU 数据区域
  2. 通知引导 CPU 自己已上线
  3. 启用本 hart 的 IPI 中断
  4. 进入空闲调度循环,等待调度器分配任务

6.6.4 Spinwait 启动方式

CONFIG_RISCV_BOOT_SPINWAIT 配置选项启用 spinwait 启动模式,用于不支持 SBI HSM 的旧固件。在这种模式下,所有 hart 同时进入内核入口点 _start

抽签机制:所有 hart 原子递增全局变量 hart_lottery。第一个读到旧值为 0 的 hart 成为引导 CPU(赢家),其余 hart 进入自旋等待:

1
2
3
li      t0, 1
amoadd.w t1, t0, (hart_lottery)
bnez t1, .Lsecondary_park

辅助 hart 等待:未赢得抽签的 hart 在 .Lsecondary_park 处自旋,检查自己的 per-CPU 变量(栈指针和任务指针)是否已被引导 CPU 设置。一旦发现这些值非零,辅助 hart 便跳出循环,执行与 HSM 模式类似的 smp_callin() 流程。

spinwait 模式简单但不优雅:所有 hart 都需要执行内核的早期代码(直到抽签点),浪费了启动时间。现代系统应优先使用 HSM 模式。

6.6.5 核间中断(IPI)

核间中断是 SMP 系统中 CPU 之间通信的基本机制。RISC-V 的 IPI 实现有两条路径:

SBI IPI 扩展sbi_send_ipi(cpumask) 通过 ecall 请求 OpenSBI 向目标 hart 集合发送中断。这是最通用的方式,适用于所有硬件平台。OpenSBI 通过 M 模式的软件中断机制触发目标 hart 的 S 模式中断。

ACLINT/IMSIC 直接发送:较新的硬件提供了 ACLINT(Advanced Core Local Interruptor)的 MSCI(Mode-Selective Interrupt)功能或 IMSIC(Incoming Message Signaled Interrupt Controller)。内核可以直接向这些硬件寄存器写入来发送 IPI,无需经过 SBI,延迟更低。

IPI 处理流程:

1
2
3
4
5
6
7
8
9
10
IPI 中断触发
→ handle_IPI()
→ ipi_mux_process()
→ 检查 IPI 类型位图
→ 分发到具体处理函数:
- RESCHEDULE:触发调度
- CALL_FUNC:执行函数调用
- CPU_STOP:停止 CPU
- IRQ_WORK:处理中断工作队列
- TIMER:处理定时器事件

ipi_mux_process() 使用位图复用机制,多个 IPI 类型共享一个硬件中断号,通过软件位图区分具体类型。

6.6.6 CPU 热插拔

RISC-V 的 CPU 热插拔基于 SBI HSM 实现:

CPU 下线:内核调用 sbi_hsm_hart_stop(),OpenSBI 将当前 hart 置为 STOPPED 状态。内核在下线前会迁移该 CPU 上的所有中断和任务,并从调度器中移除。

CPU 上线:与启动辅助 CPU 流程相同,调用 sbi_hsm_hart_start() 重新启动已停止的 hart。hart 从 secondary_start_sbi 重新开始执行。

与其他架构的对比:

  • x86:通过 ACPI 定义的热插拔事件通知内核,使用 INIT/SIPI 序列启动 CPU
  • ARM64:通过 PSCI 的 cpu_off 下线、cpu_on 上线
  • RISC-V:通过 SBI HSM 的 hart_stop/hart_start 实现,机制与 PSCI 最为相似

三种架构的热插拔实现在内核侧的框架基本相同(都使用 cpuhp 状态机),差异主要体现在固件调用接口上。

6.7 RISC-V 安全特性 —— CFI、Zkr、Svpbmt 与 KASLR

RISC-V 架构在 Linux 7.0 中引入了丰富的安全机制,涵盖地址空间随机化、内存权限执行保护、控制流完整性(CFI)、硬件随机数生成器以及页面属性扩展。本节逐一分析这些特性的实现原理,并与 x86_64、ARM64 进行对比。

6.7.1 KASLR(CONFIG_RANDOMIZE_BASE)

KASLR(Kernel Address Space Layout Randomization)通过在每次启动时随机化内核的加载地址来增加漏洞利用难度。RISC-V RV64 的 KASLR 实现位于 arch/riscv/kernel/head.S,核心函数 relocate_kernel() 负责将内核重定位到随机偏移地址。

熵源获取流程:

  1. 优先从设备树(DTB)的 /chosen/kaslr-seed 属性读取随机种子。Bootloader(如 U-Boot)负责在启动前填充此值。
  2. 若 DTB 未提供种子,且硬件支持 Zkr 扩展,则通过读取 CSR_SEED(地址 0x015)获取硬件随机数。
  3. 将随机偏移量应用于内核的虚拟地址映射,relocate_kernel() 修正所有绝对地址引用。

架构对比:

  • x86_64:使用 RDRAND/RDSEED 指令或 EFI RNG Protocol 获取熵,通过解析 startup_64 中的物理地址计算偏移。
  • ARM64:依赖 EFI RNG Protocol 或 DTB kaslr-seed,在 __primary_entry 阶段完成重定位。
  • RISC-V:依赖 DTB kaslr-seed 或 Zkr CSR_SEED,模式与 ARM64 更为接近。

7.7.2 STRICT_KERNEL_RWX(W^X 强制执行)

W^X(Write XOR Execute)原则确保内核内存页不可同时可写和可执行。RISC-V 通过 STRICT_KERNEL_RWX 配置项实现这一安全策略。

实现机制:

  • 内核代码段(.text)映射为 R+X(可读可执行)
  • 只读数据段(.rodata)映射为 R(只读)
  • 普通数据段(.data.bss)映射为 R+W(可读可写)
  • pgprot_from_va() 函数根据虚拟地址所属区间返回对应的页表权限属性
  • free_initmem() 在释放 __init 段内存前,先将其权限设置为 RW+NX(可写不可执行),防止释放后的页面残留可执行权限

此概念在所有体系结构上完全一致:x86_64 使用 PTE 的 RW/NX 位,ARM64 使用 PXN/UXN 位,RISC-V 使用 PTE 的 R/W/X 位组合(RWX=000 表示非叶子页表项,叶子页表项至少有一位为 1)。

6.7.3 用户态控制流完整性(CONFIG_RISCV_USER_CFI)

RISC-V 通过两个扩展实现完整的控制流完整性保护:

Zicfilp —— 前向 CFI(间接分支验证)

前向 CFI 确保间接分支(如函数指针调用)只能跳转到合法的目标地址。实现方式:

  • 在每个合法的间接分支目标处插入 lpad(landing pad)指令,该指令包含一个标签值(label)
  • 间接分支指令(如 jalr)携带预期标签,硬件在运行时验证目标处的 lpad 标签是否匹配
  • 通过 senvcfg.LPE 位在 S-mode 下启用此功能
  • 不匹配时触发软件检查异常

Zicfiss —— 后向 CFI(影子栈)

后向 CFI 保护函数返回地址不被篡改:

  • CSR_SSP 寄存器持有影子栈指针
  • 函数调用(call)时,硬件自动将返回地址压入影子栈
  • 函数返回(ret)时,硬件自动从影子栈弹出返回地址并与栈上的返回地址比对
  • 不匹配时触发异常
  • 通过 SBI FWCT(Firmware Feature)接口启用
  • PAGE_SHADOWSTACK 定义为只写(Write-Only)PTE,确保用户态代码无法读取影子栈内容

架构对比:

  • x86_64:使用 CET(Control-flow Enforcement Technology)影子栈,纯硬件实现,通过 VMCS 支持虚拟化场景。
  • ARM64:使用 PAC(Pointer Authentication Code)对返回地址进行密码学签名,思路完全不同——不使用影子栈,而是通过签名验证指针完整性。
  • RISC-V:Zicfilp 使用 landing pad 标签验证(类似 ARM64 BTI),Zicfiss 使用影子栈(类似 x86_64 CET),综合了两种思路。

6.7.4 Zkr 硬件熵源(CSR_SEED)

Zkr 扩展在 CSR 地址 0x015 处提供 CSR_SEED 寄存器,作为硬件随机数生成器:

  • 每次读取返回 16 位(bits [15:0])硬件随机数据
  • OPS 字段(bits [19:16])指示熵状态:SEED_OPST_ESSEED(entropy is available)表示可读取,其他值表示需要等待
  • 软件需轮询 OPS 字段直到熵可用再读取数据
  • 用于 KASLR 种子、栈金丝雀(stack canary)生成、密钥派生等场景
  • 比软件伪随机数生成器具有更高的不可预测性

架构对比:

  • x86_64:RDRAND 返回硬件随机数(最多 64 位),RDSEED 提供更高熵源的种子值,通过 rdrand_long() 调用。
  • ARM64:TRNG(True Random Number Generator)通过 ARMv8.5 RNG ID 寄存器暴露,或通过 EFI RNG Protocol 获取。
  • RISC-V:Zkr CSR_SEED 每次 16 位,需轮询等待,接口最简洁但吞吐量较低。

6.7.5 Svpbmt(基于页面的内存类型)

Svpbmt 扩展允许在页表项(PTE)中为单个页面指定缓存行为,使用 PTE 的 bits [62:61]:

Bits [62:61] 助记符 含义
00 PBMT 默认(可缓存,按序)
01 NC 非缓存(Non-Cacheable),用于设备 MMIO 区域
10 IO 强序不可缓存(I/O),用于寄存器访问,非幂等
11 保留

架构对比:

  • x86_64:使用 PAT(Page Attribute Table),PAT MSR 定义 8 种内存类型,PTE 通过 PAT/PCD/PWT 三位索引选择。
  • ARM64:使用 MAIR(Memory Attribute Indirection Register)定义多种内存属性,PTE 的 AttrIndx[2:0] 字段索引 MAIR 条目。
  • RISC-V:Svpbmt 仅提供 3 种类型,设计极为精简,满足绝大多数场景需求。

6.7.6 Svnapot(自然对齐的 2 的幂次页面)

Svnapot 扩展支持将连续的 4KB 页面合并为一个 TLB 条目:

  • PTE bit 63 标记为 NAPOT(Naturally Aligned Power-Of-Two)条目
  • 最常用的是 64KB 连续页面(16 个 4KB 页面),仅需一个 TLB 条目映射
  • 相当于透明大页(Transparent Huge Pages)的轻量级实现
  • 减少 TLB 未命中次数,提升大内存区域访问性能

6.7.7 影子调用栈(CONFIG_SHADOW_CALL_STACK)

软件实现的影子调用栈,与 Zicfiss 硬件影子栈不同:

  • 使用 s0/s1 寄存器(由编译器保留)维护独立的返回地址栈
  • 每次函数调用时,编译器插入代码将返回地址保存到影子调用栈
  • 函数返回时,从影子调用栈恢复返回地址,覆盖栈上的返回地址
  • 攻击者即使覆写栈上的返回地址也无法劫持控制流

架构对比:

  • x86_64:CET 硬件影子栈,由 CPU 自动管理,性能开销极低。
  • ARM64:使用 x18 寄存器(编译器保留)实现软件影子调用栈,与 RISC-V 方案原理一致。
  • RISC-V:使用 s0/s1 寄存器,软件方案,与 ARM64 方案高度类似。

6.7.8 Zk* 密码学扩展族

RISC-V 定义了一系列密码学扩展,以模块化方式覆盖不同需求:

扩展 全称 用途
Zkn NIST 算法套件 AES、SHA-256、SHA-512、乘法运算等
Zks ShangMi(国密)算法套件 SM3、SM4 等中国国家标准密码算法
Zkr 熵源 硬件随机数生成(CSR_SEED)
Zkt 常数时间执行 确保密码运算不泄露时序信息
Zbkb 位操作(密码学) 位排列、反转等密码学常用操作
Zbkc 进位乘法 大数乘法加速
Zbkx 交叉排列 用于 AES 等分组密码
Zknd/Zkne/Zknh AES 解密/加密/哈希 NIST AES 和 SHA 加速指令
ZkSED/Zksh SM4/SM3 国密算法加速指令
Zvk* 向量密码学扩展 利用 RISC-V Vector Unit 进行高性能加密运算

Zkn 的组合关系:Zkn = Zbkb + Zbkc + Zbkx + Zknd + Zkne + Zknh
Zks 的组合关系:Zks = Zbkb + Zbkc + ZkSED + Zksh

架构对比:

  • x86_64:AES-NI、SHA-NI、PCLMULQDQ 等独立指令扩展。
  • ARM64:ARMv8 Crypto Extension(AES、SHA1、SHA256、PMULL),ARMv9 引入 SVE 密码学扩展。
  • RISC-V:模块化密码学扩展,允许按需实现,国密(ShangMi)支持是独特优势。

6.8 RISC-V 虚拟化 —— H 扩展与 KVM

RISC-V 通过 H(Hypervisor)扩展实现了完整的硬件虚拟化支持。Linux 7.0 的 KVM/RISC-V 利用 H 扩展提供的两级地址翻译、CSR 委托和虚拟中断机制,构建了高效的虚拟机管理器。本节深入分析其架构设计与实现。

6.8.1 RISC-V 虚拟化架构

H 扩展在 RISC-V 特权级体系中引入了两个新模式:

  • HS-mode(Hypervisor Supervisor):宿主机内核运行的模式,拥有对物理硬件的完全控制
  • VS-mode(Virtual Supervisor):客户机操作系统运行的模式,受限的 S-mode 环境
  • VU-mode(Virtual User):客户机用户态进程运行的模式

两级地址翻译是虚拟化的核心机制:

  1. VS-stage 翻译(第一级):Guest Virtual Address → Guest Physical Address
    • 由客户机 OS 管理,通过 VSATP CSR 配置
    • 客户机 OS 认为自己拥有完整的物理地址空间
  2. G-stage 翻译(第二级):Guest Physical Address → Host Physical Address
    • 由宿主机内核(KVM)管理,通过 HGATP CSR 配置
    • 确保客户机只能访问宿主机分配给它的物理内存

G-stage 页表模式使用 “x4” 变体,客户机物理地址比宿主机宽 2 位:

模式 虚拟地址位数 物理地址位数 页表层级
Sv32x4 34 34 2
Sv39x4 41 56 3
Sv48x4 50 56 4
Sv57x4 59 56 5

“x4” 表示客户机虚拟地址空间是标准模式的 4 倍,因为客户机需要映射自身的内核空间和用户空间。

6.8.2 KVM 初始化(arch/riscv/kvm/main.c)

KVM 在模块初始化阶段完成以下关键检测和配置:

1
2
3
4
5
6
7
kvm_riscv_init()
├── 检测 H 扩展可用性(通过 SBI 接口或 ISA 字符串)
├── 验证 SBI 版本 ≥ 0.2 且支持 RFENCE 扩展
├── 检测 G-stage 页表模式(尝试 Sv57x4 → Sv48x4 → Sv39x4)
├── 初始化 VMID 分配器(用于 G-stage TLB 刷写优化)
├── 初始化 AIA(高级中断架构)虚拟化支持
└── 可选:初始化 NACL(嵌套加速)

VMID 机制:类似于 ASID(Address Space ID),VMID 为每个虚拟机分配唯一标识符,G-stage TLB 条目关联 VMID。切换虚拟机时无需刷写全部 G-stage TLB,只需切换 HGATP 中的 VMID 字段。

SBI RFENCE 扩展:KVM 需要通过 SBI 调用执行 TLB 刷写(hfence 指令),因为某些 hfence 变体需要 M-mode 权限。SBI v0.2 的 RFENCE 扩展提供了标准化的远程 TLB 无效化接口。

6.8.3 VM 执行与世界切换

虚拟机的执行循环由 vcpu_switch.S 汇编代码驱动:

世界切换流程:

  1. 保存宿主机状态:将当前 CPU 的全部 GPR(通用寄存器)和关键 CSR 保存到 kvm_vcpu_arch 结构体
  2. 加载客户机状态:从 kvm_vcpu_arch 恢复客户机的 GPR 和 CSR(包括 VSATPVSTVALVSCAUSE 等)
  3. 进入 VS-mode:执行 sret 指令,CPU 切换到 VS-mode,开始执行客户机代码
  4. VM Exit:客户机执行触发 trap 的操作(如 I/O 访问、异常、外部中断),CPU 自动切换回 HS-mode
  5. 处理退出原因:KVM 分析退出原因,模拟虚拟设备、注入中断或处理异常
  6. 重新进入客户机:更新客户机状态后再次执行世界切换

关键 CSR 包括:HSTATUS(控制 VS-mode 行为)、HCOUNTEREN(控制客户机可访问的性能计数器)、HTVALHTINST(提供触发 trap 的指令编码,用于指令模拟)。

6.8.4 CSR 委托机制

H 扩展允许将特定的 trap 直接委托给客户机处理,避免不必要的 VM Exit:

hedeleg(异常委托):

委托给客户机的异常包括:指令地址不对齐、断点、环境调用(ecall from VS-mode)、指令/数据页错误等。客户机 OS 可以像在真实硬件上一样处理这些异常。

hideleg(中断委托):

委托给客户机的中断包括:VS-mode 的软件中断(VSSI)、定时器中断(VSTI)、外部中断(VSEI)。这些中断通过虚拟中断机制注入到客户机。

6.8.5 中断虚拟化

RISC-V 的高级中断架构(AIA,Advanced Interrupt Architecture)为虚拟化提供了高效的中断管理:

APLIC(Advanced Platform-Level Interrupt Controller):

  • 平台级中断控制器,支持将物理中断路由到特定 hart
  • 支持虚拟化:可以为每个虚拟 CPU 维护虚拟中断状态
  • 中断优先级管理和中断域(domain)隔离

IMSIC(Incoming Message-S signaled Interrupt Controller):

  • 消息信号中断控制器,为每个 CPU 核心提供独立的 MSI 中断接口
  • 支持虚拟化:每个虚拟 CPU 可以拥有独立的虚拟 IMSIC
  • 中断直接注入:物理中断可以由硬件直接路由到虚拟 IMSIC,无需 KVM 介入
  • 支持中断优先级和中断阈值

通过 AIA,高频中断(如网络、存储)可以直接注入客户机,大幅减少 VM Exit 次数。

6.8.6 NACL(嵌套加速)

NACL(Nested Acceleration)是 RISC-V KVM 的独特优化机制:

核心思想:在 KVM(HS-mode)和 SBI 固件(M-mode)之间建立共享内存区域,批量处理 CSR 同步和特权操作。

加速的操作:

  • CSR 批量同步:将多个 CSR 的读写操作合并为一次共享内存操作,减少 SBI 调用次数
  • hfence 加速:TLB 刷写指令通过共享内存批量下发,固件一次性执行
  • sret 加速:客户机返回指令的模拟可以通过共享内存状态直接完成

性能影响:NACL 显著降低了 VM Exit 的处理延迟,特别是对于需要频繁切换 CSR 的场景(如中断密集型工作负载)。这是 RISC-V 虚拟化的独特优势——利用 SBI 固件作为可编程的”微管理器”,实现 x86/ARM64 无法直接复制的优化路径。

6.8.7 架构对比

特性 x86_64 ARM64 RISC-V
虚拟化扩展 VT-x/VT-d VHE/nVHE H 扩展
客户机模式 VMX non-root EL1(virtual) VS-mode / VU-mode
嵌套页表 EPT(Extended Page Table) Stage-2 页表 G-stage(Sv39x4/Sv48x4/Sv57x4)
状态管理 VMCS(64KB 内存结构) VCPU context(内存结构) CSR swap(寄存器直接切换)
指令模拟 VMCS 字段 + 位图 HSR/ESR syndrome HTVAL/HTINST(指令编码)
I/O 虚拟化 VT-d / IOMMU SMMU IOMMU(开发中)
加速机制 VPIDs(Virtual Processor IDs) 不需要额外机制 NACL(共享内存批处理)

关键差异分析:

  • x86_64 的 VMCS 是一个巨大的状态结构,保存了数百个字段,VM Entry/Exit 开销大。EPT 是成熟的嵌套页表方案,配合 VPID 减少 TLB 刷写。
  • ARM64 的虚拟化设计较为简洁,Stage-2 页表与普通页表结构一致,利用 EL2 的系统寄存器实现 trap 控制。
  • RISC-V 的 H 扩展设计最为轻量:CSR 直接用于状态管理,世界切换仅需保存/恢复有限数量的寄存器。NACL 机制利用 SBI 固件的可编程性实现批量操作加速,是独到的设计。

6.9 三大体系结构对比 —— x86_64 vs ARM64 vs RISC-V

Linux 内核同时支持数十种体系结构,其中 x86_64、ARM64 和 RISC-V(64 位)代表了三种截然不同的设计哲学。本节从架构基础、启动流程、内存管理、安全机制和虚拟化五个维度进行全面对比,帮助读者理解不同架构的设计取舍及其对内核实现的影响。

6.9.1 体系结构基础对比

特性 x86_64 ARM64 (AArch64) RISC-V 64 (RV64)
ISA 类型 CISC(复杂指令集) RISC(精简指令集) RISC(精简指令集)
指令长度 1-15 字节,变长 4 字节定长(+ 可选 2/4 字节 Thumb) 4 字节定长(+ 可选 2 字节压缩)
通用寄存器数量 16(rax-r15) 31(x0-x30) 31(x0-x30)
特权级模型 Ring 0-3(4 级) EL0-EL3(4 级) U/S/M + VS/VU(基础 3 级 + 虚拟化 2 级)
页表基址寄存器 CR3(单一,共享用户/内核) TTBR0 + TTBR1(双份,分离用户/内核) SATP(单一,内核在高地址,用户在低地址)
系统调用指令 SYSCALL / SYSRET SVC / ERET ecall / sret
ISA 授权模式 Intel 专有,封闭 ARM 授权许可,需付费 开放标准,免授权费
中断控制器 APIC / IOAPIC(局部 + I/O) GIC(Generic Interrupt Controller) PLIC / APLIC + SBI IPI

关键差异解读:

  • 特权级:x86_64 的 Ring 1-2 在 Linux 中未使用,实际只使用 Ring 0(内核)和 Ring 3(用户)。ARM64 的 EL2 用于虚拟化/hypervisor,EL3 用于 Secure Monitor。RISC-V 的 M-mode 专用于固件(OpenSBI),S-mode 运行内核,U-mode 运行用户态,H 扩展增加 VS/VU-mode。
  • 页表基址:ARM64 的双 TTBR 设计天然隔离用户态和内核态地址空间,切换时无需刷写整个 TLB。x86_64 和 RISC-V 使用单一页表基址寄存器,依赖 KPTI 等机制实现隔离。
  • ISA 开放性:RISC-V 是三大架构中唯一完全开放的 ISA,任何人可免费实现,这是其在嵌入式和定制芯片领域快速发展的关键动力。

6.9.2 启动流程对比

阶段 x86_64 ARM64 RISC-V
固件 BIOS / UEFI UEFI(+ TF-A/OP-TEE) OpenSBI(M-mode 固件)
引导加载器 GRUB2 U-Boot / GRUB2 U-Boot
模式转换 Real → Protected → Long mode EL2 → EL1 M-mode → S-mode(固件 → 内核)
内核解压 自解压(内嵌解压代码) 引导加载器解压 引导加载器解压
硬件描述 E820 表(BIOS)/ EFI 内存映射 DTB / ACPI DTB / ACPI
内核入口点 startup_64 primary_entry _start
内核参数传递 RSI = boot_params 指针 X0 = FDT 指针 a0 = hartid, a1 = FDT 指针
SMP 启动 SIPI(Startup IPI)唤醒 AP PSCI(Power State Coordination) SBI HSM(Hart State Management)

关键差异解读:

  • x86_64 启动最为复杂,需要从 16 位实模式逐步过渡到 64 位长模式,承载了 40 余年的历史包袱。
  • ARM64 通过 PSCI 标准化了 CPU 电源管理接口,多核启动相对简洁。
  • RISC-V 的 M-mode 固件(OpenSBI)承担了硬件初始化和 SBI 接口实现,内核仅运行在 S-mode,职责划分最为清晰。SBI HSM 接口让多核启动逻辑统一且简洁。

6.9.3 内存管理对比

特性 x86_64 ARM64 RISC-V
虚拟地址位数 48(默认)/ 57(5 级页表) 39-52(可配置) 39(Sv39)/ 48(Sv48)/ 57(Sv57)
页面大小 4KB / 2MB / 1GB 4KB / 16KB / 64KB 4KB(2MB/1GB 通过 THP)
页表层级 4 级(默认)/ 5 级 2-5 级(取决于配置) 3/4/5 级
不可执行位 Bit 63(NX 位) UXN / PXN 位 RWX=000 为非叶子;叶子至少一位为 1,三位组合编码权限
脏位(Dirty) 硬件 D 位 DBM(可选硬件) D 位(软件管理,或 Svdau 扩展硬件自动设置)
访问位(Accessed) A 位(硬件自动设置) AF 位(硬件或软件) A 位(软件管理,由内核维护)
内存类型控制 PAT(Page Attribute Table) MAIR + AttrIndx Svpbmt bits [62:61]
地址空间标识 PCID(在 CR3 中编码) ASID(在 TTBR0/1 中编码) ASID(在 SATP 中编码)

关键差异解读:

  • 脏位管理:x86_64 硬件自动设置脏位,内核无需干预。ARM64 通过 DBM(Dirty Bit Modifier)可选硬件脏位。RISC-V 默认软件管理脏位,内核在写保护时手动设置,Svdau 扩展提供硬件自动脏位。
  • 访问位管理:类似地,x86_64 硬件自动管理访问位,RISC-V 默认需要软件管理——内核需要在页错误处理程序中手动设置 A 位。
  • 内存类型:x86_64 的 PAT 最为灵活(8 种类型),ARM64 的 MAIR 居中,RISC-V 的 Svpbmt 最精简(3 种类型)。这种差异反映了各架构的设计哲学:x86_64 追求兼容性和灵活性,ARM64 追求平衡,RISC-V 追求极简。

6.9.4 安全机制对比

特性 x86_64 ARM64 RISC-V
前向 CFI KCFI(编译器插入类型检查) BTI(Branch Target Identification,硬件) Zicfilp(landing pad 标签,硬件)
后向 CFI CET 影子栈(硬件自动管理) PAC(返回地址密码学签名) Zicfiss(影子栈,CSR_SSP)
内存标记 MTE(Memory Tagging Extension) 无(尚未标准化)
KPTI CR3 切换(内核页表隔离) TTBR0 切换 SUM 位管理
特权访问保护 SMAP/SMEP SCTLR.PAN / SCTLR.SPAN sstatus.SUM(语义相反:SUM=1 允许内核访问用户)
硬件随机数 RDRAND / RDSEED TRNG(ARMv8.5 RNG) Zkr CSR_SEED(16 位/次)

关键差异解读:

  • 后向 CFI 策略差异最大:x86_64 和 RISC-V 采用影子栈方案(在独立栈中保存返回地址),ARM64 采用指针签名方案(用密钥对返回地址签名)。影子栈方案安全性高但需要额外内存和栈管理;PAC 方案开销更低但依赖密钥保护。
  • PAN 语义差异:ARM64 的 PAN(Privileged Access Never)=1 表示禁止内核访问用户内存。RISC-V 的 SUM(Supervisor User Memory)=1 表示允许内核访问用户内存——语义恰好相反,但效果一致。
  • 内存标记(MTE):ARM64 独有的 MTE 功能为每个 16 字节内存块分配 4 位标记,硬件自动检查指针标记与内存标记是否匹配。这是三大架构中唯一的硬件内存安全扩展,对防御内存越界和 UAF 漏洞极为有效。

6.9.5 虚拟化对比

特性 x86_64 ARM64 RISC-V
虚拟化扩展 VT-x / VT-d VHE / nVHE H 扩展
客户机模式 VMX non-root VS-mode / VU-mode(EL1 virtual) VS-mode / VU-mode
嵌套页表 EPT(Extended Page Table) Stage-2 页表 G-stage(Sv39x4 / Sv48x4 / Sv57x4)
状态管理 VMCS(64KB 内存结构,数百字段) VCPU context(内存结构) CSR 直接切换(少量寄存器)
I/O 虚拟化 VT-d / IOMMU SMMU(System MMU) IOMMU(开发中)
加速优化 VPID / VPET 不需要额外机制 NACL(共享内存批量 CSR 同步)
状态保存/恢复 VMREAD/VMWRITE(逐字段) 直接读写 sysreg CSR 读写(与普通寄存器操作一致)

关键差异解读:

  • x86_64 的 VMCS(Virtual Machine Control Structure)是一个约 64KB 的内存结构,包含数百个控制字段。VM Entry/Exit 时 CPU 自动保存/恢复状态到 VMCS,开销较大。EPT 是成熟的嵌套页表方案,4 级页表支持 4KB 到 1GB 的页面映射。
  • ARM64 的虚拟化利用 EL2 提供硬件支持,Stage-2 页表与 Stage-1 结构完全一致,实现简洁。VHE(Virtualization Host Extension)允许宿主机内核运行在 EL2,减少模式切换开销。
  • RISC-V 的 H 扩展最为精简:没有庞大的状态描述符,所有虚拟化状态通过少量 CSR 管理。世界切换只需保存/恢复十几个 CSR,开销最小。NACL 机制利用 SBI 固件实现批量 CSR 同步,进一步降低 VM Exit 延迟。

6.9.6 设计哲学与生态

x86_64 —— 历史包袱下的极致性能

x86_64 承载了从 8086 至今 40 余年的兼容性要求,导致其指令集极其复杂(指令长度从 1 到 15 字节不等,数千条指令),特权级模型冗余(Ring 1-2 实际未使用),启动流程繁琐。但这种复杂性换来的是极其成熟和优化的微架构实现,在 PC 和服务器市场占据绝对主导地位。Intel 和 AMD 通过持续扩展指令集(AVX-512、AMX、CET 等)保持竞争力。

ARM64 —— 干净设计下的广泛渗透

ARM64(AArch64)是 ARM 在 32 位 ARMv7 之后全新设计的 64 位 ISA,摆脱了历史包袱。统一的 4 字节指令编码、清晰的特权级模型(EL0-EL3)、标准化的虚拟化支持使其成为移动和嵌入式领域的绝对霸主。近年来通过 Neoverse 系列在服务器市场持续扩张。ARM 的授权商业模式虽然限制了自由度,但确保了软件生态的高度一致性。

RISC-V —— 开放标准下的快速增长

RISC-V 是三大架构中唯一完全开放的 ISA,任何人可以免费实现和扩展。其设计哲学是”最小基础集 + 模块化扩展”:基础的 RV64I 仅包含约 40 条指令,所有高级功能(浮点、向量、密码学、虚拟化等)都作为可选扩展实现。这种设计使得 RISC-V 可以从最低端的微控制器覆盖到最高端的 HPC 处理器,是三大架构中适用范围最广的。国密(ShangMi)扩展 Zks 的标准化支持也体现了 RISC-V 对全球多样化需求的包容性。

  • Title: Linux内核分析之基础知识-05
  • Author: 韩乔落
  • Created at : 2026-05-28 18:16:20
  • Updated at : 2026-05-28 18:40:02
  • Link: https://jelasin.github.io/2026/05/28/Linux内核分析之基础知识-05/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
Linux内核分析之基础知识-05