Вступление
На каком языке пишут программы для 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<T: AcpiHeadeds + Copy>(
&self
) -> Result<T> {
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<P: ProtocolPointer + ?Sized>() -> ScopedProtocol<P> {
let handle = boot::get_handle_for_protocol::<P>().expect("missing protocol");
unsafe {
boot::open_protocol::<P>(
OpenProtocolParams {
handle,
agent: boot::image_handle(),
controller: None,
},
OpenProtocolAttributes::GetProtocol,
)
.expect("failed to open")
}
}
fn save_bgrt_image() -> Result {
let table = locate_protocol::<acpi_sdt::AcpiSdt>();
// Найти BGRT таблицу
let bgrt_table = table
.locate_table_by_signature::<acpi_sdt::EfiAcpiBootGraphicsResourceTable>()
.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.
Собираем, пробуем и делимся результатами!
Комментарии (19)
GiveMeFreeNickName
27.01.2025 13:38А бригада по моему с гуашью будет?
VladCh_8 Автор
27.01.2025 13:38Мем сложный, можно даже сказать ребус, требующий пояснения. Если гуглить что-то по слову UEFI, забыв переключить при этом раскладку с родной кириллицы, то можно получить вместо желаемого гору картинок с краской
Kitsok
А для чего Rust в прошивке? Чем он поможет, что он улучшит?
pewpew
В каком месте автор указывает на улучшение использования Rust в отличии от Си и ассемблера? Если внимательно почитать статью, можно обнаружить, что
то есть потому что почему бы и нет?
Kitsok
Т.е. статья из серии "Дум на стиральной машине"?
withkittens
Так а в чём проблема написать прошивку на языке, который позволяет писать прошивки?
Kitsok
Да бог в помощь, я же разве против? Только брингапить хоть сколько-нибудь новое железо на этом будет очень, очень тяжело. Ну вот так тяжело, что я бы даже пробовать не стал, в взял бы EDK, или, что не менее вероятно, коммерческий BIOS.
А тяжело будет потому, что в прошивке 99.9% проблем носят железный характер, а не то, каким именно пузырьком сортируется список PCIe устройств.
Поэтому залезание сырым языком в очень сложный эмбед видится мне скорее спортивным мероприятием, нежели претендующим на какой-то прод.
withkittens
Не буду спорить со сложностью эмбеда (не моя стихия), но кажется, что автор и сам туда не лезет с сырыми языками, а просто пишет на Rust UEFI-аппку.
qwerty19106
Можете ли вы аргументировать про "сырой язык"?
Например на микроконтроллерах STM32 библиотеки на Rust значительно менее сырые, чем официальный HAL на С.
Kitsok
Сужу исключительно по распространенности в эмбеде.
Что касается STM32 и его знаменитого HALа - писал и пишу с использованием HAL, железо в проде, брат жив.
qwerty19106
Распространенность вообще ортогональна сырости. Есть куча примеров популярных сырых технологий и непопулярных отточенных. Другие аргументы про "сырой язык" будут?
Что касается HAL. Вы же не будете спорить, что STM32 HAL на каждый чих делает кучу действий, которые в нормальном коде занимают 1-2 такта?
А как насчет поддержки всех режимов работы периферии? Как в нем выключить только Noise Error, не выключая Framing Error в UART? Как написать раельно работающий SPI Slave, или еще хуже I2C Slave? Каждый раз приходится после функций HAL менять вручную биты в регистрах.
Сделать кастомное USB (не CDC или HID) устройство вообще не возможно. Проще полностью выкинуть код USB из HAL, и написать свою библиотеку USB.
Kitsok
Знаете, у меня I2C slave и SPI slave (да еще и в ненормальном режиме, он читает SGPIO) в одном устройстве работают прекрасно, единственное место, в котором я лезу куда-то мимо HAL - вызов встроенного загрузчика.
Вообще-то все рассуждения о STM32 к топику отношения не имеют. И ваши трудности с настройкой железа к выбору языка программирования тоже отношения не имеют.
Bittermann
"Наука не терпит вопроса «почему». Главный вопрос — «почему бы и нет». Почему наши исследования так опасны? Почему бы не заняться чем-нибудь менее опасным? Действительно, почему бы не изобрести безопасную дверь, которая не ударит вас по заднице по дороге отсюда, потому что вы уволены!"
OldFisher
We do what we must because we can!
c0r3dump
Моя версия: например, если человек лучше умеет работать с rust, чем с С, а такие поди уже есть и со временем станет больше таких людей. Также, чтобы пользоваться удобной системой сборки, вместо всего того, с чем надо иметь дело в Си. Ещё, в более сложном приложении весь unsafe будет в отдельном месте, а в основной логике будет safe rust, чтобы было меньше типичных для Си ошибок.
VladCh_8 Автор
Вопрос вроде бы и простой, но ответ получается объёмный. Лично моё мнение, на данный момент, полностью вытеснить Си невозможно просто в силу объёма кода, написанного на нём. Сейчас можно частично написать или переписать лишь какие-то элементы, например, Dxe-драйвера, утилиты, работающие в шелле, попадался мне как-то даже загрузчик на Rust. Если говорить о чем-то более раннем, инициализация стека, памяти, железа — тут сложнее, но думаю возможно.
Про сам язык, попробовав для себя отметил, опять же в сравнении, как разработчик, что язык даёт более удобные инструменты для работы с динамической памятью, забирает всю работу с освобождением на себя. Очень частая проблема связана с утечкой памяти, так как ты можешь это упустить или забыть просто в силу частоты появления такого события. Да, это не критично, утекают килобайты, до мегабайтов редко доходит, но всё равно неприятно.
Еще мне понравилась в языке тема с обработками результатов работы - Result. Это убирает из кода “простыни” с обработкой выхлопа из функций, позволяя сосредоточиться на главном функционале, а не на бесконечном количестве if-в с обработкой статуса.
И можно привести еще примеры с прикормкой из языка, от которой я балдею. Опять же, для себя я решил, что пока пробую rust только для написания утилит, и на данный момент мне всё нравится
Kitsok
DXE переписать? Там примерно 680к строк кода из примерно 1.6М. Сколько займет человеко-лет?
Динамическая память? Вот в этих 1.6Е6 строках кода содержится 232 вызова malloc(). Ради 232 вызовов мы перелопатим 1.6 миллиона строк?
Утечка памяти в программе, которая работает от силы 5 минут? Утечка в килобайты - некритична? Я, конечно, не знаю за фронтэнды и бэкэнды, но в тех задачах, которые решаю я, утечка памяти просто недопустима.
Я ничего не имею против Rust, как и против любого другого языка, но на мой взгляд, во встраиваемой разработке, к коей относится и UEFI, он сегодня не применим.
И вообще, я все понимаю про взрывной рост IT, но, может, вместо изобретения языка, который защищает от ошибок программиста, можно нанять квалифицированного для выполняемой работы?
Чтобы он и тесты написал, и, может, не только написал, а и стенд построил, чтобы на целевом железе тестировать, и на осциллографе углядел, например, как настройки контроллера памяти влияют на глазковую диаграмму.
Ах, это не дело программиста - глядеть в осциллограф и паять стенд? Так это эмбед, тут именно так работают.
UFO_01
Мне тоже понравился раст, даже очень (пакетный менеджер из коробки и удобная система сборки вообще как манна небесная), но честно, я не понимаю манию переписывать всё на Rust. Написать что-то новое — почему бы и нет, только за. Но тратить время на переписывание того, что уже работает, очень сомнительная затея. Может получиться как https://habr.com/ru/companies/ruvds/articles/842970/ (TL:DR, переписали часть кодека с C на Rust, ассемблер не тронули, а всё стало работать на 6% медленнее)
То есть в RTOS системах его использовать нельзя? Или в IoT устройствах где зачастую не больше 10k RAM?
Ну, я например не вижу особого смысла переписывать всё с C/C++, слишком много переписывать придётся. Язык — это просто инструмент. Можно писать небезопасно на Rust, можно писать безопасно на C/C++ (санитайзеры вам в помощь). Единственное что — из-за неформализованности компилятора Rust я бы не стал его использовать в safety-critical (где я и работаю). По сути моя единственная претензия :)
Аргументы в стиле "безопасности по памяти" там, где приходится писать unsafe код, ровно как и "сырость Rust", ну это же всё чушь, серьёзно. Имхо, любой аргумент что против, что в защиту Rust это почти всегда передёргивание.