欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-05 18:18:30
...

在上一篇文章中我们编写一个基本的操作系统,但是这个操作系统只有很简单的字符输入和输出功能,没有调度,没有内存管理等,但是没关系我们会一一实现他们,现在我们需要解决系统引导启动问题,之前的章节中我们接住了``的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阶段如果系统遇到灾难性错误,系统固件需要提供错误处理和灾难恢复机制

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<Boot>) -> 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的文档比较欠缺)为我们加载内核做准备