*-unknown-uefi

层级:2

用于应用程序、驱动程序和核心 UEFI 二进制文件的统一可扩展固件接口 (UEFI) 目标。

可用目标

  • aarch64-unknown-uefi
  • i686-unknown-uefi
  • x86_64-unknown-uefi

目标维护者

要求

所有 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 链接器 lldlink 风格将二进制文件链接到以 *.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_driverefi_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 ProtocolSimple Text Output Protocol
  • 注意:UEFI 使用 CRLF 作为换行符。这意味着 Enter 键被注册为 CR 而不是 LF。

args

  • 使用 EFI_LOADED_IMAGE_PROTOCOL->LoadOptions

示例:带 std 的 Hello World

以下代码展示了一个有效的 UEFI 应用程序,包括 stdioalloc (OsStringVec)

此示例可以通过 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 之前,用户有责任丢弃所有已分配的内存。