使用Rust开发操作系统(UEFI基本介绍)

UEFI基本介绍

  • 关于UEFI
    • BIOS
    • UEFI介绍
    • 引导管理
      • UEFI Image
    • UEFI 应用程序
    • OS Loader
  • UEFI运行时服务
  • 调用约定
    • 调用约定的数据类型
    • IA-32架构调用约定
    • Rust中的UEFI
      • UEFI入口
      • OVMF固件制作
      • Protocol
    • UEFI启动系统过程
  • 下一步要做什么

在上一篇文章中我们编写一个基本的操作系统,但是这个操作系统只有很简单的字符输入和输出功能,没有调度,没有内存管理等,但是没关系我们会一一实现他们,现在我们需要解决系统引导启动问题,之前的章节中我们接住了``的Bootloader库来完成系统引导,但是BootLoader库只提供了最基本的功能,并且是BOIS引导启动,为了让我们的系统更具现代化一些,我们使用UEFI引导启动系统

关于UEFI

在本章中我们只讲述UEFI的基本介绍,基本的组件,以及加载系统的步骤等,着重讲述使用Rust来完成UEFI的编程,有关UEFI架构的内容可以参考老狼的文章

BIOS

我们要了解UEFI为何物之前需要了解一下BIOS的内容,BIOS全称为Base Input Output System(基本输入/输出系统),它一组存储在主板ROM中的程序代码主要功能有

  1. 自检程序,用于开机时对硬件的检测
  2. 系统初始化,包括对硬件,BIOS中断向量等初始化
  3. 基本的IO处理
  4. CMOS设置程序

BIOS运行在16位模式下,实模式下最大可寻址范围为1MB其中0x0C0000~0x0FFFFF留给BIOS使用

启动过程
当按下电源按钮后CPU跳转到0xFFFF0处执行(一般为跳转指令),跳转到BIOS执行入口后BIOS进行加电自检(Power of self Test P.O.S.T.)在自检过程中如果发现硬件错误则通过蜂鸣器报警,P.O.S.T.检测通过后进行检测硬件并将硬件设备初始化,最后根据启动顺序从设备启动,将引导记录通过BIOS中断读入内存BootLoader(执行的地址从0x7c00开始)

BIOS的缺点

  1. 开发效率较低(根据程序员):大部分BIOS代码使用汇编开发并且代码与设备的耦合性太高
  2. 功能扩展性差:增加硬件功能时必须将16位代码放置在0x0C0000 ~ 0x0DFFF区间,并且设置中断处理程序
  3. 性能差: BIOS的IO操作需要通过中断来完成,并且BIOS没有提供异步工作模式
  4. 安全性:BOIS运行过程中对代码安全性没有考虑
  5. 不支持2TB以上的地址引导:BIOS采取32位地址,因而引导扇区的最大逻辑块地址为2^32(2^32 x512=2TB)

UEFI介绍

UEFI(Unified Extensible Firmware Interface)中文统一可扩展固件接口,UEFI主要定义了操作系统和平台固件之间的接口,UEFI只是一种标准,具体的实现由其他公司或开源组成提供

UEFI实现一般分为两个部分

  • 平台初始化
  • 固件 操作系统接口

UEFI启动过程
UEFI系统从加电到关机分为7个阶段

  • SEC(安全验证)
  • PEI(EIF前期初始化)
  • DXE(驱动执行环境)
  • BDS(启动设备选择)
  • TSL(操作系统前期加载)
  • RT(运行时)
  • AL(系统灾难恢复期)
  1. SEC(Security Phase): 平台初始化的第一个阶段,计算机系统加电后进入这个阶段,他将主要完成接受并初始化系统启动和启动信号,并初始化临时存储区域,在SEC阶段作为可信任系统的根,随后将系统的参数传给下一个阶段,SEC分为两大部分,Reset Vector阶段,调用SEC入口地址进入SEC,启动Reset Vector会进入固件入口,从实模式转为32为平坦模式,定位固件中的BFV(Boot Frimware Volume),如果64位系统,从32位模式转为64位模式,调用SEC入口
  2. PEI(Pre-EFI Initialization)主要功能时为DXE准备执行环境,需要将DXE的信息组成HOB(Handoff Block)列表,最终将控制权交给DXE
  3. DXE(Driver Execution Environment)执行系统初始化工作,在次阶段内存可以使用,DXE驱动通过Protocol通信,我们可以使用Protocol提供的服务,当所有的Driver执行完毕后,系统初始化完成,随后进入BDS阶段
  4. BDS(Boot Devices Selection)主要功能时执行启动策略,主要包括初始化控制台设备,加载设备驱动,根据系统设置加载和执行启动项,用于选中某个启动项后,OS Loader启动,系统进入TSL阶段
  5. TSL(Transient System Load)为系统加载器执行的第一个阶段,在这一阶段系统加载器作为UEFI应用程序运行,系统资源仍由UEFI内核控制,当启动服务调用ExitBootServer()后系统进入Runtime阶段
  6. RT(Run Time)系统的控制权从UEFI内核转给系统加载器中,UEFI占用的各种资源被回收到系统加载器中,仅保留UEFI运行服务和OS,最后OS取得最终控制权
  7. AL(After Life):在RT阶段如果系统遇到灾难性错误,系统固件需要提供错误处理和灾难恢复机制

