Более удобная разработка 64-битного графического UEFI приложения
В предыдущей статье «Разработка 64-битного графического UEFI-приложения в Visual Studio 2019» VS задействовался лишь в двух аспектах: как редактор для кода — «продвинутый Блокнот» — и как отладчик для скомпилированного приложения. Всё остальное — управление зависимостями, настройки компиляции и т.д. — было отдано на откуп фреймворку edk2. Хотелось бы использовать мощь VS как IDE более полно: как минимум заиметь в редакторе кода автодополнение.
Бонусом получим более быструю компиляцию проекта: edk2 ищет изменившиеся файлы во всём своём полугигабайтном дереве, что, очевидно, излишне.
По сути, UEFI-приложение — это обычный PE, такого же формата, как любой исполнимый файл для Windows. Его особенность в том, что у него нет импортов — все функции UEFI вызываются через глобальные указатели; и в том, что он компилируется без стандартной библиотеки, MSVCRT или аналогичной. Это значит, что компиляция UEFI-приложения отличается от компиляции обычного приложения для Windows только ключом линкера /NODEFAULTLIB
(опция «Ignore All Default Libraries» в настройках проекта); настройкой зависимостей; и хитроумным кодом инициализации, который поместит указатели на стандартные протоколы UEFI в нужные глобальные переменные. Всё это реализовано во фреймворке VisualUefi от Алекса Ионеску, известного как соавтор Марка Руссиновича по книге «Windows Internals», начиная с пятого издания (2009). Стоит отметить, что VisualUefi использует версию edk2 двухсполовинойлетней давности — до добавления в сборочные скрипты edk2 поддержки VS2019 — и, тем не менее, отлично собирается в VS2019, потому что сборочными скриптами edk2 не пользуется.
Ионеску требовал, чтобы у пользователя VisualUefi в системе уже был установлен NASM и задана системная переменная окружения NASM_PREFIX
. Первое, что я добавил в мой форк VisualUefi — это бинарники NASM и автоматическое задание NASM_PREFIX
в настройках проекта. Это значит, что для развёртывания VisualUefi не нужно ничего, кроме команды:
git clone --depth 1 --recursive --shallow-submodules https://github.com/tyomitch/VisualUefi
80 МБ трафика, 350 МБ на диске — вдвое компактнее, чем фреймворк из прошлой статьи!
Что же такое делают сборочные скрипты edk2, без которых VisualUefi позволяет обойтись? В последней версии UEFI-приложения, созданного в предыдущей статье, мы задействовали ресурсы HII, а конкретнее, BMP-изображение. Ресурсы HII имеют определённую в спецификации UEFI структуру (EFI_HII_PACKAGE_LIST_HEADER
и т.п.), и сборочные скрипты, кроме прочего, создают из всех ресурсов приложения один блоб, который кладётся в PE-файл как ресурс типа «HII» с идентификатором 1. Загрузчик UEFI находит такой ресурс в PE-файле, загружает его, и регистрирует указатель на начало ресурса как протокол gEfiHiiPackageListProtocolGuid
.
Собрать нужную для HII структуру ресурсов встроенными средствами VS мы, конечно, не сможем. Но для наших целей — одно BMP-изображение — этого и не нужно: достаточно, чтобы ресурсом типа «HII» было это изображение, и тогда UEFI нам его загрузит. Стандартная функция TranslateBmpToGopBlt
превратит BMP-изображение в такую же структуру EFI_IMAGE_INPUT
, которую в предыдущей статье мы заполняли последовательностью из двух вызовов HiiDatabase->NewPackageList
и HiiImage->GetImage
. Но вот же беда — функция TranslateBmpToGopBlt
в мастер-версии VisualUefi недоступна; не определён и протокол gEfiHiiPackageListProtocolGuid
. Придётся разобраться, как VisualUefi устроен, и как добавить в него всё недостающее. Библиотеки UEFI собираются из EDK-II\EDK-II.sln
В edk2, когда мы пишем в файле проекта:
[Protocols]
gEfiHiiDatabaseProtocolGuid ## CONSUMES
gEfiHiiImageProtocolGuid ## CONSUMES
gEfiHiiPackageListProtocolGuid ## CONSUMES
—то сборочные скрипты создают файл AutoGen.c со строками:
…
GLOBAL_REMOVE_IF_UNREFERENCED EFI_GUID gEfiHiiDatabaseProtocolGuid = {0xef9fc172, 0xa1b2, 0x4693, {0xb3, 0x27, 0x6d, 0x32, 0xfc, 0x41, 0x60, 0x42}};
GLOBAL_REMOVE_IF_UNREFERENCED EFI_GUID gEfiHiiImageProtocolGuid = {0x31a6406a, 0x6bdf, 0x4e46, {0xb2, 0xa2, 0xeb, 0xaa, 0x89, 0xc4, 0x09, 0x20}};
GLOBAL_REMOVE_IF_UNREFERENCED EFI_GUID gEfiHiiPackageListProtocolGuid = { 0x6a1ee763, 0xd47a, 0x43b4, {0xaa, 0xbe, 0xef, 0x1d, 0xe2, 0xab, 0x56, 0xfc}};
…
В VisualUefi, естественно, подобного динамически генерируемого кода быть не может. Вместо этого все нужные, по мнению Ионеску, протоколы определены в EDK-II\GlueLib\guid.c и безусловно линкуются к любому проекту. Значит, нам понадобится добавить в этот файл две недостающие строчки:
#include
…
EFI_GUID gEfiHiiPackageListProtocolGuid = EFI_HII_PACKAGE_LIST_PROTOCOL_GUID;
Функция TranslateBmpToGopBlt
определена в edk2\MdeModulePkg\Library\BaseBmpSupportLib\BmpSupportLib.c, и этот файл нужно добавить в какой-нибудь из проектов, лежащих в каталоге EDK-II. Не будем лениться, и создадим новый проект EDK-II\BaseBmpSupportLib\BaseBmpSupportLib.vcxproj — я скопировал EDK-II\UefiSortLib\UefiSortLib.vcxproj и лишь заменил в нём ProjectGuid и список компилируемых файлов:
TranslateBmpToGopBlt
пользуется функцией SafeUint32Mult
из состава BaseSafeIntLib, которой в VisualUefi тоже нет; поэтому в создаваемый проект придётся добавить файл SafeIntLib.c с определением недостающей функции.
В принципе, этого для наших нужд уже достаточно. Скомпилируем все библиотеки («Build Solution» или Ctrl+Shift+B), перейдём к samples\samples.sln, и там в файле samples\UefiApplication\helloapp.c добавим #include
и все остальные объявления из примера в прошлой статье, а содержимое UefiMain
заменим на:
// эти объявления взяты без изменений из прошлой статьи
EFI_STATUS efiStatus;
EFI_GUID gopGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_GRAPHICS_OUTPUT_PROTOCOL* gop;
double sin, cos;
// изменения начинаются отсюда
VOID* PackageList;
UINTN size;
EFI_PHYSICAL_ADDRESS Buffer;
efiStatus = gBS->OpenProtocol(ImageHandle, &gEfiHiiPackageListProtocolGuid,
&PackageList, ImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
if (EFI_ERROR(efiStatus)) {
Print(L"HII Image Package not found in PE/COFF resource section\n");
return efiStatus;
}
EFI_IMAGE_INPUT Image = { 0 };
UINTN PixelHeight;
UINTN PixelWidth;
efiStatus = TranslateBmpToGopBlt(PackageList, *(UINT32*)((UINT8*)PackageList + 2),
&Image.Bitmap, &size, &PixelHeight, &PixelWidth);
if (EFI_ERROR(efiStatus)) {
Print(L"Unable to translate BMP\n");
return EFI_NOT_STARTED;
}
Image.Height = (UINT16)PixelHeight;
Image.Width = (UINT16)PixelWidth;
// этот код взят без изменений из прошлой статьи
efiStatus = gBS->LocateProtocol(&gopGuid, NULL, (void**)&gop);
if (EFI_ERROR(efiStatus)) {
Print(L"Unable to locate GOP\n");
return EFI_NOT_STARTED;
}
UINT32* video = (UINT32*)(UINTN)gop->Mode->FrameBufferBase;
// дальше идёт без изменений код из прошлой статьи, начиная с вызова AllocatePages()
Кроме этого, в настройках проекта UefiApplication нужно добавить в «Additional Include Directories» путь $(EDK_PATH)\MdeModulePkg\Include, и в «Additional Dependencies» — библиотеку BaseBmpSupportLib.lib
Обратите внимание на подсказки IDE, которых без VisualUefi мы бы не получили:
Осталось добавить BMP-изображение в ресурсы проекта:
В файле UefiApplication.rc достаточно одной строчки:
1 HII "ruvds.bmp"
Всё, теперь можно нажимать F5, UEFI-приложение очень быстро (по сравнению с edk2) скомпилируется, тогда запустится эмулятор с UEFI Shell, и в нём для запуска нашего приложения нужно ввести fs1:UefiApplication.efi
Ничего страшного: если загружаться непосредственно в UefiApplication.efi (положив его в \EFI\BOOT\bootx64.efi), то не полностью стёртого фона видно не будет :)
VisualUefi ограничен в возможностях — например, создать в этом фреймворке приложение с несколькими ресурсами было бы затруднительно —, но для простых UEFI-приложений он подходит идеально: избавляет от лишних зависимостей, таких как Python; ускоряет написание кода в IDE; и ускоряет его компиляцию. Самый досадный недостаток VisualUefi — это то, что лежащая в репозитории версия эмулятора не поддерживает интерактивную отладку.
P.S.: Версия edk2 двухсполовинойлетней давности, используемая в VisualUefi, предшествует удалению из edk2 библиотеки StdLib, содержавшей в т.ч. тригонометрические функции. Это означает, что вместо использования SinCos.asm можно скомпилировать StdLib в составе VisualUefi, и добавить UefiStdLib.lib в «Additional Dependencies» проекта. В моём форке это проделано, но вряд ли имеет смысл описывать это подробнее, потому что при обновлении edk2 в составе VisualUefi из неё StdLib всё равно пропадёт.