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`)。
+
+
+
+- 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