- PVSM.RU - https://www.pvsm.ru -
Всем привет. В рамках проекта от компании Acronis со студентами Университета Иннополис (подробнее о проекте мы уже описали это тут [1] и тут [2]) мы изучали последовательность загрузки операционной системы Windows. Появилась идея исполнять логику даже до загрузки самой ОС. Следовательно, мы попробовали написать что-нибудь для общего развития, для плавного погружения в UEFI. В этой статье мы пройдем по теории и попрактикуемся с чтением и записью на диск в pre-OS среде.
В компании Acronis команд, которые занимаются UEFI, не так много, поэтому я решил разобраться в вопросе самостоятельно. К тому же есть проверенный способ получить огромное количество точных советов совершенно бесплатно и свободно — просто начать делать что-либо и выложить это в интернет. Поэтому комментарии и рекомендации под этим постом очень приветствуются! Вторая цель данного поста собрать небольшой дайджест статей о UEFI и помочь двигаться в этом направлении.
Для начала хочу перечислить список источников, которые мне очень помогли. Возможно вам они тоже помогут и ответят на ваши вопросы.
Хочу напомнить требования и цели проекта Active Restore. Мы планируем приоритизировать файлы в системе для более эффективного восстановления. Для этого нужно запуститься на максимально раннем этапе загрузки ОС. Для понимания наших возможностей в мире UEFI стоит немного углубиться в теорию о том как проходит цикл загрузки. Информация для этой части полностью взята из этого [3] источника, который я постараюсь популярно пересказать.
UEFI или Unified Extensible Firmware Interface стал эволюцией Legacy BIOS. В модели UEFI тоже есть базовая система ввода-вывода для взаимодействия с железом, хотя процесс загрузки системы и стал отличаться. UEFI использует GPT (Guid partition table). GPT тесно связана со спецификацией и является более продвинутой моделью для хранения информации о разделах диска. Изменился процесс, но задачи остались прежними: инициализация устройств ввода-вывода и передача управления в код операционной системы. UEFI не только заменяет бóльшую часть функций BIOS, но также предоставляет широкий спектр возможности для разработки в pre-OS среде. Хорошее сравнение Legacy BIOS и UEFI есть тут [10].
В данном представлении BIOS это компонент, обеспечивающий непосредственное общение с железом и является firmware. UEFI это унификация интерфейса железа для операционной системы, что существенно облегчает жизнь разработчикам.
В мире UEFI мы можем разрабатывать драйвера или приложения. Есть специальный подтип приложений — загрузчики. Разница лишь в том, что эти приложения не завершаются привычным нам образом. Завершаются они вызовом функции ExitBootServices() [11] и передают управление в операционную систему. Чтобы принять решение какой же драйвер нужен вам, рекомендую заглянуть сюда [12], чтобы расширить понимание о протоколах и рекомендациях по их использованию.
Небольшой список того, что мы будем использовать в нашей практике:
Коротко разберем через какие стадии проходит наша машина, прежде чем мы видим заветный логотип операционной системы. Для этого рассмотрим следующую диаграмму:
Ссылка [16]
Процесс с момента нажатия на кнопку питания на корпусе и до полной готовности UEFI интерфейса называется Platform Initialization и делится он на несколько фаз:
Ссылка [20]
Классный рассказ о этапах загрузки есть тут [21].
Пришло время поставить перед собой простую задачу. Мы можем загрузить наш драйвер в DXE фазе, открыть файл на диске и записать в него какие — нибудь данные. Задача достаточно простая, чтобы потренироваться.
Как я уже упоминал мы воспользуемся проектом VisualUEFI, однако рекомендую также попробовать способы описанные тут [4], хотя бы потому, что использовать дебагер легче в описанном по ссылке способе.
Допускаю, что у вас уже есть Visual Studio. В моем случае у меня Visual Studio 2019. Для начала клонируем себе проект VisualUEFI:
git clone --recurse-submodules -j8 https://github.com/ionescu007/VisualUefi.git
Нам понадобится NASM [22] (https://www.nasm.us/pub/nasm/releasebuilds/2.15.02/win64/ [23]). Переходим и скачиваем. На момент написания статьи актуальной версией является 2.15.02. После установки убедитесь, что в переменных средах у вас есть NASM_PREFIX, который указывает на папку, в которую был установлен NASM. В моем случае это C:Program FilesNASM.
Соберем EDKII. Для этого открываем EDK-II.sln из VisualUefiEDK-II, и просто жмем build на решении. Все проекты в решении должны успешно собраться, и можно переходить к уже готовым примерам. Открываем samples.sln из VisualUefisamples. Жмем build на приложении и драйвере, после чего можно запускать QEMU простым нажатием F5.
Проверяем наш UefiDriver и UefiApplication, именно так называются примеры в решении samples.sln.
Shell> fs1:
FS1:> load UefiDriver.efi
Отлично, драйвер не только собрался, но и успешно загрузился. Выполнив команду drivers, мы даже увидим его в списке.
Если бы в коде мы не возвращали EFI_ACCESS_DENIED в функции UefiUnload, мы бы даже смогли выгрузить наш драйвер, выполнив команду:
FS1:> unload BA
Теперь вызовем наше приложение:
FS1:> UefiApplication.efi
Рассмотрим код предоставленного нам драйвера. Все начинается с функции UefiMain, которая находится в файле drvmain.c. Мы бы могли назвать точку входа и другим именем, если бы писали драйвер “с нуля”, указать это можно было бы в .inf файле.
EFI_STATUS
EFIAPI
UefiUnload (
IN EFI_HANDLE ImageHandle
)
{
//
// Do not allow unload
//
return EFI_ACCESS_DENIED;
}
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS efiStatus;
//
// Install required driver binding components
//
efiStatus = EfiLibInstallDriverBindingComponentName2(ImageHandle,
SystemTable,
&gDriverBindingProtocol,
ImageHandle,
&gComponentNameProtocol,
&gComponentName2Protocol);
return efiStatus;
}
В проекте от нас не требуют регистрировать Unload функцию, так как VisualUEFI это и так уже делает “под капотом”, нужно просто её объявить. В примере она в этом же файле и называется UefiUnload. В этой функции мы можем написать код, который освободит все занятые нами ресурсы, так как она будет вызвана при выгрузке драйвера. Регистрация Unload функции в проекте VisualUEFI происходит в файле DriverEntryPoint.c, в функции _ModuleEntryPoint.
// _DriverUnloadHandler manages to call UefiUnload
Status = gBS->HandleProtocol (
ImageHandle,
&gEfiLoadedImageProtocolGuid,
(VOID **)&LoadedImage
);
ASSERT_EFI_ERROR (Status);
LoadedImage->Unload = _DriverUnloadHandler;
В нашем примере, в функции UefiMain, происходит вызов функции EfiLibInstallDriverBindingComponentName2, которая регистрирует имя нашего драйвера и Driver Binding Protocol. Согласно модели драйверов UEFI, все драйвера устройств должны регистрировать этот протокол для предоставления контроллеру функций Support, Start, Stop. Функция Support отвечает, может ли наш драйвер работать с данным контроллером. Если да, то вызывается функция Start. Подробнее об этом хорошо описано в спецификации [9] (раздел Protocols — UEFI Driver Model). В нашем примере функции Support, Start и Stop устанавливают наш кастомный протокол. Его реализация в файле drvpnp.c:
//
// EFI Driver Binding Protocol
//
EFI_DRIVER_BINDING_PROTOCOL gDriverBindingProtocol =
{
SampleDriverSupported,
SampleDriverStart,
SampleDriverStop,
10,
NULL,
NULL
};
…
//
// Install our custom protocol on top of a new device handle
//
efiStatus = gBS->InstallMultipleProtocolInterfaces(&deviceExtension->DeviceHandle,
&gEfiSampleDriverProtocolGuid,
&deviceExtension->DeviceProtocol,
NULL);
//
// Bind the PCI I/O protocol between our new device handle and the controller
//
efiStatus = gBS->OpenProtocol(Controller,
&gEfiPciIoProtocolGuid,
(VOID**)&childPciIo,
This->DriverBindingHandle,
deviceExtension->DeviceHandle,
EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER);
Фукнция EfiLibInstallDriverBindingComponentName2 реализована в файле UefiDriverModel.c, и, на самом деле, очень простая. Она вызывает InstallMultipleProtocolInterfaces из Boot Services (см. Спецификацию [9] стр 210). Данная функция связывает handle (в нашем случае ImageHandle, который мы получили на точке входа) и протокол.
// install component name and binding
Status = gBS->InstallMultipleProtocolInterfaces (
&DriverBinding->DriverBindingHandle,
&gEfiDriverBindingProtocolGuid, DriverBinding,
&gEfiComponentNameProtocolGuid, ComponentName,
&gEfiComponentName2ProtocolGuid, ComponentName2,
NULL
);
Соответственно, можно, и нужно, в момент выгрузки драйвера, удалить установленные компоненты. Мы сделаем это в нашей функции Unload. Теперь наш драйвер можно будет выгружать по команде unload , или перед передачей управления в операционную систему.
EFI_STATUS
EFIAPI
UefiUnload (
IN EFI_HANDLE ImageHandle
)
{
gBS->UninstallMultipleProtocolInterfaces(
ImageHandle,
&gEfiDriverBindingProtocolGuid, &gDriverBindingProtocol,
&gEfiComponentNameProtocolGuid, &gComponentNameProtocol,
&gEfiComponentName2ProtocolGuid, &gComponentName2Protocol,
NULL
);
//
// Changed from access denied in order to unload in boot
//
return EFI_SUCCESS;
}
Как вы могли заметить, в нашем коде мы взаимодействуем с UEFI через глобальное поле gBS (global Boot Services). Также, существует gRT (global Runtime Services), а вместе они являются частью структуры System Table. Источник [5].
gST = *SystemTable;
gBS = gST->BootServices;
gRT = gST->RuntimeServices;
Для работы с файлами нам понадобится Simple File System Protocol (см. Спецификацию [9] стр 504). Вызвав функцию LocateProtocol, можно получить на него указатель, хотя более правильный способ перечислить все handles на устройства файловой системы с помощью функции LocateHandleBuffer, и, перебрав все протоколы Simple File System, выбрать подходящий, который позволит нам писать и читать в файл. Пример такого кода тут [24]. А мы же воспользуемся способом проще. У протокола есть всего одна функция, которая позволит нам открыть том.
EFI_STATUS
OpenVolume(
OUT EFI_FILE_PROTOCOL** Volume
)
{
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fsProto = NULL;
EFI_STATUS status;
*Volume = NULL;
// get file system protocol
status = gBS->LocateProtocol(
&gEfiSimpleFileSystemProtocolGuid,
NULL,
(VOID**)&fsProto
);
if (EFI_ERROR(status))
{
return status;
}
status = fsProto->OpenVolume(
fsProto,
Volume
);
return status;
}
Далее, нам необходимо уметь создавать файл и закрывать его. Воспользуемся EFI_FILE_PROTOCOL, в котором есть функции для работы с файловой системой (см. Спецификацию [9] стр 506).
EFI_STATUS
OpenFile(
IN EFI_FILE_PROTOCOL* Volume,
OUT EFI_FILE_PROTOCOL** File,
IN CHAR16* Path
)
{
EFI_STATUS status;
*File = NULL;
// from root file we open file specified by path
status = Volume->Open(
Volume,
File,
Path,
EFI_FILE_MODE_CREATE |
EFI_FILE_MODE_WRITE |
EFI_FILE_MODE_READ,
0
);
return status;
}
EFI_STATUS
CloseFile(
IN EFI_FILE_PROTOCOL* File
)
{
// flush unwritten data
File->Flush(File);
// close file
File->Close(File);
return EFI_SUCCESS;
}
Для записи в файл нам придется вручную двигать каретку. Для этого будем спрашивать размер файла с помощью функции GetInfo.
EFI_STATUS
WriteDataToFile(
IN VOID* Buffer,
IN UINTN BufferSize,
IN EFI_FILE_PROTOCOL* File
)
{
UINTN infoBufferSize = 0;
EFI_FILE_INFO* fileInfo = NULL;
// retrieve file info to know it size
EFI_STATUS status = File->GetInfo(
File,
&gEfiFileInfoGuid,
&infoBufferSize,
(VOID*)fileInfo
);
if (EFI_BUFFER_TOO_SMALL != status)
{
return status;
}
fileInfo = AllocatePool(infoBufferSize);
if (NULL == fileInfo)
{
status = EFI_OUT_OF_RESOURCES;
return status;
}
// we need to know file size
status = File->GetInfo(
File,
&gEfiFileInfoGuid,
&infoBufferSize,
(VOID*)fileInfo
);
if (EFI_ERROR(status))
{
goto FINALLY;
}
// we move carriage to the end of the file
status = File->SetPosition(
File,
fileInfo->FileSize
);
if (EFI_ERROR(status))
{
goto FINALLY;
}
// write buffer
status = File->Write(
File,
&BufferSize,
Buffer
);
if (EFI_ERROR(status))
{
goto FINALLY;
}
// flush data
status = File->Flush(File);
FINALLY:
if (NULL != fileInfo)
{
FreePool(fileInfo);
}
return status;
}
Вызываем наши функции и пишем случайные данные в наш файл:
EFI_STATUS
WriteToFile(
VOID
)
{
CHAR16 path[] = L"\example.txt";
EFI_FILE_PROTOCOL* file = NULL;
EFI_FILE_PROTOCOL* volume = NULL;
CHAR16 something[] = L"Hello from UEFI driver";
//
// Open file
//
EFI_STATUS status = OpenVolume(&volume);
if (EFI_ERROR(status))
{
return status;
}
status = OpenFile(volume, &file, path);
if (EFI_ERROR(status))
{
CloseFile(volume);
return status;
}
status = WriteDataToFile(something, sizeof(something), file);
CloseFile(file);
CloseFile(volume);
return status;
}
Есть альтернативный способ выполнить нашу задачу. В проекте VisualUEFI уже реализовано то, что мы написали выше. Мы можем просто подключить заголовочный файл ShellLib.h и вызвать в самом начале функцию ShellInitialize. Все необходимые протоколы для работы с файловой системой будут открыты, а функции ShellOpenFileByName, ShellWrite и ShellRead реализованы почти так же, как и у нас.
#include <Library/ShellLib.h>
EFI_STATUS
WriteToFile2(
VOID
)
{
SHELL_FILE_HANDLE fileHandle = NULL;
CHAR16 path[] = L"fs1:\example2.txt";
CHAR16 something[] = L"Hello from UEFI driver";
UINTN writeSize = sizeof(something);
EFI_STATUS status = ShellInitialize();
if (EFI_ERROR(status))
{
return status;
}
status = ShellOpenFileByName(path,
&fileHandle,
EFI_FILE_MODE_CREATE |
EFI_FILE_MODE_WRITE |
EFI_FILE_MODE_READ,
0);
if (EFI_ERROR(status))
{
return status;
}
status = ShellWriteFile(fileHandle, &writeSize, something);
ShellCloseFile(&fileHandle);
return status;
}
Результат:
→ Код этого примера на github [25]
Если мы хотим перейти в VMWare, то наиболее правильным будет модификация firmware с помощью UEFITool [26]. Например тут [27] демонстрируется как добавляют NTFS драйвер в UEFI.
Усложнить идею нашего драйвера и ближе подвести его под требования проекта Active Restore можно следующим образом: открыть протокол BLOCK_IO, заменить функции чтения на диск нашими функциями, которые запишут данные, читаемые с диска в лог и затем вызовут оригинальные функции. Сделать это можно следующим образом:
// just pseudo code
...
// open protocol to replace callbacks
gBS->OpenProtocol(
Controller,
Guid,
(VOID**)&protocol,
DriverBindingHandle,
Controller,
EFI_OPEN_PROTOCOL_GET_PROTOCOL
);
// raise Task Priority Level to max avaliable
gBS->RaiseTPL(TPL_NOTIFY);
VOID** protocolBase = EFI_FIELD_BY_OFFSET(VOID**, FilterContainer, 0);
VOID** oldCallback = EFI_FIELD_BY_OFFSET(VOID**, *protocolBase, oldCallbackOffset);
VOID** originalCallback = EFI_FIELD_BY_OFFSET(VOID**, FilterContainer, originalCallbackOffset);
// yes, I know that it is not super obvious
// but if first and third is equal (placeholder and function)
// then the first one is not the function it is offset!
// and function itself is by offset of third one
if ((UINTN) newCallback == originalCallbackOffset)
{
newCallback = *originalCallback;
}
PRINT_DEBUG(DEBUG_INFO, L"[UefiMonitor] 0x%x -> 0x%xn", *oldCallback, newCallback);
//saving original functions
*originalCallback = *oldCallback;
//replacing them by filter function
*oldCallback = newCallback;
// restore TPL
gBS->RestoreTPL(oldTpl);
Нужно будет не забыть подписаться на ExitBootServices(), чтобы вернуть указатели на место. После того, как фильтр файловой системы в Windows будет готов, минифильтр продолжит логировать чтение с диска.
// event on exit
gBS->CreateEvent(
EVT_SIGNAL_EXIT_BOOT_SERVICES,
TPL_NOTIFY,
ExitBootServicesNotifyCallback,
NULL,
&mExitBootServicesEvent
);
Но это это уже идеи для будущих статей. Спасибо за внимание.
Автор: Daulet Tumbayev
Источник [28]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/rezervnoe-kopirovanie/355202
Ссылки в тексте:
[1] тут: https://habr.com/ru/company/acronis/blog/477658/
[2] тут: https://habr.com/ru/company/acronis/blog/479524/
[3] UEFI and EDK II Learning and Development: https://github.com/tianocore/tianocore.github.io/wiki/UEFI%20EDKII%20Learning%20Dev
[4] раз: https://habr.com/ru/post/338264/
[5] два: https://habr.com/ru/post/338404/
[6] три: https://habr.com/ru/post/338634/
[7] эту статью: https://habr.com/ru/post/274463/
[8] официальный гайдлайн по написанию драйвера: https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/
[9] спецификация: https://uefi.org/sites/default/files/resources/UEFI%20Spec%202.8B%20May%202020.pdf
[10] тут: https://habr.com/ru/post/404511/
[11] ExitBootServices(): https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/5_uefi_services/readme.3/5312_exitbootservices
[12] сюда: https://tianocore-training.github.io/Lesson-3/
[13] EDKII: https://github.com/tianocore/edk2
[14] VisualUEFI: https://github.com/ionescu007/VisualUefi
[15] Coreboot: https://doc.coreboot.org/index.html
[16] Ссылка: https://tianocore-training.github.io/Lesson-0/
[17] runtime драйвера: https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/7_driver_entry_point/711_runtime_drivers
[18] виртуализировать: https://ru.wikipedia.org/wiki/%D0%A2%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D0%B0_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86#:~:text=%D0%92%D0%B8%D1%80%D1%82%D1%83%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5%20%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%B0%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D1%8E%D1%82%D1%81%D1%8F%20%D0%B2%D1%8B%D0%BF%D0%BE%D0%BB%D0%BD%D1%8F%D1%8E%D1%89%D0%B8%D0%BC%D1%81%D1%8F%20%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BE%D0%BC,%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%D0%B0%20%D0%BA%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D0%BC%20%D0%B2%20%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8.
[19] SetVirtualAddressMap(): https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/5_uefi_services/readme.3/5313_setvirtualaddressmap
[20] Ссылка: https://uefi.org/sites/default/files/resources/UEFI-Plugfest-WindowsBootEnvironment.pdf
[21] тут: https://habr.com/ru/post/185764/
[22] NASM: https://www.nasm.us
[23] https://www.nasm.us/pub/nasm/releasebuilds/2.15.02/win64/: https://www.nasm.us/pub/nasm/releasebuilds/2.15.02/win64/
[24] тут: https://github.com/LongSoft/CrScreenshotDxe/blob/master/CrScreenshotDxe.c
[25] github: https://github.com/Dabudabot/hello-uefi
[26] UEFITool: https://github.com/LongSoft/UEFITool
[27] тут: https://github.com/pbatard/efifs/wiki/Adding-a-driver-to-a-UEFI-firmware
[28] Источник: https://habr.com/ru/post/511172/?utm_source=habrahabr&utm_medium=rss&utm_campaign=511172
Нажмите здесь для печати.