引导管理

UEFI Image

UEFI映像是UEFI定义的一类文件,其中包含可执行代码。UEFI Image是一类包含可执行代码的文件,UEFI的区别在Image Header中的Magic Number不同

UEFI使用PE32的Image格式的子集PE32+,PE32+ Image中Image Header与普通的PE32可执行文件不同,"+"表示增加了对PE32格式的64位重定位的扩展,Image分为不同的类型,下表为不同架构的Image镜像类型

架构 Machine Type
IA32 0x014c
IA64 0x0200
EBC 0x0EBC
x64 0x8664
ARMTHUMB_MIXED 0x01C2
AARCH64 0xAA64
RISCV32 0x5032
RISCV64 0x5064
RISCV128 0x5128

Image类型之间的区别是固件将Image加载到的内存类型,以及image的加载入口,退出或返回时所采取的操作当从映像的入口点返回控制权时,UEFI应用程序Image始终会被卸载。仅当使用UEFI错误代码传回控制时,才会卸载UEFI驱动程序映像。

UEFI映像通过EFI_BOOT_SERVICES.LoadImage()引导服务加载到内存中。该服务将PE32+格式的Image加载到内存中。PE32+加载程序将PE32+ Image的所有section加载到内存中。一旦将Image加载到内存中并进行适当的调整,随后在使用AddressOfEntryPointreference加载的映像,应用程序会根据所支持的32位,64位或128位处理器的调用约定运行

UEFI 应用程序

Boot Manager或其他UEFI应用程序可以加载按照规范编写的应用程序。 要加载UEFI应用程序,固件会为Application Image分配足够的内存,随后将Application Image中的section拷贝到固件所分配的内存中,根据需要进行重定位处理, 完成之后,根据image类型将被分配的内存设置CODE和DATA类型,然后将控制权转移到应用程序入口处当应用程序结束后返回,或者当它调用EFI_BOOT_SERVICES.Exit()时,UEFI应用程序将从内存中卸载,并将控制权返回给加载该UEFI应用程序的UEFI组件。

OS Loader

UEFI OS Loader是一种特殊类型的UEFI应用程序,通常会从固件中接管系统的控制权。加载后,UEFI OS加载程序的行为与任何其他UEFI应用程序相同,它只能使用从固件提供的内存分配释放功能,并且只能使用UEFI服务和协议来访问固件提供的可用的设备
如果UEFI OS加载程序遇到问题并且无法正确加载其操作系统,则它需要释放所有分配的资源,并通过Boot Service Exit()调用将控制权返回给固件。 Exit()调用允许同时返回错误代码和ExitData。 ExitData包含字符串和要返回的OS加载程序特定的数据
如果UEFI OS加载程序成功加载了其操作系统,则可以使用引导服务EFI_BOOT_SERVICES.ExitBootServices()来控制系统。调用成功后将停止系统中的所有引导服务,包括内存管理,并且由UEFI OS Loader负责系统的继续运行。

UEFI运行时服务

运行时服务的主要目的是从OS中抽象平台硬件实现的一小部分。运行时服务功能在引导过程中可用,并且在运行时也可用,只要OS切换到平坦物理寻址模式(虚拟地址=物理地址)即可使用运行时调用,但是,如果OS加载程序或OS使用SetVirtualAddressMap()服务,操作系统将只能以虚拟寻址模式调用运行时服务。所有运行时接口均为非阻塞接口,可以根据需要在禁用中断的情况下调用。在所有情况下,运行时服务使用的内存都必须保留,并且操作系统不应该使用。运行时服务内存始终可用于UEFI功能,并且不应该被OS或其组件直接操纵。UEFI负责定义运行时服务使用的硬件资源,因此OS可以在调用运行时服务时与这些资源同步,或者保证OS不会使用这些资源。

