*-未知-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 (extern "C" 在 Intel 目标上至少是不正确的)。如果需要更多细节,该规范有一个关于不同支持的调用约定的详细章节。

MMX、SSE 和其他 FP 单元默认情况下被禁用,以便在它们设置之前允许编译核心 UEFI 代码。这可以通过 rustc 命令行标志针对单独的编译进行覆盖。但并非所有固件都正确配置了这些单元,因此需要仔细检查。

作为 PE32+ 的原生格式,二进制文件是位置相关的,但如果它们所需的地址不可用,则可以在运行时重新定位。代码必须静态链接。不支持动态链接。代码通过 UEFI 接口共享,而不是通过动态链接。此外,UEFI 禁止在启动 CPU/线程之外的任何其他位置运行代码,也不允许使用中断(定时器中断除外)。设备驱动程序需要使用轮询方法。

UEFI 使用单个地址空间来运行所有代码。可以同时加载多个应用程序,并通过单个堆栈上的协作多任务进行调度。

默认情况下,UEFI 目标使用 LLVM 链接器 lldlink 风格,将二进制文件链接到后缀为 *.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_driverefi_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 ProtocolSimple Text Output Protocol
  • 注意:UEFI 使用 CRLF 作为换行符。这意味着 Enter 键被注册为 CR 而不是 LF。

args

  • 使用 EFI_LOADED_IMAGE_PROTOCOL->LoadOptions

示例:带 std 的 Hello World

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

此示例可以使用从上述源 (名为 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 之前释放所有分配的内存。