0%

Linux RISC-V Boot 流程

硬件初始化

两种启动方式:

  1. 定制化的主板(常见的 RISC-V 开发板,通常不再扩展其他设备)
  2. 通用主板(常见如 PC,通常需要再插入其他设备)

嵌入式设备启动

  • 需要初始化具体主板相关硬件如 GPIO 和内存等
  • devicetree 获知有哪些设备
  • 操作系统中需要很多设备相关的驱动
  • 通常与设备强相关
    • ROM 通常是闭源的
    • 早期启动流程通常是厂商提供的代码
  • 缺点:对可插拔外设的兼容性
    • 如 RISC-V 嵌入式设备支持 PCIe 外设,需要操作系统内核包含具体硬件上的 PCIe 控制器驱动

RISC-V嵌入式平台常见组合:

  • ROM code(在ROM):主板厂商私有
  • Bootloader(在SD卡或 SPI flash):如u-boot(非必须)
  • Linux kernel(在SD卡)

以常用的基于 U-Boot 和 OpenSBI 的启动流程为例:

  1. 从 ROM 中开始执行代码
    • SoC 芯片内部包含一块 ROM 保存了一些简单的驱动
    • 从 SD 卡或 flash 加载 U-Boot SPL 到一块小内存中(单独的 SRAM 或 L2 cache-as-RAM)
  2. U-Boot SPL 进行最早的初始化
    • DDR 内存、时钟等最重要的设备
    • 从 SD 卡加载 OpenSBI 和 U-Boot 本体到 DDR 内存,然后跳转到 OpenSBI 运行
  3. OpenSBI
    • 初始化 IPI 和时钟设备
    • 初始化自身准备好提供 SBI 服务
    • 切换到 Supervisor mode,跳转到 U-Boot
  4. U-Boot
    • 从预先配置的启动设备加载内核到内存(来源可以是 SD 卡、NVMe、网络……)
    • 跳转到内核入口点

设备树 Devicetree


U-Boot 和 OpenSBI 为了方便及复用 devicetree 已经完成的工作,也用 FDT 获得硬件配置:

  • 文本格式 (Devicetree source, DTS) 和二进制格式 (Flattened devicetree, FDT 或 DTB)
  • 包含设备信息、MMIO 地址、中断连接方式、时钟复位电源连接方式等信息
  • 与内核同时加载到内存中,跳转到各软件入口点时传入地址
  • 惯例:a0 是当前核心的 hartid,a1 是内存中 FDT 的物理地址

通用主板启动

  • 系统配置情况在开机时候是不知道的
  • 需要探测(Probe)、Training(内存和PCIe)和枚举(PCIe 等等即插即用设备)
  • UEFI/ACPI 提供了整个主板、包括主板上外插的设备的软件抽象
    通过定义的接口把这些信息传递给 OS,使 OS 不需要改动而能够适配到所有机型和硬件
  • 或者由操作系统通过 ACPI 获取硬件信息
    • 比 devicetree 提供更多的信息,还提供更多接口,如通用的 PCIe 设备访问,热插拔通知,电源管理接口
    • 比 devicetree 复杂
    • 在比较简单的设备上 UEFI 也可以不提供 ACPI 而提供 devicetree

常见组合:

  • BIOS(在ROM):传统BIOS、EFI/UEFI、coreboot
  • Bootloader(在磁盘的MBR):NTLDR、Grub
  • Linux kernel(在磁盘其他位置)

RISC-V UEFI

  • 提供 PCIe 控制器的驱动,可以以通用方式访问 PCIe 设备
  • 提供 PCIe Option ROM 支持,在早期启动时就可以用上外部设备的功能(显卡的显示功能、网卡的网络启动功能……)

内核代码

以 linux 6.10.3 为例

链接脚本: arch/riscv/kernel/vmlinux.lds.S:

  • 启动入口:_startLOAD_OFFSETKERNEL_LINK_ADDR
    • KERNEL_LINK_ADDR0xFFFF_FFFF_8000_0000
    • 内核启动时的 CPU 复位值为 0x4020_0000
  • HEAD_TEXT_SECTION 定义在 include/asm-generic/vmlinux.lds.h
    • HEAD_TEXT 中的代码从 .head.text 节开始

