Более удобная разработка 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-IIEDK-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-IIGlueLibguid.c и безусловно линкуются к любому проекту. Значит, нам понадобится добавить в этот файл две недостающие строчки:

#include <Protocol/HiiPackageList.h>
…
EFI_GUID gEfiHiiPackageListProtocolGuid = EFI_HII_PACKAGE_LIST_PROTOCOL_GUID;

Функция TranslateBmpToGopBlt определена в edk2MdeModulePkgLibraryBaseBmpSupportLibBmpSupportLib.c, и этот файл нужно добавить в какой-нибудь из проектов, лежащих в каталоге EDK-II. Не будем лениться, и создадим новый проект EDK-IIBaseBmpSupportLibBaseBmpSupportLib.vcxproj — я скопировал EDK-IIUefiSortLibUefiSortLib.vcxproj и лишь заменил в нём ProjectGuid и список компилируемых файлов:

<ItemGroup>
  <ClCompile Include="$(EDK_PATH)MdePkgLibraryBaseSafeIntLibSafeIntLib.c" />
  <ClCompile Include="$(EDK_PATH)MdeModulePkgLibraryBaseBmpSupportLibBmpSupportLib.c" />
</ItemGroup>

TranslateBmpToGopBlt пользуется функцией SafeUint32Mult из состава BaseSafeIntLib, которой в VisualUefi тоже нет; поэтому в создаваемый проект придётся добавить файл SafeIntLib.c с определением недостающей функции.

В принципе, этого для наших нужд уже достаточно. Скомпилируем все библиотеки (“Build Solution” или Ctrl+Shift+B), перейдём к samplessamples.sln, и там в файле samplesUefiApplicationhelloapp.c добавим #include <Library/BmpSupportLib.h> и все остальные объявления из примера в прошлой статье, а содержимое 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 sectionn");
    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 BMPn");
    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 GOPn");
    return EFI_NOT_STARTED;
}
UINT32* video = (UINT32*)(UINTN)gop->Mode->FrameBufferBase;

// дальше идёт без изменений код из прошлой статьи, начиная с вызова AllocatePages()

Кроме этого, в настройках проекта UefiApplication нужно добавить в “Additional Include Directories” путь $(EDK_PATH)MdeModulePkgInclude, и в “Additional Dependencies” — библиотеку BaseBmpSupportLib.lib

Обратите внимание на подсказки IDE, которых без VisualUefi мы бы не получили:

Осталось добавить BMP-изображение в ресурсы проекта:

  <ItemGroup>
    <ResourceCompile Include="UefiApplication.rc" />
    <Image Include="ruvds.bmp" />
  </ItemGroup>

В файле UefiApplication.rc достаточно одной строчки:

1 HII "ruvds.bmp"

Всё, теперь можно нажимать F5, UEFI-приложение очень быстро (по сравнению с edk2) скомпилируется, тогда запустится эмулятор с UEFI Shell, и в нём для запуска нашего приложения нужно ввести fs1:UefiApplication.efi

Ничего страшного: если загружаться непосредственно в UefiApplication.efi (положив его в EFIBOOTbootx64.efi), то не полностью стёртого фона видно не будет :)

VisualUefi ограничен в возможностях — например, создать в этом фреймворке приложение с несколькими ресурсами было бы затруднительно — но для простых UEFI-приложений он подходит идеально: избавляет от лишних зависимостей, таких как Python; ускоряет написание кода в IDE; и ускоряет его компиляцию. Самый досадный недостаток VisualUefi — это то, что лежащая в репозитории версия эмулятора не поддерживает интерактивную отладку.

P.S.: Версия edk2 двухсполовинойлетней давности, используемая в VisualUefi, предшествует удалению из edk2 библиотеки StdLib, содержавшей в т.ч. тригонометрические функции. Это означает, что вместо использования SinCos.asm можно скомпилировать StdLib в составе VisualUefi, и добавить UefiStdLib.lib в “Additional Dependencies” проекта. В моём форке это проделано, но вряд ли имеет смысл описывать это подробнее, потому что при обновлении edk2 в составе VisualUefi из неё StdLib всё равно пропадёт.

Источник 📢