*-unknown-uefi
层级:2
用于应用程序、驱动程序和核心 UEFI 二进制文件的统一可扩展固件接口 (UEFI) 目标。
可用目标
aarch64-unknown-uefi
i686-unknown-uefi
x86_64-unknown-uefi
目标维护者
- David Rheinsberg (@dvdhrm)
- Nicholas Bishop (@nicholasbishop)
要求
所有 UEFI 目标都可以通过交叉编译用作 no-std
环境。std
支持存在,但尚不完整且非常新。如果用户提供分配器或使用 std,则支持 alloc
。不支持任何主机工具。
UEFI 环境类似于 Microsoft Windows 的环境,但有一些细微的差异。因此,针对 UEFI 的交叉编译使用与针对 Windows 的交叉编译相同的工具。目标二进制文件采用 PE32+ 编码,调用约定因架构而异,但与 Windows 使用的约定匹配(如果架构受 Windows 支持)。特殊的 efiapi
Rust 调用约定为目标平台选择正确的 ABI(至少在 Intel 目标上,extern "C"
是不正确的)。规范中有一节详细介绍了不同的支持的调用约定,如果需要更多详细信息,请参考该节。
默认情况下,MMX、SSE 和其他 FP 单元被禁用,以允许编译在设置这些单元之前运行的核心 UEFI 代码。可以通过 rustc 命令行标志覆盖单个编译的此设置。但是,并非所有固件都能正确配置这些单元,因此需要仔细检查。
作为 PE32+ 的原生,二进制文件是位置相关的,但如果它们所需的位置不可用,则可以在运行时重新定位。代码必须是静态链接的。不支持动态链接。代码通过 UEFI 接口共享,而不是动态链接。此外,UEFI 禁止在除引导 CPU/线程以外的任何东西上运行代码,也不允许使用中断(除了定时器中断)。设备驱动程序需要使用轮询方法。
UEFI 使用单个地址空间来运行所有代码。多个应用程序可以同时加载,并通过单个堆栈上的协作式多任务处理进行调度。
默认情况下,UEFI 目标使用 LLVM 链接器 lld
的 link
风格将二进制文件链接到以 *.efi
为后缀的最终 PE32+ 文件中。PE 子系统设置为 EFI_APPLICATION
,但可以通过将 /subsystem:<...>
传递给链接器来修改。类似地,入口点设置为 efi_main
,但可以通过 /entry:<...>
更改。恐慌策略设置为 abort
,
UEFI 规范可在网上免费获取:UEFI 规范目录
为 UEFI 目标构建 Rust
可以通过在 rustc
构建配置中启用它们来为 UEFI 目标构建 Rust。请注意,您只能构建标准库。编译器和主机工具目前无法为 UEFI 目标编译。示例配置如下
[build]
build-stage = 1
target = ["x86_64-unknown-uefi"]
构建 Rust 程序
从 Rust 1.67 开始,预编译的工件通过 rustup
提供。例如,要使用 x86_64-unknown-uefi
# install cross-compile toolchain
rustup target add x86_64-unknown-uefi
# target flag may be used with any cargo or rustc command
cargo build --target x86_64-unknown-uefi
构建驱动程序
UEFI 可执行文件有三种类型:应用程序、启动服务驱动程序和运行时驱动程序。Rust 的所有 UEFI 目标默认生成应用程序。要改为构建驱动程序,请传递一个带有 efi_boot_service_driver
或 efi_runtime_driver
值的 subsystem
链接器标志。
示例
# In .cargo/config.toml:
[build]
rustflags = ["-C", "link-args=/subsystem:efi_runtime_driver"]
测试
UEFI 应用程序可以复制到任何 UEFI 系统上的 ESP 中,并通过固件引导菜单执行。qemu 套件允许模拟 UEFI 系统并执行 UEFI 应用程序。有关详细信息,请参阅其文档。
Rust 工具 uefi-run 是一个简单的 qemu
包装器,可以在 qemu 中生成 UEFI 应用程序。您可以通过 cargo install uefi-run
安装它,并将 qemu 应用程序执行为 uefi-run ./application.efi
。
交叉编译工具链和 C 代码
有三种常见的为 UEFI 目标编译原生 C 代码的方法
- 使用英特尔的官方 SDK:Tianocore/EDK2。它支持多种平台,附带完整的规范(转换为 C 语言),大量示例和构建系统集成。这也是英特尔唯一官方支持的平台,并被许多主要的固件实现使用。通过 SDK 编译的任何代码都与为 UEFI 目标编译的 rust 二进制文件兼容。您可以将它们直接链接到您的 rust 二进制文件中,或通过 UEFI 协议相互调用。
- 使用 GNU-EFI 套件。这种方法被 Linux/OSS 生态系统中的许多 UEFI 应用程序使用。GCC 编译器用于编译 ELF 二进制文件,并与一个预加载器链接,该预加载器将 ELF 二进制文件在运行时转换为 PE32+。您只能通过 UEFI 协议将此类二进制文件与 rust UEFI 目标组合。将两者链接到同一个可执行文件中将失败,因为一个是 ELF 可执行文件,另一个是 PE32+。如果需要链接到 GNU-EFI 可执行文件,则必须为与 GNU-EFI 相同的 GNU 目标原生编译您的 rust 代码,并使用它们的预加载器。这需要仔细考虑在调用原生 UEFI 协议或调用链接的 GNU-EFI 代码时使用哪个调用约定(类似于在编写 GNU-EFI C 代码时需要考虑这些差异)。
- 使用原生 Windows 目标。这意味着像编译 UEFI 平台一样为 Windows 平台编译您的 C 代码。这适用于静态库,但在链接到 UEFI 可执行文件时需要进行调整。但是,您可以将此类静态库无缝链接到为 UEFI 目标编译的 rust 代码中。请注意任何不适合 UEFI 目标的包含项(尤其是 C 标准库包含项并不总是兼容)。建议使用独立编译以避免不兼容性。
生态系统
Rust 语言在支持 UEFI 目标方面有着悠久的历史。许多板条箱已被开发出来,以提供对 UEFI 协议的访问,并使 UEFI 编程在 rust 中更符合人体工程学。以下列表是一个简短的概述(按字母顺序排列)
- efi: 用于编写 UEFI 应用程序的符合人体工程学的 Rust 绑定。提供对 UEFI 协议的rust 化访问,实现分配器和一个安全的环境来编写 UEFI 应用程序。
- r-efi: UEFI 参考规范协议常量和定义。将 UEFI 规范纯粹地转换为 rust。它提供了规范中的原始定义,没有任何扩展的帮助程序或rust 化。它作为实现任何更精细的 rust UEFI 层的基础。
- uefi-rs: 用于构建 UEFI 应用程序的安全且易于使用的包装器。一个提供 UEFI 协议和功能的安全抽象的详细库。它实现分配器并为用 rust 编写的 UEFI 应用程序提供执行环境。
- uefi-run: 运行 UEFI 应用程序。一个围绕qemu的小型包装器,用于在模拟的
x86_64
机器中生成 UEFI 应用程序。
示例:独立
以下代码是一个有效的 UEFI 应用程序,它在执行时立即返回,退出代码为 0。提供了一个恐慌处理程序。这是 rust 在恐慌时执行的。为简单起见,我们最终进入了一个无限循环。
此示例可以通过 cargo
编译为二进制板条箱
cargo build --target x86_64-unknown-uefi
#![no_main]
#![no_std]
#[panic_handler]
fn panic_handler(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[export_name = "efi_main"]
pub extern "C" fn main(_h: *mut core::ffi::c_void, _st: *mut core::ffi::c_void) -> usize {
0
}
示例:Hello World
这是一个示例 UEFI 应用程序,它打印 "Hello World!",然后等待按键输入,然后退出。它作为基本示例说明如何在没有 r-efi
板条箱提供的独立 UEFI 协议定义之外的任何帮助模块的情况下编写 UEFI 应用程序。
这扩展了 "独立" 示例,并建立在其设置之上。有关如何将其编译为二进制板条箱的说明,请参阅那里。
请注意,UEFI 使用 UTF-16 字符串。由于 rust 字面量是 UTF-8,因此我们必须使用一个开放编码的、以零结尾的、UTF-16 数组作为 output_string()
的参数。与恐慌处理程序类似,实际应用程序应该使用 UTF-16 模块。
#![no_main]
#![no_std]
use r_efi::efi;
#[panic_handler]
fn panic_handler(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[export_name = "efi_main"]
pub extern "C" fn main(_h: efi::Handle, st: *mut efi::SystemTable) -> efi::Status {
let s = [
0x0048u16, 0x0065u16, 0x006cu16, 0x006cu16, 0x006fu16, // "Hello"
0x0020u16, // " "
0x0057u16, 0x006fu16, 0x0072u16, 0x006cu16, 0x0064u16, // "World"
0x0021u16, // "!"
0x000au16, // "\n"
0x0000u16, // NUL
];
// Print "Hello World!".
let r =
unsafe { ((*(*st).con_out).output_string)((*st).con_out, s.as_ptr() as *mut efi::Char16) };
if r.is_error() {
return r;
}
// Wait for key input, by waiting on the `wait_for_key` event hook.
let r = unsafe {
let mut x: usize = 0;
((*(*st).boot_services).wait_for_event)(1, &mut (*(*st).con_in).wait_for_key, &mut x)
};
if r.is_error() {
return r;
}
efi::Status::SUCCESS
}
UEFI 的 Rust std
本节包含有关如何在 UEFI 上使用 std 的信息。
构建 std
构建 std 部分与官方 文档 几乎相同。应使用的链接器是 rust-lld
。以下是一个示例 config.toml
[rust]
lld = true
然后使用 x.py
构建
./x.py build --target x86_64-unknown-uefi --stage 1
或者,可以使用 build-std
功能。但是,您必须使用包含 UEFI std 修补程序的工具链。然后使用以下命令构建项目
cargo build --target x86_64-unknown-uefi -Zbuild-std=std,panic_abort
已实现的功能
alloc
- 使用
EFI_BOOT_SERVICES.AllocatePool()
和EFI_BOOT_SERVICES.FreePool()
实现。 - 通过所有测试。
- 目前使用
EfiLoaderData
作为EFI_ALLOCATE_POOL->PoolType
。
cmath
- 由编译器内置提供。
env
- 只是一些全局常量。
locks
- 提供的锁应该适用于所有标准的单线程 UEFI 实现。
os_str
- 虽然 UEFI 中的字符串应该是有效的 UCS-2,但实际上,许多实现并不关心并使用 UTF-16 字符串。
- 因此,当前实现支持完整的 UTF-16 字符串。
stdio
- 使用
Simple Text Input Protocol
和Simple Text Output Protocol
。 - 注意:UEFI 使用 CRLF 作为换行符。这意味着 Enter 键被注册为 CR 而不是 LF。
args
- 使用
EFI_LOADED_IMAGE_PROTOCOL->LoadOptions
示例:带 std 的 Hello World
以下代码展示了一个有效的 UEFI 应用程序,包括 stdio
和 alloc
(OsString
和 Vec
)
此示例可以通过 cargo
作为二进制 crate 编译,使用从上述源代码编译的工具链(名为 custom)
cargo +custom build --target x86_64-unknown-uefi
#![feature(uefi_std)]
use r_efi::{efi, protocols::simple_text_output};
use std::{
ffi::OsString,
os::uefi::{env, ffi::OsStrExt}
};
pub fn main() {
println!("Starting Rust Application...");
// Use System Table Directly
let st = env::system_table().as_ptr() as *mut efi::SystemTable;
let mut s: Vec<u16> = OsString::from("Hello World!\n").encode_wide().collect();
s.push(0);
let r =
unsafe {
let con_out: *mut simple_text_output::Protocol = (*st).con_out;
let output_string: extern "efiapi" fn(_: *mut simple_text_output::Protocol, *mut u16) -> efi::Status = (*con_out).output_string;
output_string(con_out, s.as_ptr() as *mut efi::Char16)
};
assert!(!r.is_error())
}
BootServices
当前的 std 实现一旦调用 ExitBootServices
,就会使 BootServices
不可使用。有关如何处理从使用物理地址切换到使用虚拟地址的更多信息,请参阅 运行时驱动程序。
注意:需要注意的是,在调用 ExitBootServices
之前,用户有责任丢弃所有已分配的内存。