*-未知-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 (extern "C"
在 Intel 目标上至少是不正确的)。如果需要更多细节,该规范有一个关于不同支持的调用约定的详细章节。
MMX、SSE 和其他 FP 单元默认情况下被禁用,以便在它们设置之前允许编译核心 UEFI 代码。这可以通过 rustc 命令行标志针对单独的编译进行覆盖。但并非所有固件都正确配置了这些单元,因此需要仔细检查。
作为 PE32+ 的原生格式,二进制文件是位置相关的,但如果它们所需的地址不可用,则可以在运行时重新定位。代码必须静态链接。不支持动态链接。代码通过 UEFI 接口共享,而不是通过动态链接。此外,UEFI 禁止在启动 CPU/线程之外的任何其他位置运行代码,也不允许使用中断(定时器中断除外)。设备驱动程序需要使用轮询方法。
UEFI 使用单个地址空间来运行所有代码。可以同时加载多个应用程序,并通过单个堆栈上的协作多任务进行调度。
默认情况下,UEFI 目标使用 LLVM 链接器 lld
的 link
风格,将二进制文件链接到后缀为 *.efi
的最终 PE32+ 文件中。PE 子系统设置为 EFI_APPLICATION
,但可以通过将 /subsystem:<...>
传递给链接器进行修改。类似地,入口点设置为 efi_main
,但可以通过 /entry:<...>
进行更改。panic 策略设置为 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 目标都默认为生成应用程序。要改为构建驱动程序,请传递一个 subsystem
链接器标志,其值为 efi_boot_service_driver
或 efi_runtime_driver
。
示例
# In .cargo/config.toml:
[build]
rustflags = ["-C", "link-args=/subsystem:efi_runtime_driver"]
测试
可以将 UEFI 应用程序复制到任何 UEFI 系统上的 ESP 中,并通过固件启动菜单执行。qemu 套件允许模拟 UEFI 系统并执行 UEFI 应用程序。有关详细信息,请参阅其文档。
uefi-run rust 工具是 qemu
的一个简单包装器,可以在 qemu 中启动 UEFI 应用程序。您可以通过 cargo install uefi-run
安装它,并以 uefi-run ./application.efi
的形式执行 qemu 应用程序。
交叉编译工具链和 C 代码
有 3 种常见的方法来为 UEFI 目标编译原生 C 代码
- 使用 Intel 的官方 SDK:Tianocore/EDK2。这支持多种平台,带有完整的规范转置为 C 语言、大量示例和构建系统集成。这也是 Intel 唯一官方支持的平台,并被许多主要的固件实现使用。通过 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 目标。这意味着为 Windows 平台编译您的 C 代码,就像它是 UEFI 平台一样。这适用于静态库,但需要在链接到 UEFI 可执行文件时进行调整。但是,您可以将此类静态库无缝链接到为 UEFI 目标编译的 rust 代码中。请注意任何不特别适合 UEFI 目标的包含文件(尤其 C 标准库的包含文件并不总是兼容的)。建议使用独立编译以避免不兼容。
生态系统
rust 语言在支持 UEFI 目标方面有着悠久的历史。已经开发了许多 crate 来提供对 UEFI 协议的访问,并使 rust 中的 UEFI 编程更加符合人体工程学。以下列表是一个简短的概述(按字母顺序排列)
- 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。提供了一个 panic 处理程序。这是在 panic 时由 rust 执行的。为了简单起见,我们只是陷入了一个无限循环。
此示例可以通过 cargo
编译为二进制 crate
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!",然后在退出之前等待按键输入。它作为如何编写 UEFI 应用程序的基础示例,除了 r-efi
crate 提供的独立 UEFI 协议定义之外,没有任何帮助模块。
这扩展了 "独立" 示例,并建立在其设置之上。有关如何将其编译为二进制 crate 的说明,请参阅此处。
请注意,UEFI 使用 UTF-16 字符串。由于 rust 字面量是 UTF-8,我们必须使用开放编码、零终止的 UTF-16 数组作为 output_string()
的参数。与 panic 处理程序类似,真正的应用程序应该使用 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
- 由 compiler-builtins 提供。
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
)
此示例可以使用从上述源 (名为 custom) 编译的工具链通过 cargo
编译为二进制 crate
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
一旦调用了 ExitBootServices
,std 的当前实现就会使 BootServices
不可用。有关如何处理从使用物理地址切换到使用虚拟地址的更多信息,请参阅 运行时驱动程序。
注意:应该注意的是,用户有责任在调用 ExitBootServices
之前释放所有分配的内存。