调用约定

UEFI规范中定义的所有功能都是由C编译器以及架构决定的,调用约定中的指针调用的。 在通过EFI_RUNTIME_SERVICES和EFI_BOOT_SERVICES表中找到各种全局UEFI功能的指针。 在所有情况下,所有指向UEFI功能的指针都使用EFIAPI进行强制转换。 这允许每种体系结构的编译器提供适当的编译器关键字,以实现所需的调用约定。 当将指针参数传递给引导服务,运行时服务和协议接口时,调用者具有以下职责:

  • 调用者负责传递引用物理内存位置的指针。 如果传递的指针未指向物理内存位置(即内存映射的I / O区域),可能产生不可预测的问题,并且系统可能会因此暂停。
  • 调用者负责传递经过正确对齐的指针。 如果将未对齐的指针传递给函数,可能产生不可预测的问题,并且系统可能会因此暂停。
  • 除非明确允许,否则调用者不能将NULL当做参数传递给函数。 如果将NULL指针传递给函数,可能产生不可预测的问题,并且系统可能会因此暂停。
  • 如果函数返回错误,则调用者不应对指针参数的状态做出任何假设。
  • 调用者不得传递大于本机大小的值的结构,并且调用者必须按引用(通过指针)传递这些结构。 如果在堆栈上传递大于位宽(32位处理器上为4字节;64位处理器上为8字节)的结构将产生未知的结果。

任何功能或协议都可以返回任何有效的状态码。

UEFI模块的所有公共接口必须遵循UEFI调用约定。 公共接口包括Image入口点,UEFI事件处理程序和协议成员函数。 对于非公共接口(例如私有函数和静态库调用)不需要遵循UEFI调用约定,并且可以由编译器进行优化

调用约定的数据类型

除非另有说明,否则所有数据类型都是自然对齐的。 结构体类型对齐方式为该结构体最大数据成员基准的边界上对齐,并且隐式填充内部数据以实现自然对齐。UEFI接口传递或返回的指针的值必须为自然对齐。

标识符 描述
BOOLEAN
INTN 有符号值的宽度(32位处理器为4字节。 64位处理器为8字节。 128位处理器为16字节)
UINTN 无符号值的宽度。(32位处理器为4字节。 64位处理器为8字节。 128位处理器为16字节)
INT8 1字节(有符号)
UINT8 1字节(无符号)
INT16 2字节(有符号)
UINT16 2字节(无符号)
INT32 4字节(有符号)
UINT32 4字节(无符号)
INT64 8字节(有符号)
UINT64 8字节(无符号)
INT128 16字节(有符号)
UINT128 16字节(无符号)
CHAR8 1字节的字符。除非另有说明,否则所有1字节或ASCII字符和字符串均使用ISO-Latin-1字符集以8位ASCII编码格式存储.
CHAR16 2字节字符。除非另有说明,否则所有字符和字符串都以Unicode 2.1和ISO / IEC 10646标准定义的UCS-2编码格式存储。
VOID 未声明的类型
EFI_GUID 包含唯一标识符的128位缓冲区。除非另有说明,否则在64位边界上对齐。
EFI_STATUS 状态码。类型为UINTN
EFI_HANDLE 相关接口的集合。类型为 VOID *。
EFI_EVENT 处理事件结构。类型为VOID *.
EFI_LBA 逻辑块地址。类型为UINT64.
EFI_TPL 任务优先级。类型为UINTN.
EFI_MAC_ADDRESS 包含网络访问控制地址的32字节缓冲区。
EFI_IPv4_ADDRESS 4字节缓冲区。 IPv4互联网协议地址。
EFI_IPv6_ADDRESS 16字节缓冲区。 IPv6互联网协议地址。
EFI_IP_ADDRESS 16字节缓冲区在4字节边界上对齐。 IPv4或IPv6 Internet协议地址。
<枚举类型> 标准ANSI C枚举类型声明的元素。类型为INT32或UINT32,ANSI C没有定义枚举的符号大小,因此切勿在结构中使用它们,作为参数传递给函数时,ANSI C可以使INT32或UINT32可互换。
sizeof(VOID *) 32位处理器为4字节。 64位处理器为8字节。 128位处理器为16字节。
位域 位域的排序方式是使位0为最低有效位。

