Применение Rust в UEFI/BIOS
Вступление
На каком языке пишут программы для BIOS? Ответ на этот вопрос кажется очевидным: Си и ассемблер. Да, вот так коротко и просто. Существуют и другие инструменты и языки, но так исторически сложилось, что на такой «низкой» глубине выживают только они. В настоящее время здесь доминируют два основных языка, причем с явным перекосом в одну из сторон. В последние годы наблюдается значительный рост популярности языка Rust, который стал серьезным конкурентом одного из фаворитов. Проникнув в ядро Linux, где ранее никому не удавалось потеснить Си, Rust продолжает расширять свое влияние и на другие сферы разработки. Именно с идеи попробовать и сравнить началось мое путешествие по написанию EFI-утилиты на Rust для BIOS.
Немного теории про разработку в BIOS
Разработка BIOS наиболее близка по своей сути к embedded-разработке. Здесь встречаются те же недостатки: сложная отладка, тесное взаимодействие с аппаратной частью и постоянная проблема поиска грамотной документации. В процессе решения проблем часто приходится выискивать необходимую информацию по даташитам, и объем доступных данных по теме может быть сопоставим с документацией на весь микроконтроллер или даже превышать его. Да, есть и свои преимущества, опять в сравнение, ресурсы как будто не ограничены: выше тактовая частота, объем оперативной памяти измеряется в мега/гигабайтах. Периферия в избытке, однако на порядок более сложнее, то же управлении GPIO, лишь от части похоже на то, что я видел при программировании STMок. В гите можно найти обилие опенсорсного кода для решение возникающих трудностей, однако в разработке часто встречается и черные ящики, тот же FSP. Во общем, разгуляться есть где, есть как свои плюсы, так и минусы.
Само слово BIOS уже можно считать как именем нарицательным. Хотя большинство производителей продолжают именовать свои программы так, по сути это уже очень далеко от того, что было изначально. Современный стандарт для программ, которые будут запускаться при старте на вашем компьютера, Unified Extensible Firmware Interface (UEFI). Он описывает этапы инициализации, взаимодействие оборудования и периферии, и множество другой полезной информации. Признаюсь, сам документ полностью никогда не читал, чтиво не для слабых духов, но как справочник с полезной информацией может быть весьма полезен.
Кто понял тот понял :-)
Какие базовые инструменты я могу вам порекомендовать для написания EFI утилит:
Навык программирования на языке Си, так как приходится иметь дело с указателями всех возможных видов и форм. Предупреждаю вас, уровень владения может быть и ниже среднего, но тогда могут возникнуть трудности с изучением и перевариванием новой информации.
Проект tianocore edkii. Это одна из основ, базовых кирпичиков для разработки. Тут собрано очень много готовых рецептов: утилиты, драйвера, библиотеки, исходники UEFI Shell и т.д. Это хороший инструмент, который нужен в этом нелегком пути. Как минимум, будет неплохо изучить код исходников, собранных в репозитории проектов, чтобы иметь представление о том, как все работает изнутри.
Проект gnu-efi. Еще один значимый инструмент для разработки в UEFI. Не буду лукавить, я сам не использовал его, но слышал положительные отзывы от коллег и часто встречал упоминания в профессиональной среде. Не вижу причин, если у вас будет желание, его не попробовать. Следует отметить, что gnu-efi обладает менее обширной коллекцией решений по сравнению с tianocore edkii, однако это не умаляет его значимости.
В качестве теории могу порекомендовать эту статью про разработку в UEFI. Внутри статьи также указаны полезные материалы.
Выбор между изучением теории и практикой зависит от вашего подхода. Вы можете начать с теории, затем перейти к практике, или сочетать оба метода одновременно. Камень я вам дал.
Что будем делать
При разработке efi-утилит на Rust я выбрал использовать крейт uefi-rs. Почему я выбрал его? Этот крейт является основным инструментом для создания Uefi-приложений на языке Rust. Он включает в себя несколько дополнительных крейтов, которые обеспечивают функционал для сборки, тестирования, реализацию макросов, обертки для таблиц Uefi и основные протоколы. Также у проекта имеется документация, которая поможет при старте, и не редко встречаются статьи по разработке.
На данный момент стабильная версия uefi-rs — 0.33, и проект активно поддерживается, что является его значительным преимуществом. Следует отметить, что по сравнению с edk поддержка реализована лишь для части протоколов, однако это решаемая проблема. Дальше в статье продемонстрирую, как можно создать собственную обертку для необходимого вам протокола. В качестве практического задания разработаем приложение, которое будет сохранять изображение с заставки BIOS в отдельный файл.
Возникает вопрос: где хранится изображение заставки? Заставка материнской платы расположена в Boot Graphics Record Table (BGRT), которая является частью системы ACPI-таблиц. Эти таблицы передаются операционной системе и используются для взаимодействия с аппаратным обеспечением. Если быть точнее, в BGRT таблице содержится указатель на положение в памяти изображения.
Цель данной таблицы, хранить изображение. BIOS выводит его на одном из этапов загрузки, чтобы информировать пользователя о том, что «свет» в его устройстве есть и функционирует оно правильно, а также о скором начале загрузки ОС.
Пример изображения, который может там хранится
Получается, так можно добавить свою таблицу, а старую удалить и таким образом поменять изображение? Если отвечать коротко, то, скорее всего, нет. Причина кроется в том, что таблицы ACPI инициализируются при каждом старте и хранятся в оперативной памяти. И если даже мы создадим свою структуру с изображением, а старую удалим, то это может и подхватиться, но будет работать до первой перезагрузки.
Практика. Пишем утилиту
Первые шаги будут такими же, как и для любого другого проекта на Rust. Создаем директорию, где будет храниться проект. Переходим в неё и инициализируем через cargo init:
mkdir safeBgrt
cd safeBgrt
cargo init
Добавляем в файл Cargo.toml крейт для работы с uefi:
uefi = { version = "0.33.0", features = [ "alloc", "global_allocator", "panic_handler"] }
В src/main.rs добавляем следующий код:
#![no_main]
#![no_std]
use uefi::prelude::*;
use uefi::println;
#[entry]
fn main() -> Status {
uefi::helpers::init().unwrap();
println!("Hello, habr!");
Status::SUCCESS
}
Собираем проект.
cargo build --target x86_64-unknown-uefi
После чего должна запустить сборка, и по завершении у вас должно появиться EFI-приложение по пути target/ x86_64-unknown-uefi/debug/safeBgrt.efi.
Запустим его в Qemu и посмотрим на результат:
Если у вас аналогичный результат, поздравляю!
Если сравнивать со сборкой проекта на Си, то порог вхождения на порядок проще. Теперь нам требуется получить ACPI протокол, в свою очередь через него BGRT. Однако есть проблема, как я указал выше в крейте uefi rs реализована поддержка не всех протоколов, и это как раз тот случай.
Обертка для протоколов
Теперь заглянем в документацию, чтобы узнать информации по ACPI протоколу. Там можно найти нужный нам GUID и его интерфейс:
#define EFI_ACPI_SDT_PROTOCOL_GUID \
{ 0xeb97088e, 0xcfdf, 0x49c6, { 0xbe, 0x4b, 0xd9, 0x6, 0xa5, 0xb2, 0xe, 0x86 }}
Если говорить в терминах языка Rust, любая работа с протоколом является небезопасной. Само его получение считается «противозаконным». Это связано с использованием сырых указателей при поиски, при получении и вызове предоставляемых методов. Потребуется реализовать свою обёртку для предоставления протокола и безопасного вызова его методов.
Всю логику, связанную с интерфейсом протокола, вынесем в отдельный файл. Создадим в корне проектного дерева файл aspi_sdt.rs:
#![no_std]
use core::ffi::c_void;
use core::fmt::{self, Display};
use core::ptr;
use uefi::prelude::*;
use uefi::proto::unsafe_protocol;
use uefi::{Char8, Result};
#[derive(Debug)]
#[repr(C)]
#[unsafe_protocol("eb97088e-cfdf-49c6-be4b-d906a5b20e86")]
pub struct AcpiSdt {
acpi_version: u32,
get_acpi_table: unsafe extern "efiapi" fn(
index: usize,
table: *mut *mut EfiAcpiHeader,
version: *mut EfiAcpiTableVersion,
table_key: *mut usize,
) -> Status,
register_notify:
unsafe extern "efiapi" fn(register: bool, notification: EfiAcpiNotificationFn) -> Status,
open: unsafe extern "efiapi" fn(buffer: *mut c_void, handle: *mut Handle) -> Status,
open_sdt: unsafe extern "efiapi" fn(take_key: usize, handle: *mut Handle) -> Status,
close: unsafe extern "efiapi" fn(handle: Handle) -> Status,
get_child: unsafe extern "efiapi" fn(parent_handle: Handle, handle: *mut Handle) -> Status,
get_option: unsafe extern "efiapi" fn(
handle: Handle,
index: usize,
data_type: *mut EfiAcpiDataType,
data: *mut *mut c_void,
data_size: *mut usize,
) -> Status,
set_option: unsafe extern "efiapi" fn(
handle: Handle,
index: usize,
data: *mut c_void,
data_size: usize,
) -> Status,
find_path: unsafe extern "efiapi" fn(
handle_in: Handle,
acpi_path: *mut c_void,
handle_out: *mut Handle,
) -> Status,
}
type EfiAcpiTableVersion = u32;
type EfiAcpiDataType = u32;
#[derive(Clone, Copy, Debug)]
#[repr(C, packed(1))]
pub struct EfiAcpiHeader {
signature: u32,
pub lenght: u32,
revision: u8,
checksum: u8,
oem_id: [Char8; 6],
oem_table_id: [Char8; 8],
oem_revision: u32,
creator_id: u32,
creator_revision: u32,
}
type EfiAcpiNotificationFn = unsafe extern "efiapi" fn(
table: *mut *mut EfiAcpiHeader,
version: EfiAcpiTableVersion,
table_key: usize,
) -> Status;
Этого кода достаточно для получения протокола. Из всего изобилия методов по работе с ACPI таблицами нам понадобится только один — get_acpi_table. Его прототип из документации:
typedef
EFI_STATUS
(EFIAPI *EFI_ACPI_GET_ACPI_TABLE) (
IN UINTN Index,
OUT EFI_ACPI_SDT_HEADER **Table,
OUT EFI_ACPI_TABLE_VERSION *Version,
OUT UINTN *TableKey
);
Эту функция возвращает заголовок таблицы, за которым лежит уже требуемая таблица. Поиск ведётся по сигнатуре, которая лежит в заголовке. Вот так будет выглядеть функция по поиску требуемой таблицы на Rust:
macro_rules! signature_16 {
($a:expr, $b:expr) => {
(($a as u32) | (($b as u32) << 8))
};
}
macro_rules! signature_32 {
($a:expr, $b:expr, $c:expr, $d:expr) => {
(signature_16!($a, $b) | (signature_16!($c, $d) << 16))
};
}
pub trait AcpiHeadeds {
const ACPI_SIGNATURE: u32;
fn get_header(&self) -> EfiAcpiHeader;
}
impl AcpiHeadeds for EfiAcpiHeader {
const ACPI_SIGNATURE: u32 = 0u32;
fn get_header(&self) -> EfiAcpiHeader {
*self
}
}
impl AcpiSdt {
pub fn locate_table_by_signature(
&self
) -> Result {
let mut index = 0;
let mut version: EfiAcpiTableVersion = 0;
let mut acpi_head: *mut EfiAcpiHeader = ptr::null_mut();
let mut table_key: usize = 0;
loop {
let (status, head) = unsafe {
let status =
(self.get_acpi_table)(index, &mut acpi_head, &mut version, &mut table_key);
(status, *(acpi_head as *mut T))
};
if status.is_success() {
index += 1;
if head.get_header().signature == T::ACPI_SIGNATURE {
break Ok(head);
}
} else {
break Err(status.into());
}
}
}
}
Осталось добавить структуру для BGRT:
#[derive(Clone, Copy)]
#[repr(C)]
pub struct EfiAcpiBootGraphicsResourceTable {
header: EfiAcpiHeader,
pub version: u16,
pub status: u8,
pub image_type: u8,
pub image_address: u64,
pub image_offset_x: u32,
pub image_offset_y: u32,
}
impl AcpiHeadeds for EfiAcpiBootGraphicsResourceTable {
const ACPI_SIGNATURE: u32 = signature_32!('B', 'G', 'R', 'T');
fn get_header(&self) -> EfiAcpiHeader {
self.header
}
}
Аналогичным образом можно реализовать поддержку для любой другой ACPI таблицы, которая может вам потребоваться.
Перейдём к самому вкусному — сборке наших «полуфабрикатов» в готовый продукт. Немного поправим файл main.rs, и можно готовить:
#![no_main]
#![no_std]
extern crate alloc;
use alloc::slice;
use uefi::boot::{OpenProtocolAttributes, OpenProtocolParams, ScopedProtocol};
use uefi::fs::FileSystem;
use uefi::prelude::*;
use uefi::proto::ProtocolPointer;
use uefi::{boot, println, Result};
pub mod acpi_sdt;
fn locate_protocol() -> ScopedProtocol {
let handle = boot::get_handle_for_protocol::
().expect("missing protocol");
unsafe {
boot::open_protocol::
(
OpenProtocolParams {
handle,
agent: boot::image_handle(),
controller: None,
},
OpenProtocolAttributes::GetProtocol,
)
.expect("failed to open")
}
}
fn save_bgrt_image() -> Result {
let table = locate_protocol::();
// Найти BGRT таблицу
let bgrt_table = table
.locate_table_by_signature::()
.map_err(|_| Status::NOT_FOUND)?;
let addr = bgrt_table.image_address;
let len = (bgrt_table.image_offset_x * bgrt_table.image_offset_y) as usize;
let slice: &[u8] = unsafe { slice::from_raw_parts(addr as *const u8, len) };
boot::get_image_file_system(boot::image_handle()).map(|file_system| {
let mut fs = FileSystem::new(file_system);
let _ = fs.write(cstr16!("BGRTImage.bmp"), slice);
})
}
#[entry]
fn main() -> Status {
uefi::helpers::init().unwrap();
println!("Hello, habr!");
save_bgrt_image().status()
}
Изображение сохраняется в файл в корне файловой системы под именем BGRTImage.bmp.
Собираем, пробуем и делимся результатами!