在上一篇文章中我们编写一个基本的操作系统,但是这个操作系统只有很简单的字符输入和输出功能,没有调度,没有内存管理等,但是没关系我们会一一实现他们,现在我们需要解决系统引导启动问题,之前的章节中我们接住了``的Bootloader
库来完成系统引导,但是BootLoader
库只提供了最基本的功能,并且是BOIS
引导启动,为了让我们的系统更具现代化一些,我们使用UEFI
引导启动系统
在本章中我们只讲述UEFI
的基本介绍,基本的组件,以及加载系统的步骤等,着重讲述使用Rust
来完成UEFI
的编程,有关UEFI
架构的内容可以参考老狼的文章
我们要了解UEFI为何物之前需要了解一下BIOS的内容,BIOS全称为Base Input Output System
(基本输入/输出系统),它一组存储在主板ROM中的程序代码主要功能有
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的缺点
UEFI(Unified Extensible Firmware Interface)中文统一可扩展固件接口,UEFI主要定义了操作系统和平台固件之间的接口,UEFI只是一种标准,具体的实现由其他公司或开源组成提供
UEFI实现一般分为两个部分
UEFI启动过程
UEFI系统从加电到关机分为7个阶段
HOB
(Handoff Block)列表,最终将控制权交给DXEUEFI映像是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位处理器的调用约定运行
Boot Manager或其他UEFI应用程序可以加载按照规范编写的应用程序。 要加载UEFI应用程序,固件会为Application Image分配足够的内存,随后将Application Image中的section拷贝到固件所分配的内存中,根据需要进行重定位处理, 完成之后,根据image类型将被分配的内存设置CODE和DATA类型,然后将控制权转移到应用程序入口处当应用程序结束后返回,或者当它调用EFI_BOOT_SERVICES.Exit()时,UEFI应用程序将从内存中卸载,并将控制权返回给加载该UEFI应用程序的UEFI组件。
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负责系统的继续运行。
运行时服务的主要目的是从OS中抽象平台硬件实现的一小部分。运行时服务功能在引导过程中可用,并且在运行时也可用,只要OS切换到平坦物理寻址模式(虚拟地址=物理地址)即可使用运行时调用,但是,如果OS加载程序或OS使用SetVirtualAddressMap()服务,操作系统将只能以虚拟寻址模式调用运行时服务。所有运行时接口均为非阻塞接口,可以根据需要在禁用中断的情况下调用。在所有情况下,运行时服务使用的内存都必须保留,并且操作系统不应该使用。运行时服务内存始终可用于UEFI功能,并且不应该被OS或其组件直接操纵。UEFI负责定义运行时服务使用的硬件资源,因此OS可以在调用运行时服务时与这些资源同步,或者保证OS不会使用这些资源。
UEFI规范中定义的所有功能都是由C编译器以及架构决定的,调用约定中的指针调用的。 在通过EFI_RUNTIME_SERVICES和EFI_BOOT_SERVICES表中找到各种全局UEFI功能的指针。 在所有情况下,所有指向UEFI功能的指针都使用EFIAPI进行强制转换。 这允许每种体系结构的编译器提供适当的编译器关键字,以实现所需的调用约定。 当将指针参数传递给引导服务,运行时服务和协议接口时,调用者具有以下职责:
任何功能或协议都可以返回任何有效的状态码。
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为最低有效位。 |
所有函数都使用C语言调用约定来调用。 跨函数调用易失性通用寄存器是eax,ecx和edx。 所有其他通用寄存器都是非易失性的,并由目标函数保留。 此外,除非函数定义另有规定,否则将保留所有其他寄存器。
在OS调用ExitBootServices()之前,固件启动服务和运行时服务以以下处理器执行模式运行:
NO_EXECUTE
页表。为了使操作系统使用任何UEFI运行时服务,必须保证以下状态:
RUNTIME_CODE
和RUNTIME_DATA
的所有内存我们使用的是rust-osdev
的uefi-rs
库,rust-osdev
为Rust提供了x86_64-unknown-uefi编译目标,因此我们只需要指定编译目标即可编译成.efi
文件,我们使用的UEFI标准实现是EDK2,如果使用QEMU来启动或调试需要使用OVMF
(开放虚拟机固件)
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服务进行初始化,初始化完毕后我们可以
$ git clone https://github.com/tianocore/edk2.git
$ cd edk2
// EDK2有一些依赖库比如openssl等
$ git submodule update --init
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 # 构建规则文件
sudo apt-get install build-essential uuid-dev
edk2$ cd BaseTools
edk2/BaseTools$ make
// 编译完毕后Source以下
edk2$ source edksetup.sh
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是一种约定,可以通过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启动系统需要经过以下步骤
.efi
文件,在执行前需要建立一个efi/boot
目录并且使用将.efi
文件放入efi/boot
文件夹中,如果我们使用的是uefi shell(QEMU中直接使用OVMF进入UEFI shell)在执行.efi
文件后会把.efi
文件加载内存中生成Image对象,然后启动这个Image对象,在启动Image对象时将会找出Image的入口并执行入口函数,虽然uefi-rs
的支持的功能不是特别多但是足以满足我们系统的使用
在下一篇文章中我们介绍uefi-rs
的及基本数据结构以及对应的使用方式(uefi-rs
的文档比较欠缺)为我们加载内核做准备