В предыдущей статье «Разработка 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 <Protocol/HiiPackageList.h>
…
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 и список компилируемых файлов:

<ItemGroup>
  <ClCompile Include="$(EDK_PATH)\MdePkg\Library\BaseSafeIntLib\SafeIntLib.c" />
  <ClCompile Include="$(EDK_PATH)\MdeModulePkg\Library\BaseBmpSupportLib\BmpSupportLib.c" />
</ItemGroup>

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

В принципе, этого для наших нужд уже достаточно. Скомпилируем все библиотеки (“Build Solution” или Ctrl+Shift+B), перейдём к samples\samples.sln, и там в файле samples\UefiApplication\helloapp.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 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-изображение в ресурсы проекта:

  <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 (положив его в \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 всё равно пропадёт.

Комментарии (9)


  1. nckma
    01.09.2021 13:06
    +4

    Мне почему-то кажется, что уже очень скоро новые материнские платы будут иметь опцию EFI SecureBoot по умолчанию включенной. А может быть ее и выключить будет нельзя.

    А значит запустить свое EFI приложение можно будет только подменив в биосе базы PK, KEK, DB. В общем, разрабатываеть свое EFI приложение без цифровой подписи Microsoft будет довольно затруднительно.


    1. BiosUefi
      01.09.2021 14:21
      +4

      Смешнее всего, что с активацией SecureBoot становится невозможно пользоваться и shell, все её встроенные команды ( ls, mem, pci) тоже "не подписаны"((((


    1. NeonMercury
      01.09.2021 16:49
      +2

      Я помню, что на прошлой работе мы дописывали ключи в PK, KEK, DB. И, таким образом грузились как efi, подписанные MS, так и efi, подписанные нашим ключом. Делали это на Lenovo Thinkpad`ах, но прошло уже лет 6 и я не помню деталей реализации. Помню только, что это не было открытым решением из документации, а чем-то вроде "добавить через ";" второй сертификат".

      Возможно, что там была какая-то особенность именно на thinkpad`ах. Если кто-то что-то подобное знает, мне будет тоже интересно прочитать - вспомнить, потому что сейчас я не смог найти никакой информации на эту тему.



      1. nckma
        01.09.2021 17:07

        Сказать по правде, мое мнение, что весь этот SecureBoot в UEFI это просто редкая ерунда. Главная причина такого моего мнения это то, что основной придуманный механизм блокировки скомпрометированных загрузчиков не может использоваться и дальше. После инцидента 2020 года (серия багов BootHole), когда пришлось отозвать более 150 загрузчиков база DBX (UEFI Revocation List) выросла почти до 15 килобайт из 32кб зарезервированных в NVRAM материнки. Скачать Revocation List DBX файл можно на сайте UEFI https://uefi.org/revocationlistfile

        Еще такой случай, как BootHole и места для DBX не останется совсем. Поэтому начали придумывать надстройку над всей этой SecureBoot в виде SBAT (SecureBoot Advanced Targeting) типа чтоб одной записью в NVRAM блокировать список однотипных загрузчиков скажем одной версии. Новые загрузчики shim v15.4 уже содержат SBAT таблицы, да и новый grub2.06 должен их иметь.


        1. CodeRush
          01.09.2021 17:18

          Это все ерунда, конечно, но это наша единственная работающая и относительно универсальная ерунда на ПК. Проблему с разрастанием DBX можно достаточно несложно решить разделением ее на статическую и динамическую части, с добавлением статической (того самого revocation list) в саму прошивку, и модификацией драйвера SecureBoot, чтобы он проверял обе.

          У UEFI SecureBoot действительно не очень хороший дизайн, но с практической точки зрения вот такая безопасная загрузка намного лучше, чем никакой, да и у конкурентов не особенно много более качественных альтернативных вариантов. Да, никто не думал в 2011 году, что придется отозвать две сотни подписей одним махом, но эта проблема уже известная, и решаемая, и ее даже решили частично введением в UEFI 2.4 Audit Mode и Deployment Mode с последующим отключением доверия к UEFI CA (которым все эти две сотни загрузчиков все и подписаны). BootHole — это отлично, на самом деле, потому что показывает, что улучшения в системе отзыва и упрощения цепочки доверия надо бы внедрять за 1-2 года, а не за 5-7, как сейчас.


    1. CodeRush
      01.09.2021 16:53
      +2

      Выключить все равно будет можно, это прописано в спецификации, которую настолько сильно никто уже обновлять не станет, переживать не надо. Да и дураков там тоже нет, каждую итерацию в МС подписывать.


      1. dimka11
        02.09.2021 20:15

        На ARM процессорах вроде не отключаемая, уже кучу лет как


        1. CodeRush
          03.09.2021 02:13
          +2

          Архитектура процессора там не при чем, на ARM есть сейчас стандарт Base Boot Security Requirements, в котором отключаемый SecureBoot прописан явно. Вы говорите про устройства типа смартфонов Nokia с Windows Mobile и прочие планшеты Microsoft Surface, и да, на них в свое время не было возможность отключить SecureBoot без костылей, но они и совместимость со спецификацией UEFI никогда не декларировали, а прошивки для ПК делают именно это вполне явно.