[!IMPORTANT]
在 uefi-rs 0.32 更新后,API 进行了一些修改,因此这里针对 0.32 重新编写。 参阅:
引导在英文中为 “boot”,是 bootstrap 的缩写,源自于短语 “Pull oneself up by one’s bootstraps”,即“靠自己振作起来”。 – 维基百科 - 引导程序
Linux 有 GRUB2 和 systemd-boot,Windows 有 Windows Boot Manager,Android 有 U-Boot。
我们也得写一个引导器才行!
UEFI(Unified Extensible Firmware Interface),统一可扩展固件接口,是一个负责连接硬件和软件之间的接口。
本文是为了编写了一个可以加载内核的引导器,因此将对使用 uefi-rs
、 Boot Service
和 Runtime Service
以及一些必要的 Handle
和 Protocol
进行说明,但不会对于 UEFI 本身进行详细的解析,如果对这一方面可以参考 UEFI 手册、罗冰老师的《UEFI 编程实践》和戴正华老师的《UEFI 原理与编程》。
Our mission is to provide safe and performant wrappers for UEFI interfaces, and allow developers to write idiomatic Rust code. – uefi-rs
EDK2 (EFI Development Kit)是 UEFI 的开发工具包,使用 C 语言进行 UEFI 工程编程。uefi-rs 是 rust 语言下的 EDK2 封装,巧妙运用了很多 rust 语言的语言特性,使得开发效率大大提升。
现有大多数的 UEFI 编程资料是基于 C 语言的,使用了很多指针特性来实现功能。在 Rust 中我们有更好的写法抽象和隐藏或安全传递这些指针,因此这里的主要目的是记录 C 语言的写法与 Rust 写法的异同,以便应对阅读参考资料代码时的语言障碍。(如果您有 C / C++ 基础且掌握 Rust 语言那就更好了!)
从数据类型说起:
在 EDK2 中,为了适配多种不同架构不同位数的 CPU 而对 C 语言的数据类型系统进行了封装,这些数据类型基本能够对应到 Rust 的类型系统中,下表是从 UEFI 手册中抽取的一部分,完整表格在这里查看。
EDK2 Type | Rust / uefi-rs Type | Description |
---|---|---|
BOOLEAN | bool | Logical Boolean. 1-byte value containing a 0 for FALSE or a 1 for TRUE. Other values are undefined. |
INTN | iszie | Signed value of native width. (4 bytes on supported 32-bit processor instructions, 8 bytes on supported 64-bit processor instructions, 16 bytes on supported 128-bit processor instructions) |
UINTN | usize | Unsigned value of native width. (4 bytes on supported 32-bit processor instructions, 8 bytes on supported 64-bit processor instructions, 16 bytes on supported 128-bit processor instructions) |
INT8 | i8 | 1-byte signed value. |
UINT8 | u8 | 1-byte unsigned value. |
INT16 | i16 | 2-byte signed value. |
UINT16 | u16 | 2-byte unsigned value. |
INT32 | i32 | 4-byte signed value. |
UINT32 | u32 | 4-byte unsigned value. |
INT64 | i64 | 8-byte signed value. |
UINT64 | u64 | 8-byte unsigned value. |
INT128 | i128 | 16-byte signed value. |
UINT128 | u128 | 16-byte unsigned value. |
CHAR8 | CStr8 | 1-byte character. Unless otherwise specified, all 1-byte or ASCII characters and strings are stored in 8-bit ASCII encoding format, using the ISO-Latin-1 character set. |
CHAR16 | CStr16 | 2-byte Character. Unless otherwise specified all characters and strings are stored in the UCS-2 encoding format as defined by Unicode 2.1 and ISO/IEC 10646 standards. |
其中,CStr8 和 CStr16 可以分别使用宏 cstr8 和 cstr16 进行构建。
此外常用的还有:
EFI_STATUS,用于表达函数返回状态(是否出错,是否有值)。
EFI_HANDLE,即是后续我们会提到的 Handle。
在 UEFI 手册中的接口描述中,使用了一些助记词作为参数的修饰符,如下:
Mnemonic | Description |
---|---|
IN | Datum is passed to the function. |
OUT | Datum is returned from the function. |
OPTIONAL | Passing the datum to the function is optional, and a NULL may be passed if the value is not supplied. |
CONST | Datum is read-only. |
EFIAPI | Defines the calling convention for UEFI interfaces. |
EDK2:
EFI_STATUS EFIAPI main (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
) { }
uefi-rs:
fn main(image_handle: Handle, mut system_table: SystemTable<Boot>) -> Status { }
可以看到 IN 类型数据写法实际上是没有什么区别的,但在 Rust 中能够隐藏指针类型和添加准确的泛型。
在入口中 Image Handle 指向当前 Image(其实也就是当前 EFI 程序),System Table 是一个 UEFI 环境下的全局资源表,存有一些公共数据和函数。
一般来说,在 EDK2 中函数的返回值为 EFISTATUS 类型,(返回的)数据地址会赋值给参数类型为指针的 _OUT 参数中,这意味着调用一个函数的步骤是:
Table
、Service
、Handle
和 Protocol
等对应的数据结构,以函数指针 >
的方式访问函数。以获取 Graphics Output Protocol 为例子:
EDK2:
使用 LocateProtocol 函数获取 Graphics Output Protocol。
其函数原型为:
typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_PROTOCOL) (
IN EFI_GUID *Protocol,
IN VOID *Registration OPTIONAL,
OUT VOID **Interface
);
我们需要关注的是第三个参数 Interface,可以看到是一个指针类型的 OUT 类型参数。
On return, a pointer to the first interface that matches Protocol and Registration. – EFI_LOCATE_PROTOCOL - Interface
因此有代码:
// 声明一个状态,用于接受函数表明执行状态的返回值
EFI_STATUS Status;
// 提前声明一个指针用于指向函数的返回值数据
EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput;
// gBS 是 BootService,通过 SystemTable->BootService 获取
Status = gBS->LocateProtocol(
// gEfiGraphicsOutputProtocolGuid 定义在头文件中,是 Graphics Output Protocol 的 UUID
&gEfiGraphicsOutputProtocolGuid,
NULL,
(VOID **)&GraphicsOutput
);
if (EFI_ERROR(Status)) {
return Status;
}
uefi-rs:
基于 Rust 的特性,可以使用 Result 替换掉 EFI_STATUS 这种需要额外声明一个变量来存放状态的方式。
let graphics_output_protocol_handle = boot::get_handle_for_protocol::<GraphicsOutput>()
// 返回类型为 Result<Handle>
// 这里便于理解直接使用了 unwarp,但在正常编码中,应该使用 map_or 或 expect 等方式显式处理错误。
// 尤其是在 UEFI 这类难于调试的环境下,应该尽可能地留下有用的错误信息
.unwrap();
let mut graphics_output_protocol = boot::open_protocol_exclusive::<GraphicsOutput>(graphics_output_protocol_handle)
// 返回类型为 Result<ScopedProtocol<GraphicsOutputProtocol>>
.unwrap();
…
…
…
…