泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

即插即跑:Linux Lab Disk开创了3种智能启动方式
请稍侯

RISC-V UEFI 架构支持详解,第 1 部分 - OpenSBI/U-Boot/UEFI 简介

Jacob Wang 创作于 2022/05/10

Author: Jacob Wang jiangbo.jacob@outlook.com Date: 2022/03/19 Project: RISC-V Linux 内核剖析

前言

邮件列表 中找到了一笔 patchset:adds UEFI support for RISC-V。该 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 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 模式启动

  1. 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();
        }

        # 准备下一级的 Bootloader
        sbi_hsm_prepare_next_jump(scratch, hartid);
  1. OpenSBI 二级 Bootloader 的跳转

init_coldboot 的最后,会跳转到二级 Bootloader,并切换到 S 模式。

static void __noreturn init_coldboot(struct sbi_scratch *scratch, u32 hartid)
{
    sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
                              scratch->next_mode, FALSE);
}

关于 sbi_hart_switch_mode() 的具体实现以及 next_arg1, next_addr 等的设定,可能需要分析 lib/sbi/sbi_hsm.c,待后续再结合 qemu 调试来详细分解。

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 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 阶段。

下图比较形象的对比了两者的区别:

参考文档



Read Album:

Read Related:

Read Latest: