diff --git a/articles/20220319-riscv-uefi.md b/articles/20220319-riscv-uefi.md new file mode 100644 index 0000000000000000000000000000000000000000..a1111a39b9152bd1f99d99e595aaa294299c7dd8 --- /dev/null +++ b/articles/20220319-riscv-uefi.md @@ -0,0 +1,435 @@ +> Author: Jacob Wang
+> Date: 2022/03/19
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux) +> + +# RISC-V UEFI 架构支持详解,第 1 部分 - OpenSBI/U-Boot/UEFI 简介 + +从 [邮件列表](https://lore.kernel.org/linux-riscv) 中找到了一笔 patchset:[adds UEFI support for RISC-V](https://lore.kernel.org/linux-riscv/20200917223716.2300238-1-atish.patra@wdc.com/)。该 patchset 实现 RISC-V 如下引导启动支撑: + +``` +Qemu (both RV32 & RV64) for the following bootflow + OpenSBI->U-Boot->Linux + EDK2->Linux + +HiFive unleashed using (RV64) for the following bootflow + OpenSBI->U-Boot->Linux + EDK2->Linux +``` +本文尝试对 OpenSBI,U-Boot 和 UEFI 的相关概念以及代码进行基本的解析。 + +## OpenSBI + +OpenSBI 项目致力于为 RISC-V 平台特有固件提供 RISC-V SBI 规范的开源参考实现,这类固件运行在 RISC-V M 模式。 + +### OpenSBI 加载过程涉及到的相关概念 + +RISC-V 架构下有 3 种特权级别,分别是 Machine、Supervisor 和 User,简称 M 模式、S 模式和 U 模式。 + +- M 模式权限最高,在这个级别下的程序可以访问一切硬件和执行所有特权指令; +- S 模式一般用于运行操作系统,可以设置 MMU 使用虚拟地址; +- U 模式一般是普通应用程序使用,权限最低。 + +### OpenSBI 加载过程 + +OpenSBI 会经历底层初始化阶段,该阶段主要是准备 C 执行环境;然后在布置好的 C 环境执行设备初始化;最后实现二级引导的跳转。 + +以下以 [OpenSBI](https://github.com/riscv/opensbi.git) v1.0 版本为例,开展源码分析如下。 + +1 OpenSBI 底层初始化实现在 `/firmware/fw_base.S`, 大体流程如下: + +``` +_start: + /* Find preferred boot HART id */ # 判断 HART id + MOV_3R s0, a0, s1, a1, s2, a2d + call fw_boot_hart + add a6, a0, zero + MOV_3R a0, s0, a1, s1, a2, s2 + li a7, -1 + beq a6, a7, _try_lottery + /* Jump to relocation wait loop if we are not boot hart */ + bne a0, a6, _wait_relocate_copy_done +_try_lottery: + + /* Reset all registers for boot HART */ # 清除寄存器值 + li ra, 0 + call _reset_regs + + /* Zero-out BSS */ # 清除 BSS 段 + lla s4, _bss_start + lla s5, _bss_end +_bss_zero: + REG_S zero, (s4) + add s4, s4, __SIZEOF_POINTER__ + blt s4, s5, _bss_zero + + /* Setup temporary stack */ # 设置 SP 栈指针 + lla s4, _fw_end + li s5, (SBI_SCRATCH_SIZE * 2) + add sp, s4, s5 + + /* + * Initialize platform + * Note: The a0 to a4 registers passed to the + * firmware are parameters to this function. + */ + MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4 + call fw_platform_init # 读取设备树中的设备信息 + + # FDT 重定位 + /* + * Relocate Flatened Device Tree (FDT) + * source FDT address = previous arg1 + * destination FDT address = next arg1 + * + * Note: We will preserve a0 and a1 passed by + * previous booting stage. + */ + + ... + + /* Initialize SBI runtime */ + csrr a0, CSR_MSCRATCH + call sbi_init # 底层关键初始化结束,跳转到 sbi_init + ... +``` + +**进入 sbi_init 会首先判断是通过 S 模式还是 M 模式启动** + +2 OpenSBI 设备初始化 + +进入 `sbi_init`,执行 `init_coldboot`, `init_coldboot` 实现在 `/lib/sbi/sbi_init.c`,大体初始化工作如下: + +``` +static void __noreturn init_coldboot(struct sbi_scratch *scratch, u32 hartid) +{ + ... + + # 初始化动态加载的镜像的模块 + /* Note: This has to be second thing in coldboot init sequence */ + rc = sbi_domain_init(scratch, hartid); + + ... + # 平台的早期初始化 + rc = sbi_platform_early_init(plat, TRUE); + + # 控制台初始化,从这里开始,就可以使用串口输出 + rc = sbi_console_init(scratch); + + # 核间中断初始化 + rc = sbi_ipi_init(scratch, TRUE); + + # MMU 的 TLB 表的初始化 + rc = sbi_tlb_init(scratch, TRUE); + + # Timer 初始化 + rc = sbi_timer_init(scratch, TRUE); + sbi_printf("%s: PMP configure failed (error %d)\n", + __func__, rc); + sbi_hart_hang(); + } + + # 准备下一级的 boot + sbi_hsm_prepare_next_jump(scratch, hartid); +``` + +3 OpenSBI 二级 boot 的跳转 + +## U-Boot + +U-Boot 是一种流行的嵌入式 Linux 系统引导加载程序。 + +### U-Boot 加载过程涉及的相关概念 + +- SRAM + + 掉电易失(失去电源供电后 SRAM 里存储的数据不存在了),可以随意 **读写** 数据。(容量小,程序运行速度快,价格高,一般在 SoC 里面) + +- SDRAM + + 掉电易失(失去电源供电后 SDRAM 里存储的数据不存在了),上电后没有初始化 DDR 控制器,无法进行数据读写。 + +- SPL + + U-Boot 分为 uboot-spl 和 uboot 两个组成部分。SPL 是 Secondary Program Loader 的简称,第二阶段程序加载器。 + +这里所谓的第二阶段是相对于 SOC 中的 SRAM 来说的,SOC 启动最先执行的是的固化程序。ROM 会通过检测启动方式来加载第二阶段 bootloader。 + +U-Boot 已经是一个 bootloader 了,那么为什么还多一个 U-Boot SPL 呢? + +主要原因是对于一些SOC来说,它的内部 SRAM 可能会比较小,小到无法装载下一个完整的 U-Boot 镜像,那么就需要 SPL。它主要负责初始化外部 RAM 和环境,并加载真正的 U-Boot 镜像到外部 RAM 中来执行。所以由此来看,SPL 应该是一个非常小的 loader 程序,可以运行于 SOC 的内部 SRAM 中,它的主要功能就是加载真正的 U-Boot 并运行。 + +### U-Boot 加载过程 + +嵌入式系统的 SOC 内部会有比较小的 SRAM,而外部的一般会有 DDR 或者 SDRAM,后面的 RAM 就是外部 RAM; +SPL 会先被加载到 SRAM 中,然后初始化 DDR 或者 SDRAM,总之会初始化外部的 RAM,然后再把主 U-Boot 加载到 RAM; + +如下图所示: + + + +上图解释如下: + +- 图中(1)是 SPL 在 U-Boot 第一阶段的装载程序,初始化最基本的硬件,比如关闭中断,内存初始化,设置堆栈等最基本的操作,设置重定位; +- 图中(2)是会装载主 U-Boot 程序,然后初始化其他板级硬件,比如网卡,NAND Flash 等待,设置 U-Boot 本身的命令和环境变量; +- 图中(3)是加载 Kernel 到 RAM,然后启动内核。 + +``` ++--------+----------------+----------------+----------+ +| Boot | Terminology #1 | Terminology #2 | Actual | +| stage | | | program | +| number | | | name | ++--------+----------------+----------------+----------+ +| 1 | Primary | - | ROM code | +| | Program | | | +| | Loader | | | +| | | | | +| 2 | Secondary | 1st stage | U-Boot | +| | Program | bootloader | SPL | +| | Loader (SPL) | | | +| | | | | +| 3 | - | 2nd stage | U-Boot | +| | | bootloader | | +| | | | | +| 4 | - | - | kernel | +| | | | | ++--------+----------------+----------------+----------+ +``` + +所以通常启动顺序是:ROM code --> SPL --> u-boot --> kernel。 + +### U-Boot 代码分析 + +U-Boot 其启动过程主要可以分为两个部分,Stage1 和 Stage2 。其中 Stage1 是用汇编语言实现的,主要完成硬件资源的初始化。而 Stage2 则是用 C 语言实现。 + +以下以 [U-boot](https://github.com/u-boot/u-boot) v2022.04 版本为例,开展源码分析如下。 + +第一阶段路径位于 `arch/riscv/cpu/start.S` 文件。 + +第二阶段启动阶段中, 会重点涉及到 位于 `common/board_r.c` 的 board_init_f 函数和位于 `common/board_r.c` 文件的的 board_init_r 函数。board_init_f 会初始化必要的板卡和 global_data 结构体,然后调用 board_init_r 进行下一阶段的板卡初始化工作。board_init_r 运行结束之后, 会调用位于 `common/main.c` 的 main_loop() 函数进行。来重点看一下这三个函数。 + +- board_init_f 分析 + +``` +static const init_fnc_t init_sequence_f[] = { + ... + setup_mon_len, + initf_bootstage, /* uses its own timer, so does not need DM */ + event_init, + ... + setup_spl_handoff, + arch_cpu_init, /* basic arch cpu dependent setup */ + mach_cpu_init, /* SoC/machine dependent CPU setup */ + initf_dm, + ... + env_init, /* initialize environment */ + init_baud_rate, /* initialze baudrate settings */ + serial_init, /* serial communications setup */ + console_init_f, /* stage 1 init of console */ + display_options, /* say that we are here */ + display_text_info, /* show debugging info if required */ + checkcpu, + ... + INIT_FUNC_WATCHDOG_RESET + /* + * Now that we have DRAM mapped and working, we can + * relocate the code and continue running from DRAM. + * + * Reserve memory at end of RAM for (top down in that order): + * - area that won't get touched by U-Boot and Linux (optional) + * - kernel log buffer + * - protected RAM + * - LCD framebuffer + * - monitor code + * - board info struct + */ + setup_dest_addr, + ... + do_elf_reloc_fixups, + ... + clear_bss, +}; + +void board_init_f(ulong boot_flags) +{ + ... + if (initcall_run_list(init_sequence_f)) + hang(); + ... +} +``` + +- board_init_r 函数 + +``` + +static init_fnc_t init_sequence_r[] = { + initr_trace, + ... + initr_caches, # 使能 cache 接口 + initr_reloc_global_data, # 设置 monitor_flash_len + ... + initr_barrier, + initr_malloc, + log_init, + initr_bootstage, /* Needs malloc() but has its own timer */ + ... + board_init, /* Setup chipselects */ + ... + pci_init, + ... + cpu_init_r, + ... +}; + +# board_init_r 主要执行 init_sequence_r 中一系列初始化函数 + +void board_init_r(gd_t *new_gd, ulong dest_addr) +{ + ... + if (initcall_run_list(init_sequence_r)) + hang(); + ... +} +``` + +- main_loop 函数 + +``` +void main_loop(void) +{ + const char *s; + + bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop"); + + # 设置 U-boot 的版本号 + + if (IS_ENABLED(CONFIG_VERSION_VARIABLE)) + env_set("ver", version_string); /* set version variable */ + + # 命令初始化,初始化 shell 相关的变量 + cli_init(); + + # 获取环境变量 perboot 的内容 + if (IS_ENABLED(CONFIG_USE_PREBOOT)) + run_preboot_environment_command(); + + ... + + # 读取环境变量 bootdelay 和 bootcmd 的内容 + # 然后将 bootdelay 的值赋值给全局变量 stored_bootdelay + + s = bootdelay_process(); + if (cli_process_fdt(&s)) + cli_secure_boot_cmd(s); + + # 检查倒计时是否结束 + + autoboot_command(s); + + # 命令处理函数,负责接收好处理输入的命令 + cli_loop(); + panic("No CLI available"); +} +``` + +## UEFI + +大概 20 多年的发展和积累中,UEFI 的代码量已经很庞大,相关标准白皮书也有很多 +> 1999 年:EFI 1.0 推出 +> 2000 年:EFI 1.02 发布 +> 2002 年:EFI 1.10 发布 +> 2006 年:UEFI 2.0 发布 +> 2007 年:UEFI 2.1 +> 2008 年:UEFI 2.2 + + +剥离开技术细节,引导软件基本做的就是初始化硬件和提供硬件的软件抽象,并完成引导启动。 + +启动阶段搞来搞去基本就三个步骤: + +1. Rom 阶段 + + 该阶段没有内存,没有 C 语言运行需要的栈空间,开始往往是汇编语言,直接在 ROM 空间上运行。在找到临时空间后( Cache As Ram, CAR),C 语言可以使用,然后开始用 C 语言初始化内存。 + +2. Ram 阶段 + + 这个阶段有大内存可以使用。开始会初始化芯片组、CPU、主板模块等等核心过程。 + +3. 引导 OS 阶段 + + 枚举设备,发现启动设备,并把启动设备之前需要依赖的节点统统打通。移交工作,Windows 或者 Linux 的时代开始。 + +### UEFI 与硬件及 OS 的关系 + + + +EFI 在过程中会提供运行时服务和 EFI 引导时服务; +(引导时服务只在 boot time 可用,runtime service 在引导后面的 OS 后还是可以继续被使用的) + +如上图所示,EFI 在过程中会提供运行时服务和 EFI 引导时服务,其中引导时服务只在 boot time 可用,runtime service 在引导后面的 OS 后还是可以继续被使用的。 + +下图是基本的引导过程,文章后面会更详细地展开这部分。 + + + +### UEFI 的引导流程 + +UEFI 启动过程遵循 UEFI 平台初始化(Platform Initialization)标准,共经历了七个阶段。 + + + +7 个阶段分别为 `SEC (Security)`,`PEI (Pre-EFi Initalization)`,`DXE (Driver Execution Environment)`,`BDS (Boot Device Selection)`,`TSL (Transient System Load)`,`RT (Runtime)`,`AL (After Life)`。前三个阶段是 UEFI 初始化阶段。 下面介绍一下每个阶段的主要任务; + +- SEC 阶段 + + 处理系统上电或重启;创建临时内存;提供安全信任链的根;传送系统参数到下一阶段。 + +- PEI 阶段 + + 依次进行平台的初始化,如 CPU,内存控制器,I/O 控制器,以及各种平台接口。 + +- DXE 阶段 + + 该阶段执行系统初始化工作,为后续 UEFI 和操作系统提供了 UEFI 系统表、启动服务和运行时服务。 + +- BDS 阶段 + + 复现每个启动设备,并执行启动策略。如果 BDS 启动失败,系统会重新掉用 DXE 派遣器,再次进入寻找启动设备的流程。 + +- TSL 阶段 + + 临时系统,是 UEFI 将系统控制权转移给操作系统前的一个中间状态。 + +- RT 阶段 + + UEFI 各种系统资源被转移,启动服务不能再使用,仅保留运行时服务供操作系统使用。 + +- AL 阶段 + + 这个阶段的功能一版有厂商自定义。 + +### EDK2 + +EDK2 是一个现代、功能丰富的跨平台固件开发环境,适用于 UEFI 相应编译和调试工作。在 Linux 平台上,可以使结合 QEMU+GDB 进行相应调试。 + +## 引导程序生态系统 + +引导程序生态系统除了 UEFI,还有 Coreboot/Libreboot 等组件。各个组件的组合和流动很灵活。 + + + +### Coreboot 引导阶段与 UEFI 对比 + +UEFI 的 PEI 和 DXE 阶段,分别对应 Coreboot 的 romstage 和 ramstage 阶段。 + +下图比较形象的对比了两者的区别: + + + +## 参考文档 +* [UEFI SPEC](https://uefi.org/sites/default/files/resources/UEFI_Spec_2_9_2021_03_18.pdf) +* [riscv-uefi-edk2-docs](https://github.com/riscv-admin/riscv-uefi-edk2-docs) +* [coreboot architecture](https://doc.coreboot.org/getting_started/architecture.html) +* [SPL](https://stackoverflow.com/questions/31244862/what-is-the-use-of-spl-secondary-program-loader) +* [Open-source Firmware and Bootloaders](http://lastweek.io/notes/source_code/firmware-softwares/) diff --git a/articles/images/riscv_uefi/boot_system.png b/articles/images/riscv_uefi/boot_system.png new file mode 100644 index 0000000000000000000000000000000000000000..675b7fb32dd41e9c19d6c8bf0323a6bffcd3f002 Binary files /dev/null and b/articles/images/riscv_uefi/boot_system.png differ diff --git a/articles/images/riscv_uefi/coreboot.png b/articles/images/riscv_uefi/coreboot.png new file mode 100644 index 0000000000000000000000000000000000000000..8c3c37a440dd8b7c14d2ea9399460d266b163af3 Binary files /dev/null and b/articles/images/riscv_uefi/coreboot.png differ diff --git a/articles/images/riscv_uefi/u-boot-v2.png b/articles/images/riscv_uefi/u-boot-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..bd7e638226345100064858eaeaab43439a911778 Binary files /dev/null and b/articles/images/riscv_uefi/u-boot-v2.png differ diff --git a/articles/images/riscv_uefi/uefi_boot.png b/articles/images/riscv_uefi/uefi_boot.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec7cf0a299a6ed045fc96a885a14e41f4498ce5 Binary files /dev/null and b/articles/images/riscv_uefi/uefi_boot.png differ diff --git a/articles/images/riscv_uefi/uefi_general.png b/articles/images/riscv_uefi/uefi_general.png new file mode 100644 index 0000000000000000000000000000000000000000..5d65887514681040184f11a9d7405ecf9125afe4 Binary files /dev/null and b/articles/images/riscv_uefi/uefi_general.png differ diff --git a/articles/images/riscv_uefi/uefi_pi.png b/articles/images/riscv_uefi/uefi_pi.png new file mode 100644 index 0000000000000000000000000000000000000000..9aca168a2223f30b1da199381243ca4302ff5261 Binary files /dev/null and b/articles/images/riscv_uefi/uefi_pi.png differ