简介:

对于戴正华《UEFI原理与编程》书籍的学习笔记

本文旨在以用什么学什么的原则,使用EDK2构建一个运行在UEFI机器上的boot loader,使其可以将内核载入内存并移交控制权。

最终运行情况

image01

需要的知识

UEFI启动/运行过程

UEFI启动/运行过程分为以下部分

  1. SEC:安全验证
  2. PEI:EFI前期初始化
  3. DXE:驱动执行环境
  4. BDS:启动设备选择
  5. TSL:操作系统加载前期
  6. RT:运行时
  7. AL:系统灾难恢复期

编写一个简单的Boot Loader只需要了解其中两个部分,即TSL和RT。

TSL

TSL(Transient System Load)是操作系统加载器(OS Loader)执行的第一阶段,在这一阶段OS Loader作为一个UEFI应用程序运行,系统资源仍然由UEFI内核控制。当启动服务的ExitBootServices()服务被调用后,系统进入Run Time阶段。

TSL阶段之所以称为临时系统,在于它存在的目的就是为操作系统加载器准备执行环境。虽然是临时系统,但其功能已经很强大,已经具备了操作系统的雏形,UEFI Shell是这个临时系统的人机交互界面。正常情况下,系统不会进入UEFI Shell,而是直接执行操作系统加载器,只有在用户干预下或操作系统加载器遇到严重错误时才会进入UEFI Shell。

RT

系统进入RT(Run Time)阶段后,系统的控制权从UEFI内核转交到OS Loader手中,UEFI占用的各种资源被回收到OS Loader,仅有UEFI运行时服务保留给OS Loader和OS使用。随着OS Loader的执行,OS最终取得对系统的控制权。

EDK2

源码: https://github.com/tianocore/edk2

EDK2简介

EDK2时遵循UEFI标准和PI标准的跨平台固件开发环境。我们可以通过EDK2编写UEFI Application,即本文所描述的boot loader。

一个最简单的boot loader所需的文件

.
|-- MyBootLoader.inf
|-- boot.c

其中boot.c为C语言代码,MyBootLoader.inf为EDK2所需的描述文件,其中包括

  1. [Defines] 模块属性
  2. [Sources] 本模块源文件,资源文件
  3. [Packages] 本模块引用到的所有包的包声明文件
  4. [LibraryClasses] 本模块要链接的库模块
  5. [Protocols] 本模块用到的Protocol
  6. [Guids] 本模块用到的GUID
  7. [BuildOptions] 编译和链接选项
  8. [Pcd] 本模块用到的Pcd变量,可被整个UEFI访问(Pcd: Platform Configuration Database全平台配置数据库)
  9. [PcdEx] 本模块用到的Pcd变量,可被整个UEFI访问
  10. [FixedPcd] 本模块用到的Pcd编译期常量
  11. [FeaturePcd] 本模块用到的Pcd常量
  12. [PatchPcd] 本模块用到的Pcd变量,仅本模块可用

一个简单的boot loader的UefiMain如下,LoadKernel函数为使用EDK2所提供的功能所实现的载入内核功能代码。

EFI_STATUS EFIAPI UefiMain (
    IN EFI_HANDLE           ImageHandle,
    IN EFI_SYSTEM_TABLE     *SystemTable
){
    EFI_STATUS status;
    void (*KernelEntry)();

    // load kernel
    status = LoadKernel(
        L"kernel.elf", 
        &KernelEntry
    );
    if(EFI_ERROR(status)){
        Log(L"Cannot load kernel...\n");
        return EFI_LOAD_ERROR;
    }
    Log(L"Load kernel success...\n");
    DebugLog(L"kernel entry is %x\n", (UINTN)KernelEntry);
    

    // exit BS
    UINTN mem_map_size = 0;
    EFI_MEMORY_DESCRIPTOR* mem_map = 0;;
    UINTN map_key = 0;
    UINTN des_size = 0;
    UINT32 des_version = 0;
    gBS->GetMemoryMap(&mem_map_size, mem_map, &map_key, &des_size, &des_version);
    gBS->ExitBootServices(ImageHandle, map_key);
    
    KernelEntry();
 
    // kernel will never return
    while(1){}    
    return EFI_SUCCESS;
}

UefiMain的参数:

  1. ImageHandle:Image句柄
  2. SystemTable:系统表,其中包含一些信息和最重要的两个服务BootServices与RuntimeServices的指针。这两个服务也会以gBS与gRT全局变量的形式给出,与系统表中成员的指向一样。

BootServices

todo

RuntimeServices

todo

设计

通常,我们编写一个efi application并将其置于硬盘引导分区的/efi/boot/BOOTxx.efi位置,在UEFI启动时会自动将这个efi application启动。

在我们编写的UEFI Application的UEFI Main执行的时候,系统已经位于TSL阶段。

以最简单的设计,将内存映像与boot loader都放入引导分区,即该分区的fat32文件系统中,使用UEFI原生支持的文件系统读取内核即可

这样,一个最简单的boot loader只需要:

  1. 轮询所有支持fat32文件系统的设备(分区)
  2. 找到内核映像,将其载入内存
  3. 做一些准备工作,如记录当前内存映射状况等
  4. 调用UEFI提供的接口让UEFI系统进入RT阶段
  5. 跳转至载入内存的内核,转移控制权

实现

todo