diff --git a/articles/20230417-riscv-linux-uefi-boot-1.md b/articles/20230417-riscv-linux-uefi-boot-1.md new file mode 100644 index 0000000000000000000000000000000000000000..4ad910db23b1661ea5aeedfad6e5379d76172096 --- /dev/null +++ b/articles/20230417-riscv-linux-uefi-boot-1.md @@ -0,0 +1,525 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1 - [spaces toc]
+> Author: sugarfillet
+> Date: 2023/04/17
+> Revisor: Falcon falcon@tinylab.org
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [RISC-V UEFI 启动流程分析与 EDK2 移植](https://gitee.com/tinylab/riscv-linux/issues/I64FSG)
+> Sponsor: PLCT Lab, ISCAS + +# RISC-V Linux 内核 UEFI 启动过程分析(Part1)—— 构建、加载与启动内核 + +## 前言 + +现阶段 RISC-V 主要专注于嵌入式领域,供学习和开发用的评估板一般是单板计算机的形式(Single Board Computer),软件方面基本上是依赖半导体厂商发布完整的 SDK,在 bootloader 这方面轻量级的 U-Boot 成为了首选。随着厂商不断提高 RISC-V 的硬件性能,将不可避免地向上进入台式机甚至是服务器领域。届时,RISC-V 需要面对一个成熟的、分散的、玩家众多的和重度依赖生态的市场。而 UEFI 是市场给出的选择和答案,RISC-V 也必须遵守。 + +本文结合 RISC-V 架构对 UEFI 的启动过程进行简单介绍,并重点分析 RISC-V Linux 中的 UEFI 启动相关实现。 + +*说明* + - Linux 版本采用 v6.3 + - UEFI 标准采用 [2.10][1] 版本文档 + - edk2 版本采用 `edk2-stable202302` 分支 + +## 构建 RISC-V EDK2 实验环境 + +EDK2 作为 UEFI 标准的开源实现,主要包括以下三个代码仓库: + +- [edk2][5]:edk2 主分支 +- [edk2-platforms][6]:edk2 的平台支持分支 +- [edk2-non-osi][7]: 不兼容 edk2 和 edk2-platform license 的分支 + +在构建 RISC-V edk2 实验环境过程中,主要用到前两个仓库:可通过第一个仓库构建 QEMU virt 的 edk2 镜像 ([参考][2]),也可结合第二仓库构建 QEMU sifive_u (HiFiveUnleashedBoard) 的 edk2 镜像 ([参考][3]),这里以 QEMU virt 为例,列出几个关键步骤: + +### 编译 QEMU virt edk2 + +执行如下命令构建 RISC-V QEMU virt 的 edk2 镜像,最终生成 `Build/RiscVVirtQemu/RELEASE_GCC5/FV/RISCV_VIRT.fd` 文件。 + +> 注意:需要对 edk2 镜像文件的大小进行调整以解决后续 QEMU 启动过程中有关 pflash 的报错 + +```sh +git clone --recurse-submodule git@github.com:tianocore/edk2.git + +export WORKSPACE=`pwd` +export GCC5_RISCV64_PREFIX=/usr/bin/riscv64-linux-gnu- +export PACKAGES_PATH=$WORKSPACE/edk2 +export EDK_TOOLS_PATH=$WORKSPACE/edk2/BaseTools +source edk2/edksetup.sh +make -C edk2/BaseTools clean +make -C edk2/BaseTools +make -C edk2/BaseTools/Source/C +source edk2/edksetup.sh BaseTools +build -a RISCV64 --buildtarget RELEASE -p OvmfPkg/RiscVVirt/RiscVVirtQemu.dsc -t GCC5 + +truncate -s 32M Build/RiscVVirtQemu/RELEASE_GCC5/FV/RISCV_VIRT.fd +``` + +### 制作 efi.img + +提前编译好 RISC-V Linux 内核镜像文件 `arch/riscv/boot/Image`,并使用如下命令保存内核镜像到 `efi.img` 中。 + +```sh +fallocate -l 512M efi.img +sgdisk -n 1:34: -t 1:EF00 efi.img +sudo losetup -fP efi.img +loopdev=`losetup -j efi.img | awk -F: '{print $1}'` +efi_part="$loopdev"p1 +sudo mkfs.msdos $efi_part +mkdir -p /tmp/mnt +sudo mount $efi_part /tmp/mnt/ +sudo cp linux/arch/riscv/boot/Image /tmp/mnt/ +sudo umount /tmp/mnt +sudo losetup -D $loopdev +``` + +### 启动 QEMU + +提前编译好 RISC-V rootfs 镜像文件,比如:`buildroot/output/images/rootfs.ext2`,执行如下命令启动 Qemu。之后在 EFI Shell 执行内核镜像。 + +```sh +qemu-system-riscv64 -nographic \ +-drive file=Build/RiscVVirtQemu/RELEASE_GCC5/FV/RISCV_VIRT.fd,if=pflash,format=raw,unit=1 \ +-machine virt -m 2G \ +-drive file=buildroot/output/images/rootfs.ext2,format=raw,id=hd0 -device virtio-blk-device,drive=hd0 \ +-drive file=efi.img,format=raw,id=hd1 -device virtio-blk-device,drive=hd1 + +Shell> fs0:\Image root=/dev/vda console=ttyS0 rootwait earlycon=uart8250,mmio,0x10000000 +``` + +## RISC-V EDK2 启动流程简介 + +RISC-V 架构的 edk2 移植的基本思路是基于 edk2 项目现有的启动流程以及构建环境,将 OpenSBI 编译为库并链接到 SEC 模块以充分利用 OpenSBI 进行平台的初始化。这里基于 UEFI 启动的七个启动阶段对 RISC-V 的实现做简单介绍(详见 edk2-platform 的 `Platform/RISC-V/PlatformPkg/Readme.md`)。 + +![riscv-edk2-boot.png](images/riscv_uefi/riscv-edk2-boot.png) + +- SEC 阶段 + + 处理系统上电或重启,执行 ResetVector 代码;创建临时内存;提供安全信任链的根;传送系统参数到下一阶段。 + + RISC-V: SEC 阶段调用 `sbi_init` 执行 OpenSBI 的初始化,之后以 NextAddr 和 NextMode 跳转到 PEI 阶段。其中 SEC 以及 OpenSBI 运行在 M-mode,而之后的阶段(PEI/DXE/BDS)则运行在 NextMode 指定的 S-mode (OEM 可通过相关的 PCD 设置 `PcdPeiCorePrivilegeMode` 或者 `PcdDxeCorePrivilegeMode` 指定 PEI/DXE 阶段运行在其他模式) + +- PEI 阶段 + + 此阶段依次执行 PEIM (PEI Module) 进行平台的初始化,将需要传递给 DXE 的信息组成 HOB(Handoff Block) 表,最终将控制权转交给 DXE。 + + RISC-V: PEI 运行在 `PcdPeiCorePrivilegeMode` 默认指定的 S-mode,如果需要运行 SEC 阶段的 PEI protocol interface (PPI) 代码,则要在该阶段早期安装 PPI 并通过 PlatformSecPpiLib 库来避免模式保护限制。 + + PEI 通过 RiscVFirmwareContextLib 库访问 OpenSBI 固件上下文 -- EFI_RISCV_OPENSBI_FIRMWARE_CONTEXT。 + + ```c + typedef struct { + UINT64 BootHartId; + VOID *PeiServiceTable; // PEI Service table // 向上以 PeiServiceTablePointerOpensbi 库提供访问 + UINT64 FlattenedDeviceTree; // Pointer to Flattened Device tree + UINT64 SecPeiHandOffData; // This is EFI_SEC_PEI_HAND_OFF passed to PEI Core. + EFI_RISCV_FIRMWARE_CONTEXT_HART_SPECIFIC *HartSpecific[RISC_V_MAX_HART_SUPPORTED]; // Hart 信息(拓展支持、厂商信息、模式切换方法(HartSwitchMode)) + } EFI_RISCV_OPENSBI_FIRMWARE_CONTEXT; + ``` + + PEI 驱动可通过 PEI OpenSBI PPI 调用 SBI 服务。 + +- DXE 阶段 + + 该阶段执行系统初始化工作,为后续 UEFI Application 和操作系统提供 UEFI 系统表、启动服务和运行时服务。 + + RISC-V: DXE 运行在 `PcdDxeCorePrivilegeMode` 默认指定的 S-mode,DXE 驱动可通过 DXE OpenSBI protocol 调用 OpenSBI 服务。 + +- BDS 阶段 + + 此阶段枚举每个启动设备,并执行启动策略(由全局 NVRAM 变量指定,运行时可修改)。如果 BDS 启动失败,系统会重新调用 DXE 派遣器,再次进入寻找启动设备的流程。 + + RISC-V: BDS 阶段必须要在将系统控制权移交给 S-mode 的 OS、OS loader、UEFI Application 之前切换到 S-mode。 + +- TSL 阶段 + + 此阶段为 OS loader(比如:grub、Linux EFI Boot Stub)执行的第一阶段,在这个阶段系统资源还是被 UEFI 所控制,直到 OS loader 执行 `BS.ExitBootServices()` 退出 Boot Service 进入 Runtime 阶段。 + + RISC-V:此阶段为 Linux 内核的 EFI Boot Stub 处理流程,我们放在后文详细介绍。 + +- RT 阶段 + + UEFI 各种系统资源被转移到 OS loader,启动服务不能再使用,仅保留运行时服务供操作系统使用。 + + RISC-V: 此阶段涉及 Linux 内核的 UEFI 运行时的初始化流程,我们放在后文详细介绍。 + +- AL 阶段 + + 在 RT 阶段,如果系统遇到灾难性错误,系统固件需要提供错误处理和灾难恢复机制,这种机制运行在 AL(AferLife)阶段。UEFI 和 UEFI PI 标准都没有定义此阶段的行为和规范。 + +## UEFI Linux 启动过程 + +### UEFI 内核镜像 + +UEFI Boot Manager 用于加载并执行 PE 格式的 UEFI 镜像,UEFI 镜像分为三类 UEFI Application、UEFI boot service drivers、UEFI runtime drivers,体现在 PE 头的 "Subsystem" 字段,三者的主要区别在于镜像加载时分配的内存空间不同(详见后文关于 UEFI 内存映射表的描述)。另外 PE 头的 "Machine" 字段表示该镜像可运行的平台,edk2 中 `MdePkg/Library/BasePeCoffLib/RiscV/PeCoffLoaderEx.c` 就定义了对 `EFI_IMAGE_MACHINE_RISCV64` 类镜像的处理函数。 + +```c +// BaseTools/Source/C/Include/IndustryStandard/PeImage.h : 22 + +// PE32+ Subsystem type for EFI images +#define EFI_IMAGE_SUBSYSTEM_EFI_APPLICATION 10 +#define EFI_IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11 +#define EFI_IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12 + +// PE32+ Machine type for EFI images +#define EFI_IMAGE_MACHINE_IA32 0x014c +#define EFI_IMAGE_MACHINE_IA64 0x0200 +#define EFI_IMAGE_MACHINE_EBC 0x0EBC +#define EFI_IMAGE_MACHINE_x64 0x8664 +#define EFI_IMAGE_MACHINE_ARMTHUMB_MIXED 0x01C2 +#define EFI_IMAGE_MACHINE_AARCH64 0xAA64 +#define EFI_IMAGE_MACHINE_RISCV32 0x5032 +#define EFI_IMAGE_MACHINE_RISCV64 0x5064 +#define EFI_IMAGE_MACHINE_RISCV128 0x5128 +#define EFI_IMAGE_MACHINE_LOONGARCH32 0x6232 +#define EFI_IMAGE_MACHINE_LOONGARCH64 0x6264 +``` + +在 UEFI Application 中有一类特殊的应用 - UEFI OS Loader,顾名思义,此应用是用来加载操作系统的,其被 Boot Manager 加载并执行,如果成功加载 OS,调用 `EFI_BOOT_SERVICES.ExitBootServices()` 结束 Boot Services 并将系统控制权转移给 OS,OS 继而可以使用 UEFI 提供的 Runtime Services。比如:grub 其在 EFI 分区存放的 grubx86.efi 就是一个 UEFI OS Loader, 通过 file 命令可以看到它是一个格式为 PE32+ 的 EFI Application。 + +```bash +$file /boot/efi/EFI/boot/grubx64.efi +/boot/efi/EFI/boot/grubx64.efi: PE32+ executable (EFI application) x86-64 (stripped to external PDB), for MS Windows +``` + +在“构建 RISC-V EDK2 实验环境”一节中,我们可以在 UEFI Shell 中直接运行内核镜像 -- Image,难道 Image 也是一个 UEFI Boot Loader 么? + +是的,Linux 内核提供 `CONFIG_EFI_STUB` 选项用于将内核镜像封装为 PE 镜像,当固件加载并执行此镜像时会跳转到镜像中定义的入口地址,继而执行与 OS Loader 相似的功能,并最终跳转到正式内核入口 `_start`,这一部分代码称之为 EFI Boot Stub。我们接下来,看下 UEFI 内核镜像是如何构建的: + +在 `arch/riscv/kernel/head.S` 中 `_start` 使用 `_HEAD` 修饰,声明其定义在 `.head.text` 节中,此节的开头部分按照 `struct riscv_image_header` 布局,其中: + +`riscv_image_header.{code0,code1)` 以 64 位对齐,如果开启 `CONFIG_EFI`,填充 `c.li s4,-13` 指令和 `j _start_kernel`。其中 `c.li` 指令编码为 16 位的 '0x5a4d',此值对应 `MZ_MAGIC`,使得该节经过链接以及 objcopy 可生成开头为 "MZ" 魔数的 PE 镜像。 + +```c + +// arch/riscv/include/asm/image.h : 55 + +struct riscv_image_header { + u32 code0; + u32 code1; + u64 text_offset; + ... + u32 res3; +}; + +// arch/riscv/kernel/head.S : 21 + +__HEAD +ENTRY(_start) +#ifdef CONFIG_EFI + c.li s4,-13 // #define MZ_MAGIC 0x5a4d + j _start_kernel +#else + j _start_kernel + .word 0 +#endif + .balign 8 + + // ... +#ifdef CONFIG_EFI + .word pe_head_start - _start // riscv_image_header.rev3 +pe_head_start: + __EFI_PE_HEADER +#else + .word 0 +#endif + + // ... +``` + +`riscv_image_header.res3` 为最后的成员,存储 PE 头与 _start 的偏移,并在其后追加 PE 头 `__EFI_PE_HEADER`。`__EFI_PE_HEADER` 定义在 `arch/riscv/kernel/efi-header.S` 文件中,按照 PE 镜像相关结构进行布局,这里摘录几个关键的点进行介绍: + +- `coff_header.Machine` 定义为 `IMAGE_FILE_MACHINE_RISCV64` 或者 `IMAGE_FILE_MACHINE_RISCV32`,此值与前面介绍的 UEFI 镜像中的 "Machine" 字段相对应,在 edk2 中定义为 `EFI_IMAGE_MACHINE_RISCV64` 和 `EFI_IMAGE_MACHINE_RISCV32` + +- `extra_header_fields.Subsystem` 定义为 `IMAGE_SUBSYSTEM_EFI_APPLICATION`,表明此镜像为 EFI Application 类型的 UEFI 镜像,此值在 edk2 中定义为 `EFI_IMAGE_SUBSYSTEM_EFI_APPLICATION` + +- `optional_header.AddressOfEntryPoint` 定义为 `__efistub_efi_pe_entry - _start`,表明此镜像被加载后并执行的入口函数为 `efi_pe_entry`(`__efi_stub_` 前缀为 EFI Boot Stub 相关代码 objcopy 时所添加) + +```c + +// arch/riscv/kernel/efi-header.S : 10 + + .macro __EFI_PE_HEADER + .long PE_MAGIC +coff_header: +#ifdef CONFIG_64BIT + .short IMAGE_FILE_MACHINE_RISCV64 // Machine +#else + .short IMAGE_FILE_MACHINE_RISCV32 // Machine +#endif + +optional_header: +#ifdef CONFIG_64BIT + .short PE_OPT_MAGIC_PE32PLUS // PE32+ format +#else + .short PE_OPT_MAGIC_PE32 // PE32 format +#endif + + .long __efistub_efi_pe_entry - _start // AddressOfEntryPoint + +extra_header_fields: + //... + .short IMAGE_SUBSYSTEM_EFI_APPLICATION // Subsystem + +// ./drivers/firmware/efi/libstub/Makefile : 149 + +STUBCOPY_FLAGS-$(CONFIG_RISCV) += --prefix-alloc-sections=.init \ + --prefix-symbols=__efistub_ +STUBCOPY_RELOC-$(CONFIG_RISCV) := R_RISCV_HI20 +``` + +### EFI Boot Stub efi_pe_entry + +上节中介绍到,在 UEFI Shell 中直接执行的 UEFI 内核镜像是一个 PE 格式的 UEFI Application(准确说是一个 UEFI OS Loader),其被加载后执行的入口函数为 `efi_pe_entry`(也可理解为是 EFI Boot Stub 的入口),此函数执行 OS Loader 相关的任务,并最终跳转到正式内核的入口 `_start`。 + +`efi_pe_entry` 作为 UEFI 镜像的入口函数,遵守 UEFI 标准中 EFI 镜像入口点 -- "EFI_IMAGE_ENTRY_POINT" 的接口定义,此接口的第一个参数 `ImageHandle` 是固件为当前镜像创建的句柄,在入口函数的后续流程中可通过 `EFI_LOADED_IMAGE_PROTOCOL` 获取当前镜像的一些信息;第二个参数 `SystemTable` 为系统表,这个参数主要包含以下信息: + +- 控制台的标准输入输出、错误输出 (ConsoleInHandle/ConsoleOutHandle/StandardErrorHandle) +- Boot Services / Runtime 服务表 (BootServices/RuntimeServices),后续分析中会大量用到 Boot Services 提供的服务 +- 配置表 (ConfigurationTable),比如:ACPI, SMBIOS、设备树 等等 + +```c +// MdePkg/Include/Uefi/UefiSpec.h : 1975 + +typedef +EFI_STATUS +(EFIAPI *EFI_IMAGE_ENTRY_POINT) ( + IN EFI_HANDLE ImageHandle, + IN EFI_SYSTEM_TABLE *SystemTable + ); + +typedef struct { + EFI_TABLE_HEADER Hdr; + CHAR16 *FirmwareVendor; + UINT32 FirmwareRevision; + EFI_HANDLE ConsoleInHandle; + EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; + EFI_HANDLE ConsoleOutHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; + EFI_HANDLE StandardErrorHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; + EFI_RUNTIME_SERVICES *RuntimeServices; + EFI_BOOT_SERVICES *BootServices; + UINTN NumberOfTableEntries; + EFI_CONFIGURATION_TABLE *ConfigurationTable; +} EFI_SYSTEM_TABLE; +``` + +`efi_pe_entry()` 函数执行如下步骤: + +调用 `BS.HandleProtocol()` 接口获取当前 UEFI 镜像到 `image` 变量;`efi_handle_cmdline` 函数可通过 `image->load_options` 获取 UEFI Shell 中指定的内核命令行参数。 + +`handle_kernel_image()` 调用 `efi_relocate_kernel` 进行内核的重定位:调用 `BS.AllocatePages(EFI_ALLOCATE_ADDRESS)` 在 `EFI_LOADER_DATA` 内存空间为内核镜像分配内存,分配的起始地址为 2M(if 64bit),分配大小为 `_end - start` 即内核镜像大小,并逐字拷贝内核镜像,需要注意的是这里没有拷贝 bss 相关段。如果给定的起始地址不满足条件,则会调用 `efi_low_alloc_above()` 在 UEFI 内存映射表的 `EFI_LOADER_DATA` 空间找到尽可能小的地址进行内存分配。 + +```c +// drivers/firmware/efi/libstub/efi-stub-entry.c + +efi_status_t __efiapi efi_pe_entry(efi_handle_t handle, efi_system_table_t *systab) + + WRITE_ONCE(efi_system_table, systab); + + // get image by BS.HandleProtocol(handle,EFI_LOADED_IMAGE_PROTOCOL,) + efi_bs_call(handle_protocol, handle, &loaded_image_proto, (void *)&image); + + // 处理 EFI 应用的命令行 + efi_handle_cmdline(image, &cmdline_ptr); + + // kernel 重定位 + handle_kernel_image(&image_addr, &image_size, &reserve_addr, &reserve_size, image, handle); // relocate kernel + kernel_size = _edata - _start; + *image_addr = (unsigned long)_start; + *image_size = kernel_size + (_end - _edata); // efi kernel size + + efi_relocate_kernel(image_addr, kernel_size, *image_size, preferred_addr, efi_get_kimg_min_align(), 0x0); + efi_bs_call(allocate_pages, EFI_ALLOCATE_ADDRESS, EFI_LOADER_DATA, nr_pages, &efi_addr); + memcpy((void *)new_addr, (void *)cur_image_addr, image_size); + image_addr = efi_addr or new_addr; // update image_addr + + efi_stub_common(handle, image, image_addr, cmdline_ptr); +``` + +`efi_pe_entry()` 在对内核镜像进行重定位后,调用 `efi_stub_common()` 访问必要的 UEFI 接口执行一些简单的初始化任务,最终调用 `efi_boot_kernel()` 启动正式内核: + +- `check_platform_features()` 通过 `RISCV_EFI_BOOT_PROTOCOL_GUID` 协议设置 `hartid`,如果失败则通过配置表中的 FDT 的 "chosen" 节点的 "boot-hartid" 属性获取(可通过平台级的 `PcdBootHartId` 进行配置) + +- `setup_graphics()` 通过 `EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID` 协议获取显示相关信息 + +- `efi_load_initrd()` 加载 initrd + + initrd 一般有两个来源,固件提供(比如:QEMU 命令行指定、或者通过 UEFI Shell 的 initrd 命令指定)以及 Linux 命令行指定。第一种情况下,执行 `efi_load_initrd_dev_path()` 访问 `LINUX_EFI_INITRD_MEDIA_GUID` 配置表来获取;第二种情况下,执行 `efi_load_initrd_cmdline()` 调用 `efi_open_file` 来获取。之后,在 `EFI_LOADER_DATA` 中为其分配内存空间,并将 initrd 以 `LINUX_EFI_INITRD_MEDIA_GUID` 安装到配置表中。 + +- `efi_random_get_seed()` 通过 `EFI_RNG_PROTOCOL_GUID` 获取随机源,并将其以 `LINUX_EFI_RANDOM_SEED_TABLE_GUID` 安装到配置表中 + +- `install_memreserve_table()` 安装 `LINUX_EFI_MEMRESERVE_TABLE_GUID` 配置表 + +```c +// drivers/firmware/efi/libstub/efi-stub.c : 287 + +efi_stub_common(handle, image, image_addr, cmdline_ptr); + + check_platform_features(); // set `hartid` by RISCV_EFI_BOOT_PROTOCOL_GUID + + setup_graphics(); // get struct screen_info by EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID + + efi_load_initrd() // loaded initrd + efi_load_initrd_dev_path() + efi_load_initrd_cmdline() + + efi_random_get_seed() // EFI_RNG_PROTOCOL random bytes saved as a configuration table + + efi_novamap // 此变量表示代表是否支持为 RT 设置虚拟地址,后文做详细介绍 + + install_memreserve_table() // BS.InstallConfigurationTable LINUX_EFI_MEMRESERVE_TABLE_GUID + + efi_boot_kernel(handle, image, image_addr, cmdline_ptr); +``` + +### EFI Boot Stub efi_boot_kernel + +`efi_boot_kernel()` 主要执行两个函数 -- `allocate_new_fdt_and_exit_boot()`、`efi_enter_kernel()`。 + +`allocate_new_fdt_and_exit_boot()` 函数最终会调用 `BS.ExitBootServices()` 接口结束所有的 UEFI Boot Services,但在这个过程中有两个额外的任务需要处理: + +第一个是与 dtb 相关的处理:dtb 与 initrd 类似有两个来源,一个是固件提供(比如:QEMU 提供给 edk2 的 dtb),还有一个是通过 Linux 命令行提供的。前者从配置表 `DEVICE_TREE_GUID` 中获取,后者通过 `efi_load_dtb()` 走 UEFI 文件接口来获取。之后为 dtb 分配内存,并执行 `update_fdt()` 函数在 dtb 的 chosen 节点中创建如下几个 chosen 变量,配合后续的 `update_fdt_memmap()` 函数对其进行设置。这几个变量会在 EFI Boot Stub 跳转到正式内核后以 dtb 的形式提供,而正式内核则解析这些变量继而执行相应的初始化。 + +- `bootargs` + + 存放命令行参数,传递给正式内核进行解析 + +- `linux,uefi-system-table` + + 存放系统表,正式内核可通过系统表获取 ACPI/INITRD/SMBIOS 等配置表信息并执行对应的初始化,也可通过系统表获取到 UEFI Runtime 服务表。相关内容会在后文详细介绍。 + +- `linux,uefi-mmap-start`, `linux,uefi-mmap-size`, `linux,uefi-mmap-desc-size`, `linux,uefi-mmap-desc-ver` + + 存放 UEFI 内存映射,正式内核可通过 UEFI 内存映射表了解物理内存布局,从而更新 memblock 内存分配器。 + + edk2 中以内存描述符 -- `EFI_MEMORY_DESCRIPTOR` 构成的链表描述 UEFI 内存映射表,并对外提供 `EFI_BOOT_SERVICES.GetMemoryMap()` 接口获取内存映射表。在执行之后的 `efi_exit_boot_services()` 过程中会调用此接口并通过 `update_fdt_memmap()` 函数对相关的 chosen 变量进行更新。相关结构定义如下: + +```c +// MdePkg/Include/Uefi/UefiSpec.h : 160 + +typedef struct { + UINT32 Type; // enum EFI_MEMORY_TYPE eg: EfiLoaderCode、EfiLoaderData、EfiBootServicesCode、EfiBootServicesData .. + EFI_PHYSICAL_ADDRESS PhysicalStart; // 物理内存起始地址 + EFI_VIRTUAL_ADDRESS VirtualStart; // 虚拟地址起始地址 + UINT64 NumberOfPages; // 内存空间大小 + UINT64 Attribute; // 内存属性 eg: Memory cacheability attribute、Physical memory protection attribute、Runtime memory attribute + } EFI_MEMORY_DESCRIPTOR; + +typedef +EFI_STATUS +(EFIAPI *EFI_GET_MEMORY_MAP) ( + IN OUT UINTN *MemoryMapSize, // 整个内存映射表的大小 + OUT EFI_MEMORY_DESCRIPTOR *MemoryMap, // 内存映射表 + OUT UINTN *MapKey, // 固件返回的内存映射 key 值 + OUT UINTN *DescriptorSize, // 内存描述符的大小 + OUT UINT32 *DescriptorVersion // 内存描述符的版本 -- EFI_MEMORY_DESCRIPTOR_VERSION = 1 + ); +``` + +`allocate_new_fdt_and_exit_boot()` 函数还有一个任务就是与 Runtime Services 相关的处理: + +在 UEFI 标准中定义 `EFI_RT_PROPERTIES_TABLE` 结构来表示 `EFI_RT_PROPERTIES_TABLE_GUID` 配置表,其关键成员 `RuntimeServicesSupported` 用来表示 Runtime 所支持的服务,该成员的 `EFI_RT_SUPPORTED_SET_VIRTUAL_ADDRESS_MAP` 标志位表示是否支持为 Runtime 服务设置虚拟地址。在 `efi_stub_common` 阶段会对此标志位进行检查并保存到 `efi_novamap` 变量中。 + +```c +// MdePkg/Include/Guid/RtPropertiesTable.h : 28 + +typedef struct { + UINT16 Version; + UINT16 Length; + UINT32 RuntimeServicesSupported; +} EFI_RT_PROPERTIES_TABLE; + +typedef +EFI_STATUS +SetVirtualAddressMap ( + IN UINTN MemoryMapSize, + IN UINTN DescriptorSize, + IN UINT32 DescriptorVersion, + IN EFI_MEMORY_DESCRIPTOR *VirtualMap // runtime_map + ); +``` + +`allocate_new_fdt_and_exit_boot()` 函数对 `efi_novamap` 进行判断,如果支持虚拟地址设置,则在 `EFI_LOADER_DATA` 空间分配 UEFI 内存映射大小的内存,保存在 `struct exit_boot_struct` 实例的 `runtime_map` 中。在执行之后的 `efi_exit_boot_services()` 函数过程中,会调用 `efi_get_virtmap()` 遍历 UEFI 内存映射表,如果为 `EFI_MEMORY_RUNTIME` 类型的内存描述符,则以 `phys_addr + EFI_RT_VIRTUAL_OFFSET` 设置其 `virt_addr`(线性映射),最后将此描述符拷贝到 `runtime_map` 中,并更新其计数 `runtime_entry_count`。 + +`efi_exit_boot_services()` 结尾处调用 `BS.ExitBootServices()` 接口结束所有的 UEFI Boot Services,在此接口成功返回后,调用 `RT.SetVirtualAddressMap()` 接口,从而固件中的所有运行时服务都采用虚拟地址进行访问。 + +```c +// drivers/firmware/efi/libstub/fdt.c : 184 + +struct exit_boot_struct { + struct efi_boot_memmap *boot_memmap; // UEFI 内存映射表 + efi_memory_desc_t *runtime_map; // 已设置虚拟地址的 EFI_MEMORY_RUNTIME 类型的内存描述符链表 + int runtime_entry_count; // runtime_map 表数目 + void *new_fdt_addr; // 分配的 fdt 地址 +}; + +struct efi_boot_memmap { // 对应 EFI_GET_MEMORY_MAP 接口 + unsigned long map_size; + unsigned long desc_size; + u32 desc_ver; + unsigned long map_key; + unsigned long buff_size; + efi_memory_desc_t map[]; +}; + +// drivers/firmware/efi/libstub/fdt.c : 343 + +efi_boot_kernel(void *handle, efi_loaded_image_t *image, unsigned long kernel_addr, char *cmdline_ptr); + + allocate_new_fdt_and_exit_boot(handle, image, &fdt_addr, cmdlinetr); + + // 创建 p->runtime_map in LOADER_DATA + !efi_novamap && efi_alloc_virtmap(&priv.runtime_map, &desc_size, &desc_ver); + + // 处理 fdt + efi_load_dtb(image, &fdt_addr, &fdt_size); // same as efi_load_initrd + + efi_allocate_pages(MAX_FDT_SIZE, new_fdt_addr, ULONG_MAX); + update_fdt((void *)fdt_addr, fdt_size,...)) // 添加 chosen 变量 + priv.new_fdt_addr = (void *)*new_fdt_addr; + + // 更新 fdt,退出 Boot Services,设置 RT 为虚拟地址 + + efi_exit_boot_services(handle, &priv, exit_boot_func) + efi_get_memory_map(&map, true); // BS.GetMemoryMemmep + exit_boot_func(map,priv) + p->boot_memmap = map; // struct exit_boot_struct p; + + efi_get_virtmap(map->map, map->map_size, map->desc_size, p->runtime_map, &p->runtime_entry_count); + in->virt_addr = in->phys_addr + EFI_RT_VIRTUAL_OFFSET // RISC-V EFI_RT_VIRTUAL_OFFSET = 0 + + update_fdt_memmap(p->new_fdt_addr, map) + + efi_bs_call(exit_boot_services, handle, map->map_key); // BS.ExitBootServices + // RT.SetVirtualAddressMap() : Changes the runtime addressing mode of EFI firmware from physical to virtual + efi_system_table->runtime->set_virtual_address_map(priv.runtime_entry_count * desc_size, desc_size, desc_ver, priv.runtime_map); + + efi_enter_kernel(kernel_addr, fdt_addr, fdt_totalsize((void fdt_addr)); +``` + +`efi_boot_kernel()` 函数最后调用 `efi_enter_kernel()`,清空 `satp` 以关闭 MMU,以 `hartid` 和 `fdt` 为参数调用 `_start`。 + +```c +// drivers/firmware/efi/libstub/riscv.c : 97 + +// entrypoint 就是 efi_relocate_kernel 阶段返回的 _start 加载地址 +efi_enter_kernel(unsigned long entrypoint, unsigned long fdt, unsigned long fdt_size); + csr_write(CSR_SATP, 0); + jump_kernel(hartid, fdt); +``` + +## 小结 + +Linux EFI Boot Stub 作为一种 UEFI OS Loader 在 UEFI 的 TSL 阶段调用 Boot Services 接口为正式内核准备系统表、UEFI 内存映射表、命令行参数,并在退出 Boot Services 后,又为 Runtime Services 设置虚拟地址,最终跳转到正式内核。那么正式内核又将如何处理 EFI Boot Stub 传递的数据呢?且看下文分解。 + +## 参考资料 + +- [UEFI 标准][1] +- [OpenSBI/U-Boot/UEFI 简介][4] + +[1]: https://uefi.org/specs/UEFI/2.10/index.html +[2]: https://github.com/vlsunil/riscv-uefi-edk2-docs/wiki/RISC-V-Qemu-Virt-support +[3]: https://github.com/riscv-admin/riscv-uefi-edk2-docs +[4]: https://tinylab.org/riscv-uefi-part1/ +[5]: http://github.com/tianocore/edk2 +[6]: https://github.com/tianocore/edk2-platforms.git +[7]: https://github.com/tianocore/edk2-non-osi diff --git a/articles/20230421-riscv-linux-uefi-boot-2.md b/articles/20230421-riscv-linux-uefi-boot-2.md new file mode 100644 index 0000000000000000000000000000000000000000..d0787e9c4d831a655aa8a5ab24e57dca6c8bb616 --- /dev/null +++ b/articles/20230421-riscv-linux-uefi-boot-2.md @@ -0,0 +1,282 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1 - [spaces header toc codeinline pangu epw]
+> Author: sugarfillet
+> Date: 2023/04/21
+> Revisor: Falcon falcon@tinylab.org
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [RISC-V UEFI 启动流程分析与 EDK2 移植](https://gitee.com/tinylab/riscv-linux/issues/I64FSG)
+> Sponsor: PLCT Lab, ISCAS + +# RISC-V Linux 内核 UEFI 启动过程分析(Part2):内核侧 UEFI 支持 + +## 前言 + +上文对 RISC-V Linux 的 EFI Boot Stub 进行了介绍,它给正式内核传递了不少的信息。本文趁热打铁,继续分析正式内核的 UEFI 初始化相关流程。 + +*说明* + - Linux 版本采用 v6.3 + +## UEFI 初始化 -- efi_init + +Linux EFI Boot Stub 以 `boothardid` 和修改了 `chosen` 变量的 fdt 跳转到 `_start` 启动正式内核。正式内核在 `setup_arch()` 阶段调用 `efi_init()` 进行 EFI 的初始化。整体来看,`efi_init()` 的主要工作有两个,一个是对 UEFI 系统表的处理,包括运行时服务的保存、配置表的解析和初步处理;还有一个是把从 UEFI Boot Stub 传递过来的 UEFI 内存映射表交接给 memblock。此函数的关键过程按序分析如下: + +> `memblock` 是内核启动初期用于管理内存的机制,主要将可用、保留以及不可用的物理内存进行划分和管理,后续会移交管理权给伙伴系统。 +> +> Linux 维护一个 `struct memblock memblock` 实体,其中 `memblock.memory` 描述了 `memblock` 管理的可用内存,`memblock.reserved` 描述了 `memblock` 管理的预留内存 + +`efi_get_fdt_params()` 函数以 `dt_params` 全局变量匹配 fdt 中的 `chosen` 变量,保存内存映射表信息到 `struct efi_memory_map_data` 实例中,并返回 UEFI 系统表的物理地址。 + +`efi_memmap_init_early()` 函数重新以 `struct efi_memory_map` 结构保存内存映射表,其中 `map` 成员为映射表的虚拟地址,通过 `early_memremap()` 函数在 fixed-mapping 中创建页表映射,最终记录到 `efi.memap` 全局结构中。 + +```c +// include/linux/efi.h : 547 + +struct efi_memory_map_data { + phys_addr_t phys_map; + unsigned long size; + unsigned long desc_version; + unsigned long desc_size; + unsigned long flags; +}; + +struct efi_memory_map { + phys_addr_t phys_map; + void *map; + void *map_end; + int nr_map; + unsigned long desc_version; + unsigned long desc_size; + unsigned long flags; +}; +``` + +```c +// drivers/firmware/efi/fdtparams.c : 35 + +static __initconst const struct { + const char path[17]; + u8 paravirt; + const char params[PARAMCOUNT][26]; +} dt_params[] = { + + .path = "/chosen", + .params = { // <-----------26-----------> + [SYSTAB] = "linux,uefi-system-table", + [MMBASE] = "linux,uefi-mmap-start", + [MMSIZE] = "linux,uefi-mmap-size", + [DCSIZE] = "linux,uefi-mmap-desc-size", + [DCVERS] = "linux,uefi-mmap-desc-ver", + } +} + +// drivers/firmware/efi/efi-init.c :199 + +efi_init() + // Grab UEFI information placed in FDT by stub + efi_system_table = efi_get_fdt_params(&data); // struct efi_memory_map_data * data + return systab; + + efi_memmap_init_early(&data) + struct efi_memory_map map; + map.map = early_memremap(data->phys_map, data->size); // 内存映射表的虚拟地址 + set_bit(EFI_MEMMAP, &efi.flags); // we use EFI memory map + efi.memmap = map +``` + +`uefi_init()` 函数用于处理 UEFI 系统表,保存运行时服务到 `efi.runtime`;调用 `efi_config_parse_tables()` 函数以 `common_tables` 为参照,保存配置表到对应的变量中进行部分初步处理,比如: + +- `LINUX_EFI_INITRD_MEDIA_GUID` 表保存到 initrd 变量中,initrd 地址和大小分别保存到 `phys_initrd_start`、`phys_initrd_size` +- `EFI_RT_PROPERTIES_TABLE_GUID` 表代表 UEFI 运行时所支持的服务,通过 `rt_prop` 进一步更新到 `efi.runtime_supported_mask` +- `LINUX_EFI_MEMRESERVE_TABLE_GUID` 表代 UEFI 所保留的物理内存空间,调用 `memblock_reserve()` 接口将其更新到 `memblock.reserved` 中 + +而其他的一些变量,比如 `efi.acpi*` 则在 `setup_arch()` 的后续流程 `acpi_boot_table_init()` 中处理。 + +```c +efi_init() + // drivers/firmware/efi/efi-init.c : 78 + uefi_init(efi_system_table) + systab = early_memremap_ro(efi_system_table // remap systable + set_bit(EFI_BOOT, &efi.flags); + set_bit(EFI_64BIT, &efi.flags); + + efi.runtime = systab->runtime; + + config_tables = early_memremap_ro(efi_to_phys(systab->tables), table_size); // remap conftable + efi_config_parse_tables(config_tables, systab->nr_tables, efi_arch_tables); + match_config_table(guid, table, common_tables) // 解析 common_tables (ACPI/SMBIOS/ESRT/INITRD/MEMRESERVE) + set_bit(EFI_CONFIG_TABLES, &efi.flags); + + // 处理 efi_rng_seed mem_reserve rt_prop initrd ... + + // set the reserved memory in the memblock.reserved + memblock_reserve(prsv, struct_size(rsv, entry, rsv->size)); + +static const efi_config_table_type_t common_tables[] __initconst = { + {ACPI_20_TABLE_GUID, &efi.acpi20, "ACPI 2.0" }, + {ACPI_TABLE_GUID, &efi.acpi, "ACPI" }, + {SMBIOS_TABLE_GUID, &efi.smbios, "SMBIOS" }, + {SMBIOS3_TABLE_GUID, &efi.smbios3, "SMBIOS 3.0" }, + {EFI_SYSTEM_RESOURCE_TABLE_GUID, &efi.esrt, "ESRT" }, + {EFI_MEMORY_ATTRIBUTES_TABLE_GUID, &efi_mem_attr_table, "MEMATTR" }, + {LINUX_EFI_RANDOM_SEED_TABLE_GUID, &efi_rng_seed, "RNG" }, + // ... + {LINUX_EFI_MEMRESERVE_TABLE_GUID, &mem_reserve, "MEMRESERVE" }, + {LINUX_EFI_INITRD_MEDIA_GUID, &initrd, "INITRD" }, + {EFI_RT_PROPERTIES_TABLE_GUID, &rt_prop, "RTPROP" }, + // ... +} +``` + +`reserve_regions()` 函数首先清空从 dtb 中构建的 memblock,之后遍历 UEFI 内存描述表 (efi.memmap),对于在 [MIN_MEMBLOCK_ADDR,MAX_MEMBLOCK_ADDR] 范围内的内存执行 `memblock_add()` 添加到 `memblock.memory` 类型中,并对那些不可用的内存(比如:用于 Runtime Services 的内存、用于特殊目的的内存 `EFI_MEMORY_SP` 等等)调用 `memblock_mark_nomap()` 设置其内存区域(memblock region)标志位为 `MEMBLOCK_NOMAP`,此标志位表示此内存区域不用于内存映射。 + +`efi_init()` 继续执行从 UEFI 内存映射表到 memblock 的交接工作,篇幅有限,这里不一一列举: + +- `early_init_dt_check_for_usable_mem_range()` 函数解析 dtb 中的 `linux,usable-memory-range` 节点,并修饰 memblock,此节点描述用于内核 kdump 的内存范围 +- `efi_find_mirror()` 函数根据 UEFI 内存标志位 `EFI_MEMORY_MORE_RELIABLE` 处理高可靠内存,调用 `memblock_mark_mirror()` 设置 `MEMBLOCK_MIRROR` 标志位 +- `init_screen_info()` 函数处理 UEFI 屏幕信息 `struct screen_info` 的物理地址 `screen_info.lfb_base`,在 memblock 中设置为 `MEMBLOCK_NOMAP` + +```c +efi_init() + //drivers/firmware/efi/efi-init.c : 155 + + reserve_regions() + // discard memblock which originated from memory nodes in the DT + memblock_dump_all(); && memblock_remove(0, PHYS_ADDR_MAX); + + for_each_efi_memory_desc(md) + early_init_dt_add_memory_arch() + memblock_add(md->phys_addr, size); + !is_usable_memory(md) && memblock_mark_nomap() // nomap some ram + + early_init_dt_check_for_usable_mem_range() // 处理 linux,usable-memory-range + efi_find_mirror() // 处理高可靠内存 EFI_MEMORY_MORE_RELIABLE + efi_esrt_init() // Reserving ESRT space in memblock.reserved + efi_mokvar_table_init(); // 处理 EFI MOK config table + memblock_reserve(data.phys_map & PAGE_MASK, PAGE_ALIGN(data.size + (data.phys_map & ~PAGE_MASK))); // 设置 UEFI 内存映射表到 memblock.reserved + init_screen_info() // 处理 screen_info_table +``` + +## UEFI 运行时服务 + +### UEFI 运行时服务初始化 + +`riscv_enable_runtime_services()` 为 RISC-V 架构下 UEFI Runtime Services 初始化函数,主要执行如下流程: + +对于 UEFI 内存映射表的内存映射,存在两个版本:一个是调用 `efi_memmap_init_early()` 以 fixed-mapping 空间进行早期映射,另一个调用 `efi_memmap_init_late()` 在 vmalloc 空间进行后期映射。两个函数都调用 `__efi_memmap_init()` 函数,以传入的参数中是否有 `EFI_MEMMAP_LATE` 标志为条件分别调用 `early_memremap()`, `memremap()`。 + +在 `efi_init()` 阶段完成早期映射,考虑到 fixed-mapping 空间的稀缺性,在当前阶段调用 `efi_memmap_unmap()` 解除早期映射,并调用 `efi_memmap_init_late()` 对 UEFI 内存映射表进行后期映射。 + +`efi_virtmap_init` 对 `efi_mm` 进行初始化,首先为其分配页目录项,之后遍历 UEFI 内存映射表,对 `EFI_MEMORY_RUNTIME` 类型的内存描述符,执行 `efi_create_mapping(&efi_mm, md)` 创建 `md->virt_addr` 到 `md->phys_addr` 的页表映射(这里的虚拟地址 `md->virt_addr` 正是在 EFI Boot Stub 基于 `EFI_RT_VIRTUAL_OFFSET` 计算的);最后调用 `efi_memattr_apply_permissions()` 基于 UEFI 内存属性配置表 -- `efi_mem_attr_table` 对虚拟地址进行权限设置。如果设置了 `efi=debug` 命令行选项,可以看到这样的输出: + +``` +[ 0.115472] Remapping and enabling EFI services. +[ 0.122078] efi: memattr: Processing EFI Memory Attributes table: +[ 0.122844] efi: memattr: 0x0000ffe3d000-0x0000ffe8dfff [Runtime Code|RUN| | | | |XP| | | | | | | | ] +[ 0.124471] efi: memattr: 0x0000ffe8e000-0x0000ffe8ffff [Runtime Code|RUN| | | | | | | |RO| | | | | ] +[ 0.125622] efi: memattr: 0x0000ffe90000-0x0000ffe92fff [Runtime Code|RUN| | | | |XP| | | | | | | | ] +[ 0.126453] efi: memattr: 0x0000ffe93000-0x0000ffe95fff [Runtime Code|RUN| | | | | | | |RO| | | | | ] +... +``` + +`efi_native_runtime_setup()` 函数负责对 `efi` 变量中的 Runtime Services 函数进行设置,比如:设置 `efi.get_time = virt_efi_get_time`),而其他的模块(比如:rtc-efi -- `drivers/rtc/rtc-efi.c`)则可通过 `efi.get_time` 来获取固件提供的时间。 + +```c +// drivers/firmware/efi/riscv-runtime.c : 66 + +early_initcall(riscv_enable_runtime_services); + +riscv_enable_runtime_services() + + efi_memmap_unmap(); + early_memunmap(efi.memmap.map, size); // clear the early EFI memmap + efi.memmap.map = NULL; + clear_bit(EFI_MEMMAP, &efi.flags); + + // EFI map 有两个初始化 + // 早期初始化(efi_init/efi_memmap_init_early -> early_memremap)使用稀缺的 fixmap 空间 + // 后期初始化(efi_memmap_init_late -> memremap)使用 vmalloc 空间 + efi_memmap_init_late(efi.memmap.phys_map, mapsize) + + // Remapping and enabling EFI services + efi_virtmap_init() + + efi_mm.pgd = pgd_alloc(&efi_mm); + for_each_efi_memory_desc(md) + if (md->attribute & EFI_MEMORY_RUNTIME) efi_create_mapping(&efi_mm, md); + for (i = 0; i < md->num_pages; i++) + create_pgd_mapping(mm->pgd, md->virt_addr + i * PAGE_SIZE, md->phys_addr + i * PAGE_SIZE, PAGE_SIZE, prot); + + efi_memattr_apply_permissions(&efi_mm, efi_set_mapping_permissions) + tbl = memremap(efi_mem_attr_table, tbl_size, MEMREMAP_WB); // 映射内存属性表 + efi_set_mapping_permissions(mm, &md, has_bti); // print EFI memmap attr table + apply_to_page_range(mm, md->virt_addr, md->num_pages << EFI_PAGE_SHIFT, set_permissions, md); // 对 vm 设置权限,底层实现为设置 pte_val(pte) + + efi_native_runtime_setup() // efi.get_time = virt_efi_get_time // efi.reset_system = virt_efi_reset_system + set_bit(EFI_RUNTIME_SERVICES, &efi.flags); +``` + +### UEFI 运行时服务函数 + +我们以获取系统时间函数 -- `virt_efi_get_time()` 为例进行分析,此函数内部使用 `efi_queue_work` 宏,此宏对传入的参数保存到 `struct efi_runtime_work efi_rts_work` 全局变量中,同时保存当前运行时服务 ID 到 `efi_rts_work.efi_rts_id`,并以 `efi_call_rts` 函数初始化工作 `struct work_struct &efi_rts_work.work`,插入工作队列 `efi_rts_wq`,之后等待工作队列函数释放 `&efi_rts_work.efi_rts_comp` 完成变量。 + +`efi_call_rts` 函数根据保存的运行时服务 ID 调用对应的运行时服务,并释放完成变量。运行时服务的调用过程分为三个阶段: + +1. `arch_efi_call_virt_setup()` 以 `efi_mm.pgd` 设置内核根页目录项,并调用 `efi_virtmap_load` 切换当前进程的内存上下文为 `efi_mm` +2. `arch_efi_call_virt(efi.runtime, get_time, args)` 调用 UEFI 提供的运行时服务 `efi.runtime.get_time()` +3. `arch_efi_call_virt_teardown()` 调用 `efi_virtmap_unload()` 恢复进程的内存上下文 + +关键代码摘录如下: + +```c +// include/linux/efi.h : 1249 + +struct efi_runtime_work { + void *arg1; + void *arg2; + void *arg3; + void *arg4; + void *arg5; + efi_status_t status; + struct work_struct work; + enum efi_rts_ids efi_rts_id; + struct completion efi_rts_comp; +}; + +// drivers/firmware/efi/runtime-wrappers.c : 253 + +static efi_status_t virt_efi_get_time(efi_time_t *tm, efi_time_cap_t *tc) + status = efi_queue_work(EFI_GET_TIME, tm, tc, NULL, NULL, NULL); // + init_completion(&efi_rts_work.efi_rts_comp); + INIT_WORK(&efi_rts_work.work, efi_call_rts); + efi_rts_work.arg1 = _arg1; + //... + efi_rts_work.efi_rts_id = _rts; + if (queue_work(efi_rts_wq, &efi_rts_work.work)) + wait_for_completion(&efi_rts_work.efi_rts_comp); + +// drivers/firmware/efi/runtime-wrappers.c : 174 + +void efi_call_rts(struct work_struct *work) + + switch (efi_rts_work.efi_rts_id) { + case EFI_GET_TIME: + status = efi_call_virt(get_time, (efi_time_t *)arg1, (efi_time_cap_t *)arg2); + efi_call_virt_pointer(efi.runtime, f, args) + + arch_efi_call_virt_setup() + sync_kernel_mappings(efi_mm.pgd); + efi_virtmap_load(); // switch_mm + + arch_efi_call_virt(efi.runtime,f,args) + // call efi.runtime.get_time + arch_efi_call_virt_teardown() +``` + +## 小结 + +本文介绍了 RISC-V Linux 内核在加载并启动后的 UEFI 初始化流程,包括从 UEFI 内存映射表到 memblock 分配器的交接过程、UEFI 配置表的部分解析过程,以及 UEFI 运行时服务的初始化和调用过程,希望对你有帮助。 + +## 参考资料 + +- [memblock 内存分配器原理和代码分析][1] + +[1]: https://tinylab.org/riscv-memblock/ diff --git a/articles/images/riscv_uefi/riscv-edk2-boot.png b/articles/images/riscv_uefi/riscv-edk2-boot.png new file mode 100644 index 0000000000000000000000000000000000000000..7a8a6e04f83b5c11565a68c2fde174e0548be22a Binary files /dev/null and b/articles/images/riscv_uefi/riscv-edk2-boot.png differ