IA-32架构调用约定

所有函数都使用C语言调用约定来调用。 跨函数调用易失性通用寄存器是eax,ecx和edx。 所有其他通用寄存器都是非易失性的,并由目标函数保留。 此外,除非函数定义另有规定,否则将保留所有其他寄存器。
在OS调用ExitBootServices()之前,固件启动服务和运行时服务以以下处理器执行模式运行:

  • 单处理器模式可参考英特尔64和IA-32架构软件开发人员手册第3卷第8.4章
  • 系统运行在64位长模式
  • 启用了分页模式,UEFI所定义内存空间都进行了映射(虚拟地址等于物理地址),尽管某些区域的属性可能没有全部读取,写入和执行属性,也可能没有标记以进行平台保护 。到其他区域的映射是不确定的,并且可能因实现方式而异。
  • 选择子设置为flat,否则不使用。
  • 启用了中断-UEFI引导服务计时器功能除外,但不支持任何中断服务(所有加载的设备驱动程序均通过“轮询”进行同步服务。)
  • EFLAG中的DF标记已复位
  • 其他通用标志寄存器未定义
  • 128KB或更多可用堆栈空间
  • 堆栈必须对齐16字节。 堆栈在映射中可能被标记为NO_EXECUTE页表。
  • 浮点控制字必须初始化为0x037F(所有带掩码的例外,双精度扩展)
  • 多媒体扩展控制字(如果支持)必须初始化为0x1F80
  • CR0.EM必须为零
  • CR0.TS必须为零

为了使操作系统使用任何UEFI运行时服务,必须保证以下状态:

  • 保留内存映射中标记为RUNTIME_CODERUNTIME_DATA的所有内存
  • 在64位长模式下
  • 启用分页
  • 如果UEFI OS加载程序或OS使用SetVirtualAddressMap()在虚拟地址空间中重新定位运行时服务,则不必满足此条件。
  • EFLAG中的DF标记已复位
  • 4KB以上可用的堆栈空间
  • 堆栈必须对齐16字节
  • 浮点控制字必须初始化为0x037F
  • CR0.EM必须为零
  • CR0.TS必须为零
  • 调用者可以自行决定是否禁用或启用中断。
  • 引导时加载的ACPI表可以包含EfiACPIReclaimMemory(推荐)或EfiACPIMemoryNVS类型。 ACPI FACS必须包含EfiACPIMemoryNVS类型。
  • 系统固件不得为EfiACPIReclaimMemory或EfiACPIMemoryNVS类型的任何内存描述符进行虚拟映射
  • EfiACPIReclaimMemory和EfiACPIMemoryNVS类型的EFI内存描述符必须在4 KB边界上对齐,并且必须是4 KB的倍数。
  • 设置了EFI_MEMORY_RUNTIME位的EFI_MEMORY_DESCRIPTOR虚拟映射的任何UEFI内存描述符都必须在4 KB边界上对齐,并且大小必须为4 KB的倍数。

Rust中的UEFI

我们使用的是rust-osdevuefi-rs库,rust-osdev为Rust提供了x86_64-unknown-uefi编译目标,因此我们只需要指定编译目标即可编译成.efi文件,我们使用的UEFI标准实现是EDK2,如果使用QEMU来启动或调试需要使用OVMF(开放虚拟机固件)

UEFI的基础服务如下

  1. 系统表: 系统表提供了用户空间与内核空间的通道(UEFI内核)UEFI应用程序和驱动通过系统表才能访问硬件资源和IO设备
  2. 启动服务:在系统启动过程中,系统资源通过启动服务来管理,系统进入DXE阶段时启动服务表,系统服务分为以下几类
    1. UEFI事件服务:有了事件才能在UEFI系统内执行异步并发操作
    2. 内存管理服务:主要提供内存分配与释放,管理系统内存映射
    3. Protocol管理服务:提供Protocol安装,注册和卸载
    4. Protocol使用服务:Protocol的打开与关闭
    5. 驱动管理服务: 提供驱动的安装卸载服务
    6. Image服务i,包括加载,卸载,启动,退出UEFI应用程序或驱动
    7. ExitBootService:用于结束启动服务,执行成功后系统进入RT阶段
    8. 其他服务
  3. 运行时服务:从进入DXE阶段运行时服务被初始化,直到操作系统结束
    1. 时间服务:读取/设定系统时间,读取设定系统从睡眠中唤醒的时间
    2. 读写内存变量:读取设置系统变量,例如指定启动项顺序
    3. 虚拟内存服务:将物理地址转为虚拟地址
    4. 其他服务