boot into kernel: arch/riscv/kernel/head.S

  1. _start
    • _HEAD : 定义于 include/linux/init.h, 表示 .section ".head.text","ax"
    • 跳转到 _start_kernel
  2. _start_kernel
    1. 禁用中断: 清空相应特权级下的 IE 和 IP CSR (mie/sie, mip/sip)
    2. 对于特权级 M : …
    3. _global_pointer 加载到 gp 寄存器
    4. 禁用浮点和向量单元: 相应特权级下的 status CSR 的 FS 和 VS 域置 0
    5. 如果配置了 CONFIG_RISCV_BOOT_SPINWAIT: …
    6. 如果配置了 CONFIG_XIP_KERNEL: …
      否则:将 .bss 段清零
    7. 保存启动时的 cpu hartid
    8. 初始化寄存器
      • tp: init_task
      • sp: init_thread_union + THREAD_SIZE
    9. 如果配置了 CONFIG_BUILTIN_DTB: …
    10. 页表和 MMU 初始化
    11. 设置异常向量: .Lsetup_trap_vector
    12. 其他组件初始化
      • KASAN: kasan_early_init()
      • SOC: soc_early_init()
    13. 进入内核:尾调用跳转到 start_kernel

页表和 MMU 初始化

页表初始化

setup_vm 用到两个页表:

  1. trampoline_pg_dir 启用 MMU 前后所用,映射启用 MMU 的代码到高地址
    • 使用 2M 的大页映射内核的起始部分, 作为启动 MMU 的跳板
    • pgd 为第一级页表项, pmd 为 2MiB 大页页表项
1
2
3
4
5
/* Setup trampoline PGD and PMD */
create_pgd_mapping(trampoline_pg_dir, kernel_map.virt_addr,
trampoline_pgd_next, PGDIR_SIZE, PAGE_TABLE);
create_pmd_mapping(trampoline_pmd, kernel_map.virt_addr,
kernel_map.phys_addr, PMD_SIZE, PAGE_KERNEL_EXEC);
  1. early_pg_dir 内核最初启动的时候所用,映射整个内核到高地址
1
2
3
4
5
6
7
8
9
/*
* Setup early PGD covering entire kernel which will allow
* us to reach paging_init(). We map all memory banks later
* in setup_vm_final() below.
*/
create_kernel_page_table(early_pg_dir, true);

/* Setup early mapping for FDT early scan */
create_fdt_early_page_table(__fix_to_virt(FIX_FDT), dtb_pa);

MMU 初始化


in arch/riscv/kernel/head.S

relocate_enable_mmu 启用 MMU,并跳转到高地址继续执行

  • 用 trampoline_pg_dir 衔接
  • 启用 MMU
  • 返回后 pc 在高地址
  1. 计算返回地址:原有 ra 加上偏移量变为高地址
    • 查询 kernel_map 和 _start 之间的地址差作为偏移量
  2. 设置异常处理地址 stvec 为 标签 1 的高地址
    • 之后将通过触发 page fault 的方式切换到高地址运行
  3. 计算 satp
    • 计算 early_pg_dir 对应的 satp 到 a2 寄存器
    • 计算 trampoline_pg_dir 对应的 satp 到 a0 寄存器
  4. 加载 trampoline_pg_dir 页表:sfence.vma 之后将 trampoline_pg_dir(a0) 的 satp 写入 stap CSR
  5. 随后将触发 page fault: 因为下一条指令依旧是低地址,没有映射
    • page fault 后跳转到 stvec 即标签 1 的高地址
  6. 开始以高地址执行标签 1 及之后的指令
    • 标签 1 处代码: 将 stvec 设置到一个死循环的入口(Lsecondary_park),用于调试
  7. 重新加载 gp 寄存器(高地址)
  8. 加载 early_pg_dir 页表:将 early_pg_dir(a2) 的 satp 写入 stap CSR 后 sfence.vma
  9. ret 返回原有调用地址的高地址

异常向量初始化(异常/中断/系统调用)

设置异常向量入口: setup_trap_vector

.Lsetup_trap_vector
in arch/riscv/kernel/head.S

  1. handle_exception 设置到 stvec
    • RISC-V Linux 唯一的异常入口点: handle_exception
  2. 将 sccratch 设置为 0
    • sscratch = 0 说明异常从内核发生
    • sscratch $\neq$ 0 指向一个 task_struct 表示内核从用户态发生
  3. ret 返回

异常处理: handle_exception

