使用Rust开发操作系统(UEFI基本介绍)
UEFI基本介绍
在上一篇文章中我们编写一个基本的操作系统,但是这个操作系统只有很简单的字符输入和输出功能,没有调度,没有内存管理等,但是没关系我们会一一实现他们,现在我们需要解决系统引导启动问题,之前的章节中我们接住了``的Bootloader
库来完成系统引导,但是BootLoader
库只提供了最基本的功能,并且是BOIS
引导启动,为了让我们的系统更具现代化一些,我们使用UEFI
引导启动系统
关于UEFI
在本章中我们只讲述UEFI
的基本介绍,基本的组件,以及加载系统的步骤等,着重讲述使用Rust
来完成UEFI
的编程,有关UEFI
架构的内容可以参考老狼的文章
BIOS
我们要了解UEFI为何物之前需要了解一下BIOS的内容,BIOS全称为Base Input Output System
(基本输入/输出系统),它一组存储在主板ROM中的程序代码主要功能有
- 自检程序,用于开机时对硬件的检测
- 系统初始化,包括对硬件,BIOS中断向量等初始化
- 基本的IO处理
- 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的缺点
- 开发效率较低(根据程序员):大部分BIOS代码使用汇编开发并且代码与设备的耦合性太高
- 功能扩展性差:增加硬件功能时必须将16位代码放置在0x0C0000 ~ 0x0DFFF区间,并且设置中断处理程序
- 性能差: BIOS的IO操作需要通过中断来完成,并且BIOS没有提供异步工作模式
- 安全性:BOIS运行过程中对代码安全性没有考虑
- 不支持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(系统灾难恢复期)
- SEC(Security Phase): 平台初始化的第一个阶段,计算机系统加电后进入这个阶段,他将主要完成接受并初始化系统启动和启动信号,并初始化临时存储区域,在SEC阶段作为可信任系统的根,随后将系统的参数传给下一个阶段,SEC分为两大部分,Reset Vector阶段,调用SEC入口地址进入SEC,启动Reset Vector会进入固件入口,从实模式转为32为平坦模式,定位固件中的BFV(Boot Frimware Volume),如果64位系统,从32位模式转为64位模式,调用SEC入口
- PEI(Pre-EFI Initialization)主要功能时为DXE准备执行环境,需要将DXE的信息组成
HOB
(Handoff Block)列表,最终将控制权交给DXE - DXE(Driver Execution Environment)执行系统初始化工作,在次阶段内存可以使用,DXE驱动通过Protocol通信,我们可以使用Protocol提供的服务,当所有的Driver执行完毕后,系统初始化完成,随后进入BDS阶段
- BDS(Boot Devices Selection)主要功能时执行启动策略,主要包括初始化控制台设备,加载设备驱动,根据系统设置加载和执行启动项,用于选中某个启动项后,OS Loader启动,系统进入TSL阶段
- TSL(Transient System Load)为系统加载器执行的第一个阶段,在这一阶段系统加载器作为UEFI应用程序运行,系统资源仍由UEFI内核控制,当启动服务调用ExitBootServer()后系统进入Runtime阶段
- RT(Run Time)系统的控制权从UEFI内核转给系统加载器中,UEFI占用的各种资源被回收到系统加载器中,仅保留UEFI运行服务和OS,最后OS取得最终控制权
- AL(After Life):在RT阶段如果系统遇到灾难性错误,系统固件需要提供错误处理和灾难恢复机制
Rust中的UEFI
我们使用的是rust-osdev
的uefi-rs
库,rust-osdev
为Rust提供了x86_64-unknown-uefi编译目标,因此我们只需要指定编译目标即可编译成.efi
文件,我们使用的UEFI标准实现是EDK2,如果使用QEMU来启动或调试需要使用OVMF
(开放虚拟机固件)
UEFI的基础服务如下
- 系统表: 系统表提供了用户空间与内核空间的通道(UEFI内核)UEFI应用程序和驱动通过系统表才能访问硬件资源和IO设备
- 启动服务:在系统启动过程中,系统资源通过启动服务来管理,系统进入DXE阶段时启动服务表,系统服务分为以下几类
- UEFI事件服务:有了事件才能在UEFI系统内执行异步并发操作
- 内存管理服务:主要提供内存分配与释放,管理系统内存映射
- Protocol管理服务:提供Protocol安装,注册和卸载
- Protocol使用服务:Protocol的打开与关闭
- 驱动管理服务: 提供驱动的安装卸载服务
- Image服务i,包括加载,卸载,启动,退出UEFI应用程序或驱动
- ExitBootService:用于结束启动服务,执行成功后系统进入RT阶段
- 其他服务
- 运行时服务:从进入DXE阶段运行时服务被初始化,直到操作系统结束
- 时间服务:读取/设定系统时间,读取设定系统从睡眠中唤醒的时间
- 读写内存变量:读取设置系统变量,例如指定启动项顺序
- 虚拟内存服务:将物理地址转为虚拟地址
- 其他服务
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<Boot>) -> Status {
// 初始化
uefi_services::init(&st).expect_success("Failed to initialize utilities");
....
}
efi_main
函数相当于普通应用程序的main
函数,在进入入口后我们需要对UEFI服务进行初始化,初始化完毕后我们可以
OVMF固件制作
- 下载EDK2
$ git clone https://github.com/tianocore/edk2.git
$ cd edk2
// EDK2有一些依赖库比如openssl等
$ git submodule update --init
- 指定编译平台,在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 # 构建规则文件
- 编译EDK2工具链 安装依赖环境
sudo apt-get install build-essential uuid-dev
- 开始编译
edk2$ cd BaseTools
edk2/BaseTools$ make
// 编译完毕后Source以下
edk2$ source edksetup.sh
- 编译OVMF 编译64位固件
edk2$ build -a X64 -p OvmfPkg/OvmfPkgX64.dsc -t GCC5
- 编译后在
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启动系统需要经过以下步骤
- 制作系统固件,我们将代码最终编译成
.efi
文件,在执行前需要建立一个efi/boot
目录并且使用将.efi
文件放入efi/boot
文件夹中,如果我们使用的是uefi shell(QEMU中直接使用OVMF进入UEFI shell)在执行.efi
文件后会把.efi
文件加载内存中生成Image对象,然后启动这个Image对象,在启动Image对象时将会找出Image的入口并执行入口函数, - 进入到执行入口后我们需要对基本的服务进行初始化,初始化完毕后我们需要检测系统的运行环境并收集系统所需的参数
- 随后我们使用SimpleFileSystem来寻找系统内核文件并加载到内存中,解析内核文件找到内核入口
- 最后我们调用ExitBootService结束启动过程,跳转到内核入口
虽然uefi-rs
的支持的功能不是特别多但是足以满足我们系统的使用
下一步要做什么
在下一篇文章中我们介绍uefi-rs
的及基本数据结构以及对应的使用方式(uefi-rs
的文档比较欠缺)为我们加载内核做准备
上一篇: [.NET] 引用类型和值类型
下一篇: SQLAlchemy 事务回滚