UEFI入口

在Rust中SXE入口声明如下

#![no_std]
#![no_main]
#![feature(asm)]
#![feature(slice_patterns)]
#![feature(abi_efiapi)]
use uefi::prelude::*;

#[entry]
fn efi_main(image: Handle, st: SystemTable) -> Status {
    // 初始化
    uefi_services::init(&st).expect_success("Failed to initialize utilities");
    ....
}

efi_main函数相当于普通应用程序的main函数,在进入入口后我们需要对UEFI服务进行初始化,初始化完毕后我们可以

OVMF固件制作

  1. 下载EDK2
$ git clone https://github.com/tianocore/edk2.git
$ cd edk2
// EDK2有一些依赖库比如openssl等
$ git submodule update --init
  1. 指定编译平台,在edk2/Conf/target.txt更改平台
    例如Ubuntu(64)
ACTIVE_PLATFORM       =  EmulatorPkg/EmulatorPkg.dsc   
TARGET                = DEBUG # 编译目标 
TARGET_ARCH           = IA32 # 目标平台
TOOL_CHAIN_CONF       = Conf/tools_def.txt       # 工具配置文件      
TOOL_CHAIN_TAG        = GCC5  # 使用编译器 MVSC 支持MSVC
# MAX_CONCURRENT_THREAD_NUMBER = 1
BUILD_RULE_CONF = Conf/build_rule.txt # 构建规则文件
  1. 编译EDK2工具链 安装依赖环境
sudo apt-get install build-essential uuid-dev
  1. 开始编译
edk2$ cd BaseTools
edk2/BaseTools$ make
// 编译完毕后Source以下
edk2$ source edksetup.sh
  1. 编译OVMF 编译64位固件
edk2$ build -a X64 -p OvmfPkg/OvmfPkgX64.dsc -t GCC5
  1. 编译后在edk2/Build/Ovmfla32/DEBUG_GCC5/FV下面会生成OVMF.fd,OVMF_CODE.fd, OVMF_VARS.fd等文件这些文件我们后面会使用到

Protocol

Protocol是一种约定,可以通过BootServices.locate_protocol()来获取对应的Protocol,每个Protocol必须有一个唯一的UUID,每一个Protocol提供了一种功能,例如

#[repr(C)]
#[unsafe_guid("964e5b22-6459-11d2-8e39-00a0c969723b")]
#[derive(Protocol)]
pub struct SimpleFileSystem {
    revision: u64,
    open_volume: extern "efiapi" fn(this: &mut SimpleFileSystem, root: &mut *mut FileImpl) -> Status,
}

964e5b22-6459-11d2-8e39-00a0c969723b就是SimpleFileSystemProtocol的UUID, SimpleFileSystem可以访问FAT-12 / 16/32等文件系统,后续我们会介绍各种各样的Protocol

UEFI启动系统过程

我们要通过UEFI启动系统需要经过以下步骤

  1. 制作系统固件,我们将代码最终编译成.efi文件,在执行前需要建立一个efi/boot目录并且使用将.efi文件放入efi/boot文件夹中,如果我们使用的是uefi shell(QEMU中直接使用OVMF进入UEFI shell)在执行.efi文件后会把.efi文件加载内存中生成Image对象,然后启动这个Image对象,在启动Image对象时将会找出Image的入口并执行入口函数,
  2. 进入到执行入口后我们需要对基本的服务进行初始化,初始化完毕后我们需要检测系统的运行环境并收集系统所需的参数
  3. 随后我们使用SimpleFileSystem来寻找系统内核文件并加载到内存中,解析内核文件找到内核入口
  4. 最后我们调用ExitBootService结束启动过程,跳转到内核入口

虽然uefi-rs的支持的功能不是特别多但是足以满足我们系统的使用

下一步要做什么

在下一篇文章中我们介绍uefi-rs的及基本数据结构以及对应的使用方式(uefi-rs的文档比较欠缺)为我们加载内核做准备

你可能感兴趣的:(使用Rust开发操作系统)