in arch/riscv/kernel/entry.S

  1. 检查异常来自内核态还是用户态 (sccratch 是否为 0)
    • 如果来自内核,则恢复内核的 tp 和 sp 寄存器:
      • tp 恢复为 sccratch 中表示的 task_struct 结构体指针
      • sp 恢复为 tp 的 task_struct 对应的内核栈
        如果配置了 CONFIG_VMAP_STACK: …
  2. 保存上下文: .Lsave_context
    • 栈切换:
      • 将 sp 写入 tp 指向的 task_struct 结构体中的用户栈指针
      • 将 tp 指向的 task_struct 结构体中的内核栈指针写入 sp
    • 将除 x0, x2(sp), x4(tp) 寄存器之外的所有 GPR 压栈
    • 将 task_struct 的用户栈指针压栈
    • 将 status, epc, tval, cause, scratch CSR 寄存器压栈
      并清空当前 status 中的 SUM, FS, VS 域 (禁用 U mode 访存和 FPU/Vector Unit)
  3. 将 scratch 寄存器设置为 0 (内核)
    • 防止发生递归异常触发
  4. 加载 gp 寄存器
  5. scs_load_current_if_task_changed s5: ?
  6. 如果配置了 CONFIG_RISCV_ISA_V_PREEMPTIVE: …
  7. ret_from_exception 地址作为返回地址(ra)
  8. 判断是异常还是中断: cause CSR 的 MSb 是否为 1 (1 为中断)
    • 如果是中断:尾调用进入 do_irq 处理中断(do_irq ret 后直接进入 ret_from_exception)
    • 对于异常,则计算 excp_vect_table(异常向量表) 中的第 cause 个 entry 的地址
      检查地址是否超出 excp_vect_table_end
      超出则跳转到 do_trap_unknown
      未超出则跳转到相应的异常处理函数地址

中断处理: do_irq


in arch/riscv/kernel/traps.c

todo…

异常向量表:excp_vect_table

in arch/riscv/kernel/entry.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	.section ".rodata"
.align LGREG
/* Exception vector table */
SYM_DATA_START_LOCAL(excp_vect_table)
RISCV_PTR do_trap_insn_misaligned
ALT_INSN_FAULT(RISCV_PTR do_trap_insn_fault)
RISCV_PTR do_trap_insn_illegal
RISCV_PTR do_trap_break
RISCV_PTR do_trap_load_misaligned
RISCV_PTR do_trap_load_fault
RISCV_PTR do_trap_store_misaligned
RISCV_PTR do_trap_store_fault
RISCV_PTR do_trap_ecall_u /* system call */
RISCV_PTR do_trap_ecall_s
RISCV_PTR do_trap_unknown
RISCV_PTR do_trap_ecall_m
/* instruciton page fault */
ALT_PAGE_FAULT(RISCV_PTR do_page_fault)
RISCV_PTR do_page_fault /* load page fault */
RISCV_PTR do_trap_unknown
RISCV_PTR do_page_fault /* store page fault */
SYM_DATA_END_LABEL(excp_vect_table, SYM_L_LOCAL, excp_vect_table_end)

向量表中的异常处理函数均定义于 arch/riscv/kernel/traps.c.

系统调用: do_trap_ecall_u
  1. 通过上下文检查 ecall 是否来自于 U mode ,否则触发 do_trap_error
  2. 通过上下文获取系统调用参数
    • a7: 系统调用号
    • a0: 系统调用参数
  3. 对系统调用号进行检查和调整:syscall_enter_from_user_mode in entry-common.h
  4. 调用系统调用函数进行处理:syscall_handler in arch/riscv/include/asm/syscall.h
    • sys_call_table 中找到第 syscall 个函数,然后调用
    • sys_call_table 定义于 arch/riscv/kernel/syscall_table.c
    • 其中 0 号系统调用为 __riscv_sys_ni_syscall,其余系统调用通过 _SYSCALL 定义于 arch/riscv/include/asm/unistd.h
    • riscv 特有的系统调用声明于 arch/riscv/include/uapi/asm/unistd.h
    • linux 通用系统调用声明于 include/uapi/asm-generic/unistd.h

异常返回处理: ret_from_exception

in arch/riscv/kernel/entry.S

  1. 将 status CSR 恢复到 s0 寄存器,并设置 PP 特权级位 (MPP/SPP)
  2. 如果 s0 为 0 则:…
  3. 如果配置了 CONFIG_RISCV_ISA_V_PREEMPTIVE: …
  4. 恢复上下文
    1. 将 status , epc CSR 恢复
    2. 将 除 x0, x2(sp) 外的 GPR 恢复
    3. 最后恢复 sp 寄存器(因为恢复的过程本身需要 sp )
  5. trap 返回: mret/sret