Написание протокола UEFI на языке Rust

Привет всем. В рамках портирования Rust std для UEFI мне пришлось написать хакерскую реализацию pipes (с использованием переменных UEFI) для передачи вывода запущенных программ. Однако, эта реализация имела некоторые проблемы и не смогла запустить тесты с использованием функции panic_abort_tests.

Поскольку у меня было немного времени на этой неделе, я решил написать протокол для исправления труб. По пути я также нашел много способов застрелиться. Поэтому я решил написать этот пост, документирующий все проблемы, которые я обнаружил во время написания нового протокола в Rust. Этот пост будет содержать только один из двух протоколов, которые я решил создать, с внутренним названием UEFI_COMMAND_PROTOCOL.

Примечание: Этот протокол не является публичным API и поэтому не должен использоваться вне Rust std, по крайней мере, в обозримом будущем.

Определение протокола

Протокол UEFI_COMMAND_PROTOCOL имеет прямое назначение: передавать хэндлы труб (stdout, stderr, stdin) запускаемому образу. Возможно, в будущем он будет содержать больше возможностей, но на данный момент это все, что он делает. Он устанавливается на все исполняемые файлы UEFI, запускаемые с помощью API Rust std::process::Command и нуждающиеся в Pipes (В случае, если им не нужны никакие типы Pipes, этот протокол не будет установлен).

Структура

Здесь приведена структура Rust для определения протокола:

#[repr(C)]
pub struct Protocol {
    pub stdout: r_efi::efi::Handle,
    pub stderr: r_efi::efi::Handle,
    pub stdin: r_efi::efi::Handle,
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Как вы можете видеть, здесь не так много.

Guid

Здесь находится GUID протокола:

pub const PROTOCOL_GUID: r_efi::efi::Guid = r_efi::efi::Guid::from_fields(
    0xc3cc5ede,
    0xb029,
    0x4daa,
    0xa5,
    0x5f,
    &[0x93, 0xf8, 0x82, 0x5b, 0x29, 0xe7],
);
Войти в полноэкранный режим Выход из полноэкранного режима

Использование протокола

При создании и установке протокола из Rust необходимо помнить о некоторых вещах. Вот некоторые из уроков, которые я усвоил трудным путем:

Протокол должен быть неперемещаемым

По умолчанию все переменные Rust являются подвижными в памяти. После регистрации протокола UEFI он должен быть действителен до тех пор, пока его не удалят. Установка протокола, интерфейс которого находится в стеке, может привести к разного рода неприятностям. В моем конкретном случае я решил использовать Heap выделенный протокол в std::box::Box. Насколько я понимаю, существуют способы перемещения содержимого Box в памяти. Однако для моих простых нужд этого было достаточно, поскольку я не выполнял никаких операций с Box после установки протокола.

На практике код выглядит примерно так:

let mut protocol = Box::new(uefi_command_protocol::new(stdout, stderr, stdin));
install_interface(&mut command handle, &mut protocol).unwrap();
// Do something with Command
Войти в полноэкранный режим Выход из полноэкранного режима

Для внутреннего использования у меня есть обертки, которые обрабатывают удаление протокола на Drop, чтобы хорошо работать с управлением памятью Rust. Возможно, лучшее решение можно получить, используя обертку rust std::pin::Pin, но лично я ее не использовал.

Используйте EFI_BOOT_SERVICES.InstallProtocolInterface() и EFI_BOOT_SERVICES.UninstallProtocolInterface()

Большинство современных драйверов UEFI используют EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces() и EFI_BOOT_SERVICES.UninstallMultipleProtocolInterfaces() для установки и удаления протоколов на рукоятке. Однако rustc в настоящее время не поддерживает переменные аргументы для UEFI. Для получения дополнительной информации об этом вы можете обратиться к этому вопросу.

Забота о времени жизни

Вам нужно убедиться, что время жизни протоколов строго определено с учетом времени жизни, ожидаемого UEFI. Я написал некоторые структуры в Rust std, чтобы облегчить внутреннюю работу. Однако разница во времени жизни может привести ко всему — от исключений процессора до бесконечного цикла.

Также позаботьтесь о любых данных, на которые указывает протокол.

Забота о порядке падения в Rust

Бывают случаи, когда вы хотите строго определить порядок выпадения членов структур-оберток протокола. Я нашел отличный ответ на этот вопрос. Однако эмпирическое правило заключается в том, что Rust по умолчанию отбрасывает члены в том же порядке, в котором они были объявлены. Поэтому не забывайте об этом.

Избегайте хранения типов, специфичных для Rust, в протоколе

Хранение динамических данных в протоколе UEFI Protocol, как правило, не самая лучшая идея. Однако типы, специфичные для Rust (Vec, Box и т.д.), могут привести к разного рода UB, поскольку все типы Rust по умолчанию подвижны. Я обошел эту проблему, храня указатели на struct, который оборачивается над типом, специфичным для Rust. В качестве простого примера можно привести следующее:

struct PipeData {
    data: VecDequeue<u8>
}

#[repr(C)]
pub struct Protocol {
    data: *mut PipeData
}
Войти в полноэкранный режим Выход из полноэкранного режима

Заключение

Поскольку примеров использования Rust в UEFI-драйверах не так много, то и стандартных практик пока не выработано. Этой статьей я надеюсь помочь всем, кто пытается использовать Rust для разработки драйверов UEFI. Наконец, благодаря новым UEFI_COMMAND_PROTOCOL и UEFI_PIPE_PROTOCOL, std-порт Rust теперь может использовать полный libtest (даже should_panic, который ранее был сломан). Это полезно, поскольку даже если вы пойдете по пути no_std, вы все равно сможете использовать std для тестирования вашего no_std кода. Наконец, не стесняйтесь проверить Rust std для UEFI.

Оцените статью
devanswers.ru
Добавить комментарий